@lexical/markdown 0.2.5 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LexicalMarkdown.d.ts +84 -6
- package/LexicalMarkdown.dev.js +685 -1220
- package/LexicalMarkdown.js.flow +84 -6
- package/LexicalMarkdown.prod.js +21 -39
- package/README.md +88 -6
- package/package.json +8 -8
package/LexicalMarkdown.dev.js
CHANGED
|
@@ -6,12 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
|
-
var code = require('@lexical/code');
|
|
10
|
-
var list = require('@lexical/list');
|
|
11
9
|
var lexical = require('lexical');
|
|
10
|
+
var code = require('@lexical/code');
|
|
12
11
|
var link = require('@lexical/link');
|
|
12
|
+
var list = require('@lexical/list');
|
|
13
13
|
var richText = require('@lexical/rich-text');
|
|
14
|
-
var text = require('@lexical/text');
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
16
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
@@ -21,911 +20,394 @@ var text = require('@lexical/text');
|
|
|
21
20
|
*
|
|
22
21
|
*
|
|
23
22
|
*/
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const SEPARATOR_BETWEEN_TEXT_AND_NON_TEXT_NODES = '\u0004'; // Select an unused unicode character to separate text and non-text nodes.
|
|
39
|
-
|
|
40
|
-
const SEPARATOR_LENGTH = SEPARATOR_BETWEEN_TEXT_AND_NON_TEXT_NODES.length;
|
|
41
|
-
const spaceTrigger = {
|
|
42
|
-
triggerKind: 'space_trigger',
|
|
43
|
-
triggerString: '\u0020'
|
|
44
|
-
}; // Future todo: add support for ``` + carriage return either inside or not inside code block. Should toggle between.
|
|
45
|
-
// const codeBlockTrigger : AutoFormatTrigger = {
|
|
46
|
-
// triggerKind: 'codeBlock_trigger',
|
|
47
|
-
// triggerString: '```', // + new paragraph element or new code block element.
|
|
48
|
-
// };
|
|
49
|
-
|
|
50
|
-
const triggers = [spaceTrigger
|
|
51
|
-
/*, codeBlockTrigger*/
|
|
52
|
-
]; // Future Todo: speed up performance by having non-capture group variations of the regex.
|
|
53
|
-
|
|
54
|
-
const autoFormatBase = {
|
|
55
|
-
markdownFormatKind: null,
|
|
56
|
-
regEx: /(?:)/,
|
|
57
|
-
regExForAutoFormatting: /(?:)/,
|
|
58
|
-
requiresParagraphStart: false
|
|
59
|
-
};
|
|
60
|
-
const paragraphStartBase = { ...autoFormatBase,
|
|
61
|
-
requiresParagraphStart: true
|
|
62
|
-
};
|
|
63
|
-
const markdownHeader1 = { ...paragraphStartBase,
|
|
64
|
-
export: createHeadingExport(1),
|
|
65
|
-
markdownFormatKind: 'paragraphH1',
|
|
66
|
-
regEx: /^(?:# )/,
|
|
67
|
-
regExForAutoFormatting: /^(?:# )/
|
|
68
|
-
};
|
|
69
|
-
const markdownHeader2 = { ...paragraphStartBase,
|
|
70
|
-
export: createHeadingExport(2),
|
|
71
|
-
markdownFormatKind: 'paragraphH2',
|
|
72
|
-
regEx: /^(?:## )/,
|
|
73
|
-
regExForAutoFormatting: /^(?:## )/
|
|
74
|
-
};
|
|
75
|
-
const markdownHeader3 = { ...paragraphStartBase,
|
|
76
|
-
export: createHeadingExport(3),
|
|
77
|
-
markdownFormatKind: 'paragraphH3',
|
|
78
|
-
regEx: /^(?:### )/,
|
|
79
|
-
regExForAutoFormatting: /^(?:### )/
|
|
80
|
-
};
|
|
81
|
-
const markdownHeader4 = { ...paragraphStartBase,
|
|
82
|
-
export: createHeadingExport(4),
|
|
83
|
-
markdownFormatKind: 'paragraphH4',
|
|
84
|
-
regEx: /^(?:#### )/,
|
|
85
|
-
regExForAutoFormatting: /^(?:#### )/
|
|
86
|
-
};
|
|
87
|
-
const markdownHeader5 = { ...paragraphStartBase,
|
|
88
|
-
export: createHeadingExport(5),
|
|
89
|
-
markdownFormatKind: 'paragraphH5',
|
|
90
|
-
regEx: /^(?:##### )/,
|
|
91
|
-
regExForAutoFormatting: /^(?:##### )/
|
|
92
|
-
};
|
|
93
|
-
const markdownBlockQuote = { ...paragraphStartBase,
|
|
94
|
-
export: blockQuoteExport,
|
|
95
|
-
markdownFormatKind: 'paragraphBlockQuote',
|
|
96
|
-
regEx: /^(?:> )/,
|
|
97
|
-
regExForAutoFormatting: /^(?:> )/
|
|
98
|
-
};
|
|
99
|
-
const markdownUnorderedListDash = { ...paragraphStartBase,
|
|
100
|
-
export: listExport,
|
|
101
|
-
markdownFormatKind: 'paragraphUnorderedList',
|
|
102
|
-
regEx: /^(\s{0,10})(?:- )/,
|
|
103
|
-
regExForAutoFormatting: /^(\s{0,10})(?:- )/
|
|
104
|
-
};
|
|
105
|
-
const markdownUnorderedListAsterisk = { ...paragraphStartBase,
|
|
106
|
-
export: listExport,
|
|
107
|
-
markdownFormatKind: 'paragraphUnorderedList',
|
|
108
|
-
regEx: /^(\s{0,10})(?:\* )/,
|
|
109
|
-
regExForAutoFormatting: /^(\s{0,10})(?:\* )/
|
|
110
|
-
};
|
|
111
|
-
const markdownCodeBlock = { ...paragraphStartBase,
|
|
112
|
-
export: codeBlockExport,
|
|
113
|
-
markdownFormatKind: 'paragraphCodeBlock',
|
|
114
|
-
regEx: /^(```)$/,
|
|
115
|
-
regExForAutoFormatting: /^(```)([a-z]*)( )/
|
|
116
|
-
};
|
|
117
|
-
const markdownOrderedList = { ...paragraphStartBase,
|
|
118
|
-
export: listExport,
|
|
119
|
-
markdownFormatKind: 'paragraphOrderedList',
|
|
120
|
-
regEx: /^(\s{0,10})(\d+)\.\s/,
|
|
121
|
-
regExForAutoFormatting: /^(\s{0,10})(\d+)\.\s/
|
|
122
|
-
};
|
|
123
|
-
const markdownHorizontalRule = { ...paragraphStartBase,
|
|
124
|
-
markdownFormatKind: 'horizontalRule',
|
|
125
|
-
regEx: /^(?:\*\*\*)$/,
|
|
126
|
-
regExForAutoFormatting: /^(?:\*\*\* )/
|
|
127
|
-
};
|
|
128
|
-
const markdownHorizontalRuleUsingDashes = { ...paragraphStartBase,
|
|
129
|
-
markdownFormatKind: 'horizontalRule',
|
|
130
|
-
regEx: /^(?:---)$/,
|
|
131
|
-
regExForAutoFormatting: /^(?:--- )/
|
|
132
|
-
};
|
|
133
|
-
const markdownInlineCode = { ...autoFormatBase,
|
|
134
|
-
exportFormat: 'code',
|
|
135
|
-
exportTag: '`',
|
|
136
|
-
markdownFormatKind: 'code',
|
|
137
|
-
regEx: /(`)(\s*)([^`]*)(\s*)(`)()/,
|
|
138
|
-
regExForAutoFormatting: /(`)(\s*\b)([^`]*)(\b\s*)(`)(\s)$/
|
|
139
|
-
};
|
|
140
|
-
const markdownBold = { ...autoFormatBase,
|
|
141
|
-
exportFormat: 'bold',
|
|
142
|
-
exportTag: '**',
|
|
143
|
-
markdownFormatKind: 'bold',
|
|
144
|
-
regEx: /(\*\*)(\s*)([^\*\*]*)(\s*)(\*\*)()/,
|
|
145
|
-
regExForAutoFormatting: /(\*\*)(\s*\b)([^\*\*]*)(\b\s*)(\*\*)(\s)$/
|
|
146
|
-
};
|
|
147
|
-
const markdownItalic = { ...autoFormatBase,
|
|
148
|
-
exportFormat: 'italic',
|
|
149
|
-
exportTag: '*',
|
|
150
|
-
markdownFormatKind: 'italic',
|
|
151
|
-
regEx: /(\*)(\s*)([^\*]*)(\s*)(\*)()/,
|
|
152
|
-
regExForAutoFormatting: /(\*)(\s*\b)([^\*]*)(\b\s*)(\*)(\s)$/
|
|
153
|
-
};
|
|
154
|
-
const markdownBold2 = { ...autoFormatBase,
|
|
155
|
-
exportFormat: 'bold',
|
|
156
|
-
exportTag: '_',
|
|
157
|
-
markdownFormatKind: 'bold',
|
|
158
|
-
regEx: /(__)(\s*)([^__]*)(\s*)(__)()/,
|
|
159
|
-
regExForAutoFormatting: /(__)(\s*)([^__]*)(\s*)(__)(\s)$/
|
|
160
|
-
};
|
|
161
|
-
const markdownItalic2 = { ...autoFormatBase,
|
|
162
|
-
exportFormat: 'italic',
|
|
163
|
-
exportTag: '_',
|
|
164
|
-
markdownFormatKind: 'italic',
|
|
165
|
-
regEx: /(_)()([^_]*)()(_)()/,
|
|
166
|
-
regExForAutoFormatting: /(_)()([^_]*)()(_)(\s)$/ // Maintain 7 groups.
|
|
167
|
-
|
|
168
|
-
}; // Markdown does not support underline, but we can allow folks to use
|
|
169
|
-
// the HTML tags for underline.
|
|
170
|
-
|
|
171
|
-
const fakeMarkdownUnderline = { ...autoFormatBase,
|
|
172
|
-
exportFormat: 'underline',
|
|
173
|
-
exportTag: '<u>',
|
|
174
|
-
exportTagClose: '</u>',
|
|
175
|
-
markdownFormatKind: 'underline',
|
|
176
|
-
regEx: /(\<u\>)(\s*)([^\<]*)(\s*)(\<\/u\>)()/,
|
|
177
|
-
regExForAutoFormatting: /(\<u\>)(\s*\b)([^\<]*)(\b\s*)(\<\/u\>)(\s)$/
|
|
178
|
-
};
|
|
179
|
-
const markdownStrikethrough = { ...autoFormatBase,
|
|
180
|
-
exportFormat: 'strikethrough',
|
|
181
|
-
exportTag: '~~',
|
|
182
|
-
markdownFormatKind: 'strikethrough',
|
|
183
|
-
regEx: /(~~)(\s*)([^~~]*)(\s*)(~~)()/,
|
|
184
|
-
regExForAutoFormatting: /(~~)(\s*\b)([^~~]*)(\b\s*)(~~)(\s)$/
|
|
185
|
-
};
|
|
186
|
-
const markdownStrikethroughItalicBold = { ...autoFormatBase,
|
|
187
|
-
markdownFormatKind: 'strikethrough_italic_bold',
|
|
188
|
-
regEx: /(~~_\*\*)(\s*\b)([^~~_\*\*][^\*\*_~~]*)(\b\s*)(\*\*_~~)()/,
|
|
189
|
-
regExForAutoFormatting: /(~~_\*\*)(\s*\b)([^~~_\*\*][^\*\*_~~]*)(\b\s*)(\*\*_~~)(\s)$/
|
|
190
|
-
};
|
|
191
|
-
const markdownItalicbold = { ...autoFormatBase,
|
|
192
|
-
markdownFormatKind: 'italic_bold',
|
|
193
|
-
regEx: /(_\*\*)(\s*\b)([^_\*\*][^\*\*_]*)(\b\s*)(\*\*_)/,
|
|
194
|
-
regExForAutoFormatting: /(_\*\*)(\s*\b)([^_\*\*][^\*\*_]*)(\b\s*)(\*\*_)(\s)$/
|
|
195
|
-
};
|
|
196
|
-
const markdownStrikethroughItalic = { ...autoFormatBase,
|
|
197
|
-
markdownFormatKind: 'strikethrough_italic',
|
|
198
|
-
regEx: /(~~_)(\s*)([^~~_][^_~~]*)(\s*)(_~~)/,
|
|
199
|
-
regExForAutoFormatting: /(~~_)(\s*)([^~~_][^_~~]*)(\s*)(_~~)(\s)$/
|
|
200
|
-
};
|
|
201
|
-
const markdownStrikethroughBold = { ...autoFormatBase,
|
|
202
|
-
markdownFormatKind: 'strikethrough_bold',
|
|
203
|
-
regEx: /(~~\*\*)(\s*\b)([^~~\*\*][^\*\*~~]*)(\b\s*)(\*\*~~)/,
|
|
204
|
-
regExForAutoFormatting: /(~~\*\*)(\s*\b)([^~~\*\*][^\*\*~~]*)(\b\s*)(\*\*~~)(\s)$/
|
|
205
|
-
};
|
|
206
|
-
const markdownLink = { ...autoFormatBase,
|
|
207
|
-
markdownFormatKind: 'link',
|
|
208
|
-
regEx: /(\[)([^\]]*)(\]\()([^)]*)(\)*)()/,
|
|
209
|
-
regExForAutoFormatting: /(\[)([^\]]*)(\]\()([^)]*)(\)*)(\s)$/
|
|
210
|
-
};
|
|
211
|
-
const allMarkdownCriteriaForTextNodes = [// Place the combination formats ahead of the individual formats.
|
|
212
|
-
// Combos
|
|
213
|
-
markdownStrikethroughItalicBold, markdownItalicbold, markdownStrikethroughItalic, markdownStrikethroughBold, // Individuals
|
|
214
|
-
markdownInlineCode, markdownBold, markdownItalic, // Must appear after markdownBold
|
|
215
|
-
markdownBold2, markdownItalic2, // Must appear after markdownBold2.
|
|
216
|
-
fakeMarkdownUnderline, markdownStrikethrough, markdownLink];
|
|
217
|
-
const allMarkdownCriteriaForParagraphs = [markdownHeader1, markdownHeader2, markdownHeader3, markdownHeader4, markdownHeader5, markdownBlockQuote, markdownUnorderedListDash, markdownUnorderedListAsterisk, markdownOrderedList, markdownCodeBlock, markdownHorizontalRule, markdownHorizontalRuleUsingDashes];
|
|
218
|
-
const allMarkdownCriteria = [...allMarkdownCriteriaForParagraphs, ...allMarkdownCriteriaForTextNodes];
|
|
219
|
-
function getAllTriggers() {
|
|
220
|
-
return triggers;
|
|
221
|
-
}
|
|
222
|
-
function getAllMarkdownCriteriaForParagraphs() {
|
|
223
|
-
return allMarkdownCriteriaForParagraphs;
|
|
224
|
-
}
|
|
225
|
-
function getAllMarkdownCriteriaForTextNodes() {
|
|
226
|
-
return allMarkdownCriteriaForTextNodes;
|
|
227
|
-
}
|
|
228
|
-
function getAllMarkdownCriteria() {
|
|
229
|
-
return allMarkdownCriteria;
|
|
23
|
+
function indexBy(list, callback) {
|
|
24
|
+
const index = {};
|
|
25
|
+
|
|
26
|
+
for (const item of list) {
|
|
27
|
+
const key = callback(item);
|
|
28
|
+
|
|
29
|
+
if (index[key]) {
|
|
30
|
+
index[key].push(item);
|
|
31
|
+
} else {
|
|
32
|
+
index[key] = [item];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return index;
|
|
230
37
|
}
|
|
231
|
-
function
|
|
38
|
+
function transformersByType(transformers) {
|
|
39
|
+
const byType = indexBy(transformers, t => t.type);
|
|
232
40
|
return {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
markdownFormatKind: 'noTransformation',
|
|
240
|
-
regEx: /(?:)/,
|
|
241
|
-
// Empty reg ex.
|
|
242
|
-
regExForAutoFormatting: /(?:)/,
|
|
243
|
-
// Empty reg ex.
|
|
244
|
-
requiresParagraphStart: null
|
|
245
|
-
},
|
|
246
|
-
patternMatchResults: {
|
|
247
|
-
regExCaptureGroups: []
|
|
248
|
-
},
|
|
249
|
-
textNodeWithOffset,
|
|
250
|
-
triggerState
|
|
41
|
+
// $FlowFixMe
|
|
42
|
+
element: byType.element,
|
|
43
|
+
// $FlowFixMe
|
|
44
|
+
textFormat: byType['text-format'],
|
|
45
|
+
// $FlowFixMe
|
|
46
|
+
textMatch: byType['text-match']
|
|
251
47
|
};
|
|
252
48
|
}
|
|
253
|
-
function resetScanningContext(scanningContext) {
|
|
254
|
-
scanningContext.joinedText = null;
|
|
255
|
-
scanningContext.markdownCriteria = {
|
|
256
|
-
markdownFormatKind: 'noTransformation',
|
|
257
|
-
regEx: /(?:)/,
|
|
258
|
-
// Empty reg ex.
|
|
259
|
-
regExForAutoFormatting: /(?:)/,
|
|
260
|
-
// Empty reg ex.
|
|
261
|
-
requiresParagraphStart: null
|
|
262
|
-
};
|
|
263
|
-
scanningContext.patternMatchResults = {
|
|
264
|
-
regExCaptureGroups: []
|
|
265
|
-
};
|
|
266
|
-
scanningContext.triggerState = null;
|
|
267
|
-
scanningContext.textNodeWithOffset = null;
|
|
268
|
-
return scanningContext;
|
|
269
|
-
}
|
|
270
|
-
function getCodeBlockCriteria() {
|
|
271
|
-
return markdownCodeBlock;
|
|
272
|
-
}
|
|
273
|
-
function getPatternMatchResultsForCriteria(markdownCriteria, scanningContext, parentElementNode) {
|
|
274
|
-
if (markdownCriteria.requiresParagraphStart === true) {
|
|
275
|
-
return getPatternMatchResultsForParagraphs(markdownCriteria, scanningContext);
|
|
276
|
-
}
|
|
277
49
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
52
|
+
*
|
|
53
|
+
* This source code is licensed under the MIT license found in the
|
|
54
|
+
* LICENSE file in the root directory of this source tree.
|
|
55
|
+
*
|
|
56
|
+
*
|
|
57
|
+
*/
|
|
58
|
+
function createMarkdownExport(transformers) {
|
|
59
|
+
const byType = transformersByType(transformers); // Export only uses text formats that are responsible for single format
|
|
60
|
+
// e.g. it will filter out *** (bold, italic) and instead use separate ** and *
|
|
284
61
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
for (let captureGroupIndex = 0; captureGroupIndex < captureGroupsCount; captureGroupIndex++) {
|
|
296
|
-
const textContent = regExMatches[captureGroupIndex];
|
|
297
|
-
patternMatchResults.regExCaptureGroups.push({
|
|
298
|
-
offsetInParent: runningLength,
|
|
299
|
-
text: textContent
|
|
300
|
-
}); // The 0th capture group is special in that it's text contents is
|
|
301
|
-
// a join of all subsequent capture groups. So, skip this group
|
|
302
|
-
// when calculating the runningLength.
|
|
303
|
-
|
|
304
|
-
if (captureGroupIndex > 0) {
|
|
305
|
-
runningLength += textContent.length;
|
|
62
|
+
const textFormatTransformers = byType.textFormat.filter(transformer => transformer.format.length === 1);
|
|
63
|
+
return () => {
|
|
64
|
+
const output = [];
|
|
65
|
+
const children = lexical.$getRoot().getChildren();
|
|
66
|
+
|
|
67
|
+
for (const child of children) {
|
|
68
|
+
const result = exportTopLevelElements(child, byType.element, textFormatTransformers, byType.textMatch);
|
|
69
|
+
|
|
70
|
+
if (result != null) {
|
|
71
|
+
output.push(result);
|
|
306
72
|
}
|
|
307
73
|
}
|
|
308
74
|
|
|
309
|
-
return
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return null;
|
|
75
|
+
return output.join('\n');
|
|
76
|
+
};
|
|
313
77
|
}
|
|
314
78
|
|
|
315
|
-
function
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
function getTextNodeWithOffsetOrThrow(scanningContext) {
|
|
319
|
-
const textNodeWithOffset = scanningContext.textNodeWithOffset;
|
|
79
|
+
function exportTopLevelElements(node, elementTransformers, textTransformersIndex, textMatchTransformers) {
|
|
80
|
+
for (const transformer of elementTransformers) {
|
|
81
|
+
const result = transformer.export(node, _node => exportChildren(_node, textTransformersIndex, textMatchTransformers));
|
|
320
82
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
throw Error(`Expect to have a text node with offset.`);
|
|
83
|
+
if (result != null) {
|
|
84
|
+
return result;
|
|
324
85
|
}
|
|
325
86
|
}
|
|
326
87
|
|
|
327
|
-
return
|
|
88
|
+
return lexical.$isElementNode(node) ? exportChildren(node, textTransformersIndex, textMatchTransformers) : null;
|
|
328
89
|
}
|
|
329
90
|
|
|
330
|
-
function
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
if (textNodeWithOffset.node.getPreviousSibling() === null) {
|
|
334
|
-
const textToSearch = textNodeWithOffset.node.getTextContent();
|
|
335
|
-
return getPatternMatchResultsWithRegEx(textToSearch, true, false, scanningContext.isAutoFormatting ? markdownCriteria.regExForAutoFormatting : markdownCriteria.regEx);
|
|
336
|
-
}
|
|
91
|
+
function exportChildren(node, textTransformersIndex, textMatchTransformers) {
|
|
92
|
+
const output = [];
|
|
93
|
+
const children = node.getChildren();
|
|
337
94
|
|
|
338
|
-
|
|
339
|
-
|
|
95
|
+
mainLoop: for (const child of children) {
|
|
96
|
+
if (lexical.$isLineBreakNode(child)) {
|
|
97
|
+
output.push('\n');
|
|
98
|
+
} else if (lexical.$isTextNode(child)) {
|
|
99
|
+
output.push(exportTextFormat(child, child.getTextContent(), textTransformersIndex));
|
|
100
|
+
} else {
|
|
101
|
+
for (const transformer of textMatchTransformers) {
|
|
102
|
+
const result = transformer.export(child, parentNode => exportChildren(parentNode, textTransformersIndex, textMatchTransformers), (textNode, textContent) => exportTextFormat(textNode, textContent, textTransformersIndex));
|
|
340
103
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
// Lazy calculate the text to search.
|
|
346
|
-
scanningContext.joinedText = text.$joinTextNodesInElementNode(parentElementNode, SEPARATOR_BETWEEN_TEXT_AND_NON_TEXT_NODES, getTextNodeWithOffsetOrThrow(scanningContext));
|
|
104
|
+
if (result != null) {
|
|
105
|
+
output.push(result);
|
|
106
|
+
continue mainLoop;
|
|
107
|
+
}
|
|
347
108
|
}
|
|
348
|
-
|
|
349
|
-
{
|
|
350
|
-
|
|
109
|
+
|
|
110
|
+
if (lexical.$isElementNode(child)) {
|
|
111
|
+
output.push(exportChildren(child, textTransformersIndex, textMatchTransformers));
|
|
351
112
|
}
|
|
352
113
|
}
|
|
353
114
|
}
|
|
354
115
|
|
|
355
|
-
|
|
356
|
-
return getPatternMatchResultsWithRegEx(scanningContext.joinedText, false, matchMustAppearAtEndOfString, scanningContext.isAutoFormatting ? markdownCriteria.regExForAutoFormatting : markdownCriteria.regEx);
|
|
116
|
+
return output.join('');
|
|
357
117
|
}
|
|
358
118
|
|
|
359
|
-
function
|
|
360
|
-
let
|
|
361
|
-
const
|
|
362
|
-
const children = element.getChildren();
|
|
363
|
-
const markdownCriteria = scanningContext.markdownCriteria;
|
|
364
|
-
const patternMatchResults = scanningContext.patternMatchResults;
|
|
365
|
-
|
|
366
|
-
if (markdownCriteria.markdownFormatKind != null) {
|
|
367
|
-
switch (markdownCriteria.markdownFormatKind) {
|
|
368
|
-
case 'paragraphH1':
|
|
369
|
-
{
|
|
370
|
-
newNode = richText.$createHeadingNode('h1');
|
|
371
|
-
newNode.append(...children);
|
|
372
|
-
return {
|
|
373
|
-
newNode,
|
|
374
|
-
shouldDelete
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
case 'paragraphH2':
|
|
379
|
-
{
|
|
380
|
-
newNode = richText.$createHeadingNode('h2');
|
|
381
|
-
newNode.append(...children);
|
|
382
|
-
return {
|
|
383
|
-
newNode,
|
|
384
|
-
shouldDelete
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
case 'paragraphH3':
|
|
389
|
-
{
|
|
390
|
-
newNode = richText.$createHeadingNode('h3');
|
|
391
|
-
newNode.append(...children);
|
|
392
|
-
return {
|
|
393
|
-
newNode,
|
|
394
|
-
shouldDelete
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
case 'paragraphH4':
|
|
399
|
-
{
|
|
400
|
-
newNode = richText.$createHeadingNode('h4');
|
|
401
|
-
newNode.append(...children);
|
|
402
|
-
return {
|
|
403
|
-
newNode,
|
|
404
|
-
shouldDelete
|
|
405
|
-
};
|
|
406
|
-
}
|
|
119
|
+
function exportTextFormat(node, textContent, textTransformers) {
|
|
120
|
+
let output = textContent;
|
|
121
|
+
const applied = new Set();
|
|
407
122
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
newNode.append(...children);
|
|
412
|
-
return {
|
|
413
|
-
newNode,
|
|
414
|
-
shouldDelete
|
|
415
|
-
};
|
|
416
|
-
}
|
|
123
|
+
for (const transformer of textTransformers) {
|
|
124
|
+
const format = transformer.format[0];
|
|
125
|
+
const tag = transformer.tag;
|
|
417
126
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
newNode.append(...children);
|
|
422
|
-
return {
|
|
423
|
-
newNode,
|
|
424
|
-
shouldDelete
|
|
425
|
-
};
|
|
426
|
-
}
|
|
127
|
+
if (hasFormat(node, format) && !applied.has(format)) {
|
|
128
|
+
// Multiple tags might be used for the same format (*, _)
|
|
129
|
+
applied.add(format); // Prevent adding opening tag is already opened by the previous sibling
|
|
427
130
|
|
|
428
|
-
|
|
429
|
-
{
|
|
430
|
-
createListOrMergeWithPrevious(element, children, patternMatchResults, 'ul');
|
|
431
|
-
return {
|
|
432
|
-
newNode: null,
|
|
433
|
-
shouldDelete: false
|
|
434
|
-
};
|
|
435
|
-
}
|
|
131
|
+
const previousNode = getTextSibling(node, true);
|
|
436
132
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
// For short-cuts aka autoFormatting, use start number.
|
|
441
|
-
// Later, this should be surface dependent and externalized.
|
|
442
|
-
|
|
443
|
-
const start = scanningContext.isAutoFormatting ? parseInt(startAsString, 10) : undefined;
|
|
444
|
-
createListOrMergeWithPrevious(element, children, patternMatchResults, 'ol', start);
|
|
445
|
-
return {
|
|
446
|
-
newNode: null,
|
|
447
|
-
shouldDelete: false
|
|
448
|
-
};
|
|
449
|
-
}
|
|
133
|
+
if (!hasFormat(previousNode, format)) {
|
|
134
|
+
output = tag + output;
|
|
135
|
+
} // Prevent adding closing tag if next sibling will do it
|
|
450
136
|
|
|
451
|
-
case 'paragraphCodeBlock':
|
|
452
|
-
{
|
|
453
|
-
// Toggle code and paragraph nodes.
|
|
454
|
-
if (scanningContext.isAutoFormatting === false) {
|
|
455
|
-
const shouldToggle = hasPatternMatchResults(scanningContext);
|
|
456
|
-
|
|
457
|
-
if (shouldToggle) {
|
|
458
|
-
scanningContext.isWithinCodeBlock = scanningContext.isWithinCodeBlock !== true; // When toggling, always clear the code block element node.
|
|
459
|
-
|
|
460
|
-
scanningContext.currentElementNode = null;
|
|
461
|
-
return {
|
|
462
|
-
newNode: null,
|
|
463
|
-
shouldDelete: true
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
if (scanningContext.isWithinCodeBlock) {
|
|
468
|
-
// Create the code block and return it to the caller.
|
|
469
|
-
if (scanningContext.currentElementNode == null) {
|
|
470
|
-
const newCodeBlockNode = code.$createCodeNode();
|
|
471
|
-
newCodeBlockNode.append(...children);
|
|
472
|
-
scanningContext.currentElementNode = newCodeBlockNode;
|
|
473
|
-
return {
|
|
474
|
-
newNode: newCodeBlockNode,
|
|
475
|
-
shouldDelete: false
|
|
476
|
-
};
|
|
477
|
-
} // Build up the code block with a line break and the children.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if (scanningContext.currentElementNode != null) {
|
|
481
|
-
const codeBlockNode = scanningContext.currentElementNode;
|
|
482
|
-
const lineBreakNode = lexical.$createLineBreakNode();
|
|
483
|
-
codeBlockNode.append(lineBreakNode);
|
|
484
|
-
|
|
485
|
-
if (children.length) {
|
|
486
|
-
codeBlockNode.append(lineBreakNode);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
codeBlockNode.append(...children);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
return {
|
|
494
|
-
newNode: null,
|
|
495
|
-
shouldDelete: true
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
if (scanningContext.triggerState != null && scanningContext.triggerState.isCodeBlock) {
|
|
500
|
-
newNode = lexical.$createParagraphNode();
|
|
501
|
-
} else {
|
|
502
|
-
newNode = code.$createCodeNode();
|
|
503
|
-
const codingLanguage = patternMatchResults.regExCaptureGroups.length >= 3 ? patternMatchResults.regExCaptureGroups[2].text : null;
|
|
504
|
-
|
|
505
|
-
if (codingLanguage != null && codingLanguage.length > 0) {
|
|
506
|
-
newNode.setLanguage(codingLanguage);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
newNode.append(...children);
|
|
511
|
-
return {
|
|
512
|
-
newNode,
|
|
513
|
-
shouldDelete
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
137
|
|
|
517
|
-
|
|
518
|
-
{
|
|
519
|
-
if (createHorizontalRuleNode != null) {
|
|
520
|
-
// return null for newNode. Insert the HR here.
|
|
521
|
-
const horizontalRuleNode = createHorizontalRuleNode();
|
|
522
|
-
element.insertBefore(horizontalRuleNode);
|
|
523
|
-
}
|
|
138
|
+
const nextNode = getTextSibling(node, false);
|
|
524
139
|
|
|
525
|
-
|
|
526
|
-
|
|
140
|
+
if (!hasFormat(nextNode, format)) {
|
|
141
|
+
output += tag;
|
|
142
|
+
}
|
|
527
143
|
}
|
|
528
144
|
}
|
|
529
145
|
|
|
530
|
-
return
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function createListOrMergeWithPrevious(element, children, patternMatchResults, tag, start) {
|
|
537
|
-
const listItem = list.$createListItemNode();
|
|
538
|
-
const indentMatch = patternMatchResults.regExCaptureGroups[0].text.match(/^\s*/);
|
|
539
|
-
const indent = indentMatch ? Math.floor(indentMatch[0].length / 4) : 0;
|
|
540
|
-
listItem.append(...children); // Checking if previous element is a list, and if so append
|
|
541
|
-
// new list item inside instead of creating new list
|
|
146
|
+
return output;
|
|
147
|
+
} // Get next or previous text sibling a text node, including cases
|
|
148
|
+
// when it's a child of inline element (e.g. link)
|
|
542
149
|
|
|
543
|
-
const prevElement = element.getPreviousSibling();
|
|
544
150
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
element.remove();
|
|
548
|
-
} else {
|
|
549
|
-
const list$1 = list.$createListNode(tag, start);
|
|
550
|
-
list$1.append(listItem);
|
|
551
|
-
element.replace(list$1);
|
|
552
|
-
}
|
|
151
|
+
function getTextSibling(node, backward) {
|
|
152
|
+
let sibling = backward ? node.getPreviousSibling() : node.getNextSibling();
|
|
553
153
|
|
|
554
|
-
if (
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
}
|
|
154
|
+
if (!sibling) {
|
|
155
|
+
const parent = node.getParentOrThrow();
|
|
558
156
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
} else {
|
|
563
|
-
transformTextNodeForText(scanningContext, elementNode);
|
|
157
|
+
if (parent.isInline()) {
|
|
158
|
+
sibling = backward ? parent.getPreviousSibling() : parent.getNextSibling();
|
|
159
|
+
}
|
|
564
160
|
}
|
|
565
|
-
}
|
|
566
161
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
const text = scanningContext.patternMatchResults.regExCaptureGroups[0].text; // Remove the text which we matched.
|
|
162
|
+
while (sibling) {
|
|
163
|
+
if (lexical.$isElementNode(sibling)) {
|
|
164
|
+
if (!sibling.isInline()) {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
573
167
|
|
|
574
|
-
const
|
|
168
|
+
const descendant = backward ? sibling.getLastDescendant() : sibling.getFirstDescendant();
|
|
575
169
|
|
|
576
|
-
if (
|
|
577
|
-
|
|
578
|
-
|
|
170
|
+
if (lexical.$isTextNode(descendant)) {
|
|
171
|
+
return descendant;
|
|
172
|
+
} else {
|
|
173
|
+
sibling = backward ? sibling.getPreviousSibling() : sibling.getNextSibling();
|
|
579
174
|
}
|
|
580
175
|
}
|
|
581
|
-
} // Transform the current element kind to the new element kind.
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const {
|
|
585
|
-
newNode,
|
|
586
|
-
shouldDelete
|
|
587
|
-
} = getNewNodeForCriteria(scanningContext, elementNode, createHorizontalRuleNode);
|
|
588
|
-
|
|
589
|
-
if (shouldDelete) {
|
|
590
|
-
elementNode.remove();
|
|
591
|
-
} else if (newNode !== null) {
|
|
592
|
-
elementNode.replace(newNode);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function transformTextNodeForText(scanningContext, parentElementNode) {
|
|
597
|
-
const markdownCriteria = scanningContext.markdownCriteria;
|
|
598
176
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
if (formatting != null) {
|
|
603
|
-
transformTextNodeWithFormatting(formatting, scanningContext, parentElementNode);
|
|
604
|
-
return;
|
|
177
|
+
if (lexical.$isTextNode(sibling)) {
|
|
178
|
+
return sibling;
|
|
605
179
|
}
|
|
606
180
|
|
|
607
|
-
if (
|
|
608
|
-
|
|
181
|
+
if (!lexical.$isElementNode(sibling)) {
|
|
182
|
+
return null;
|
|
609
183
|
}
|
|
610
184
|
}
|
|
611
|
-
}
|
|
612
185
|
|
|
613
|
-
|
|
614
|
-
const patternMatchResults = scanningContext.patternMatchResults;
|
|
615
|
-
const groupCount = patternMatchResults.regExCaptureGroups.length;
|
|
616
|
-
|
|
617
|
-
if (groupCount !== 7) {
|
|
618
|
-
// For BIUS and similar formats which have a pattern + text + pattern:
|
|
619
|
-
// given '*italic* ' below are the capture groups by index:
|
|
620
|
-
// 0. '*italic* '
|
|
621
|
-
// 1. '*'
|
|
622
|
-
// 2. whitespace // typically this is "".
|
|
623
|
-
// 3. 'italic'
|
|
624
|
-
// 4. whitespace // typicallly this is "".
|
|
625
|
-
// 5. '*'
|
|
626
|
-
// 6. ' '
|
|
627
|
-
return;
|
|
628
|
-
} // Remove unwanted text in reg ex pattern.
|
|
629
|
-
// Remove group 5.
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
removeTextByCaptureGroups(5, 5, scanningContext, parentElementNode); // Remove group 1.
|
|
633
|
-
|
|
634
|
-
removeTextByCaptureGroups(1, 1, scanningContext, parentElementNode); // Apply the formatting.
|
|
635
|
-
|
|
636
|
-
formatTextInCaptureGroupIndex(formatting, 3, scanningContext, parentElementNode); // Place caret at end of final capture group.
|
|
637
|
-
|
|
638
|
-
selectAfterFinalCaptureGroup(scanningContext, parentElementNode);
|
|
186
|
+
return null;
|
|
639
187
|
}
|
|
640
188
|
|
|
641
|
-
function
|
|
642
|
-
|
|
643
|
-
const regExCaptureGroups = patternMatchResults.regExCaptureGroups;
|
|
644
|
-
const groupCount = regExCaptureGroups.length;
|
|
645
|
-
|
|
646
|
-
if (groupCount !== 7) {
|
|
647
|
-
// For links and similar formats which have: pattern + text + pattern + pattern2 text2 + pattern2:
|
|
648
|
-
// Given '[title](url) ', below are the capture groups by index:
|
|
649
|
-
// 0. '[title](url) '
|
|
650
|
-
// 1. '['
|
|
651
|
-
// 2. 'title'
|
|
652
|
-
// 3. ']('
|
|
653
|
-
// 4. 'url'
|
|
654
|
-
// 5. ')'
|
|
655
|
-
// 6. ' '
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
const title = regExCaptureGroups[2].text;
|
|
660
|
-
const url = regExCaptureGroups[4].text;
|
|
661
|
-
|
|
662
|
-
if (title.length === 0 || url.length === 0) {
|
|
663
|
-
return;
|
|
664
|
-
} // Remove the initial pattern through to the final pattern.
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
removeTextByCaptureGroups(1, 5, scanningContext, parentElementNode);
|
|
668
|
-
insertTextPriorToCaptureGroup(1, // Insert at the beginning of the meaningful capture groups, namely index 1. Index 0 refers to the whole matched string.
|
|
669
|
-
title, scanningContext, parentElementNode);
|
|
670
|
-
const newSelectionForLink = createSelectionWithCaptureGroups(1, 1, false, true, scanningContext, parentElementNode);
|
|
671
|
-
|
|
672
|
-
if (newSelectionForLink == null) {
|
|
673
|
-
return;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
lexical.$setSelection(newSelectionForLink);
|
|
677
|
-
scanningContext.editor.dispatchCommand(link.TOGGLE_LINK_COMMAND, url); // Place caret at end of final capture group.
|
|
678
|
-
|
|
679
|
-
selectAfterFinalCaptureGroup(scanningContext, parentElementNode);
|
|
680
|
-
} // Below are lower level helper functions.
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
function getParentElementNodeOrThrow(scanningContext) {
|
|
684
|
-
return getTextNodeWithOffsetOrThrow(scanningContext).node.getParentOrThrow();
|
|
189
|
+
function hasFormat(node, format) {
|
|
190
|
+
return lexical.$isTextNode(node) && node.hasFormat(format);
|
|
685
191
|
}
|
|
686
192
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
195
|
+
*
|
|
196
|
+
* This source code is licensed under the MIT license found in the
|
|
197
|
+
* LICENSE file in the root directory of this source tree.
|
|
198
|
+
*
|
|
199
|
+
*
|
|
200
|
+
*/
|
|
201
|
+
const CODE_BLOCK_REG_EXP = /^```(\w{1,10})?\s?$/;
|
|
202
|
+
function createMarkdownImport(transformers) {
|
|
203
|
+
const byType = transformersByType(transformers);
|
|
204
|
+
const textFormatTransformersIndex = createTextFormatTransformersIndex(byType.textFormat);
|
|
205
|
+
return markdownString => {
|
|
206
|
+
const lines = markdownString.split('\n');
|
|
207
|
+
const linesLength = lines.length;
|
|
208
|
+
const root = lexical.$getRoot();
|
|
209
|
+
root.clear();
|
|
698
210
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
case 'strikethrough':
|
|
705
|
-
case 'code':
|
|
706
|
-
return [markdownFormatKind];
|
|
707
|
-
|
|
708
|
-
case 'strikethrough_italic_bold':
|
|
709
|
-
{
|
|
710
|
-
return ['strikethrough', 'italic', 'bold'];
|
|
711
|
-
}
|
|
211
|
+
for (let i = 0; i < linesLength; i++) {
|
|
212
|
+
const lineText = lines[i]; // Codeblocks are processed first as anything inside such block
|
|
213
|
+
// is ignored for further processing
|
|
214
|
+
// TODO:
|
|
215
|
+
// Abstract it to be dynamic as other transformers (add multiline match option)
|
|
712
216
|
|
|
713
|
-
|
|
714
|
-
{
|
|
715
|
-
return ['italic', 'bold'];
|
|
716
|
-
}
|
|
217
|
+
const [codeBlockNode, shiftedIndex] = importCodeBlock(lines, i, root);
|
|
717
218
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
219
|
+
if (codeBlockNode != null) {
|
|
220
|
+
i = shiftedIndex;
|
|
221
|
+
continue;
|
|
721
222
|
}
|
|
722
223
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
return ['strikethrough', 'bold'];
|
|
726
|
-
}
|
|
727
|
-
}
|
|
224
|
+
importBlocks(lineText, root, byType.element, textFormatTransformersIndex, byType.textMatch);
|
|
225
|
+
}
|
|
728
226
|
|
|
729
|
-
|
|
227
|
+
root.selectEnd();
|
|
228
|
+
};
|
|
730
229
|
}
|
|
731
230
|
|
|
732
|
-
function
|
|
733
|
-
const
|
|
734
|
-
const
|
|
735
|
-
|
|
231
|
+
function importBlocks(lineText, rootNode, elementTransformers, textFormatTransformersIndex, textMatchTransformers) {
|
|
232
|
+
const textNode = lexical.$createTextNode(lineText);
|
|
233
|
+
const elementNode = lexical.$createParagraphNode();
|
|
234
|
+
elementNode.append(textNode);
|
|
235
|
+
rootNode.append(elementNode);
|
|
736
236
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
const anchorCaptureGroupDetail = regExCaptureGroups[anchorCaptureGroupIndex];
|
|
743
|
-
const focusCaptureGroupDetail = regExCaptureGroups[focusCaptureGroupIndex];
|
|
744
|
-
const anchorLocation = startAtEndOfAnchor ? anchorCaptureGroupDetail.offsetInParent + anchorCaptureGroupDetail.text.length : anchorCaptureGroupDetail.offsetInParent;
|
|
745
|
-
const focusLocation = finishAtEndOfFocus ? focusCaptureGroupDetail.offsetInParent + focusCaptureGroupDetail.text.length : focusCaptureGroupDetail.offsetInParent;
|
|
746
|
-
const anchorTextNodeWithOffset = text.$findNodeWithOffsetFromJoinedText(anchorLocation, joinedTextLength, SEPARATOR_LENGTH, parentElementNode);
|
|
747
|
-
const focusTextNodeWithOffset = text.$findNodeWithOffsetFromJoinedText(focusLocation, joinedTextLength, SEPARATOR_LENGTH, parentElementNode);
|
|
748
|
-
|
|
749
|
-
if (anchorTextNodeWithOffset == null && focusTextNodeWithOffset == null && parentElementNode.getChildren().length === 0) {
|
|
750
|
-
const emptyElementSelection = lexical.$createRangeSelection();
|
|
751
|
-
emptyElementSelection.anchor.set(parentElementNode.getKey(), 0, 'element');
|
|
752
|
-
emptyElementSelection.focus.set(parentElementNode.getKey(), 0, 'element');
|
|
753
|
-
return emptyElementSelection;
|
|
754
|
-
}
|
|
237
|
+
for (const {
|
|
238
|
+
regExp,
|
|
239
|
+
replace
|
|
240
|
+
} of elementTransformers) {
|
|
241
|
+
const match = lineText.match(regExp);
|
|
755
242
|
|
|
756
|
-
|
|
757
|
-
|
|
243
|
+
if (match) {
|
|
244
|
+
textNode.setTextContent(lineText.slice(match[0].length));
|
|
245
|
+
replace(elementNode, [textNode], match, true);
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
758
248
|
}
|
|
759
249
|
|
|
760
|
-
|
|
761
|
-
selection.anchor.set(anchorTextNodeWithOffset.node.getKey(), anchorTextNodeWithOffset.offset, 'text');
|
|
762
|
-
selection.focus.set(focusTextNodeWithOffset.node.getKey(), focusTextNodeWithOffset.offset, 'text');
|
|
763
|
-
return selection;
|
|
250
|
+
importTextFormatTransformers(textNode, textFormatTransformersIndex, textMatchTransformers);
|
|
764
251
|
}
|
|
765
252
|
|
|
766
|
-
function
|
|
767
|
-
const
|
|
768
|
-
const regExCaptureGroups = patternMatchResults.regExCaptureGroups;
|
|
769
|
-
const newSelection = createSelectionWithCaptureGroups(anchorCaptureGroupIndex, focusCaptureGroupIndex, false, true, scanningContext, parentElementNode);
|
|
770
|
-
|
|
771
|
-
if (newSelection != null) {
|
|
772
|
-
lexical.$setSelection(newSelection);
|
|
773
|
-
const currentSelection = lexical.$getSelection();
|
|
253
|
+
function importCodeBlock(lines, startLineIndex, rootNode) {
|
|
254
|
+
const openMatch = lines[startLineIndex].match(CODE_BLOCK_REG_EXP);
|
|
774
255
|
|
|
775
|
-
|
|
776
|
-
|
|
256
|
+
if (openMatch) {
|
|
257
|
+
let endLineIndex = startLineIndex;
|
|
258
|
+
const linesLength = lines.length;
|
|
777
259
|
|
|
778
|
-
|
|
779
|
-
const
|
|
260
|
+
while (++endLineIndex < linesLength) {
|
|
261
|
+
const closeMatch = lines[endLineIndex].match(CODE_BLOCK_REG_EXP);
|
|
780
262
|
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
if (i <= focusCaptureGroupIndex) {
|
|
789
|
-
runningLength += captureGroupDetail.text.length;
|
|
790
|
-
captureGroupDetail.text = '';
|
|
791
|
-
}
|
|
263
|
+
if (closeMatch) {
|
|
264
|
+
const codeBlockNode = code.$createCodeNode(openMatch[1]);
|
|
265
|
+
const textNode = lexical.$createTextNode(lines.slice(startLineIndex + 1, endLineIndex).join('\n'));
|
|
266
|
+
codeBlockNode.append(textNode);
|
|
267
|
+
rootNode.append(codeBlockNode);
|
|
268
|
+
return [codeBlockNode, endLineIndex];
|
|
792
269
|
}
|
|
793
270
|
}
|
|
794
271
|
}
|
|
795
|
-
}
|
|
796
272
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
273
|
+
return [null, startLineIndex];
|
|
274
|
+
} // Processing text content and replaces text format tags.
|
|
275
|
+
// It takes outermost tag match and its content, creates text node with
|
|
276
|
+
// format based on tag and then recursively executed over node's content
|
|
277
|
+
//
|
|
278
|
+
// E.g. for "*Hello **world**!*" string it will create text node with
|
|
279
|
+
// "Hello **world**!" content and italic format and run recursively over
|
|
280
|
+
// its content to transform "**world**" part
|
|
281
|
+
|
|
801
282
|
|
|
802
|
-
|
|
283
|
+
function importTextFormatTransformers(textNode, textFormatTransformersIndex, textMatchTransformers) {
|
|
284
|
+
const textContent = textNode.getTextContent();
|
|
285
|
+
const match = findOutermostMatch(textContent, textFormatTransformersIndex);
|
|
286
|
+
|
|
287
|
+
if (!match) {
|
|
288
|
+
// Once text format processing is done run text match transformers, as it
|
|
289
|
+
// only can span within single text node (unline formats that can cover multiple nodes)
|
|
290
|
+
importTextMatchTransformers(textNode, textMatchTransformers);
|
|
803
291
|
return;
|
|
804
292
|
}
|
|
805
293
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
text
|
|
810
|
-
};
|
|
811
|
-
const newSelection = createSelectionWithCaptureGroups(captureGroupIndex, captureGroupIndex, false, false, scanningContext, parentElementNode);
|
|
294
|
+
let currentNode, remainderNode; // If matching full content there's no need to run splitText and can reuse existing textNode
|
|
295
|
+
// to update its content and apply format. E.g. for **_Hello_** string after applying bold
|
|
296
|
+
// format (**) it will reuse the same text node to apply italic (_)
|
|
812
297
|
|
|
813
|
-
if (
|
|
814
|
-
|
|
815
|
-
|
|
298
|
+
if (match[0] === textContent) {
|
|
299
|
+
currentNode = textNode;
|
|
300
|
+
} else {
|
|
301
|
+
const startIndex = match.index;
|
|
302
|
+
const endIndex = startIndex + match[0].length;
|
|
816
303
|
|
|
817
|
-
if (
|
|
818
|
-
|
|
304
|
+
if (startIndex === 0) {
|
|
305
|
+
[currentNode, remainderNode] = textNode.splitText(endIndex);
|
|
306
|
+
} else {
|
|
307
|
+
[, currentNode, remainderNode] = textNode.splitText(startIndex, endIndex);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
819
310
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
const newGroupCount = regExCaptureGroups.length;
|
|
311
|
+
currentNode.setTextContent(match[2]);
|
|
312
|
+
const transformer = textFormatTransformersIndex.transformersByTag[match[1]];
|
|
823
313
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
314
|
+
if (transformer) {
|
|
315
|
+
for (const format of transformer.format) {
|
|
316
|
+
if (!currentNode.hasFormat(format)) {
|
|
317
|
+
currentNode.toggleFormat(format);
|
|
827
318
|
}
|
|
828
319
|
}
|
|
829
|
-
}
|
|
830
|
-
}
|
|
320
|
+
} // Recursively run over inner text if it's not inline code
|
|
831
321
|
|
|
832
|
-
function formatTextInCaptureGroupIndex(formatTypes, captureGroupIndex, scanningContext, parentElementNode) {
|
|
833
|
-
const patternMatchResults = scanningContext.patternMatchResults;
|
|
834
|
-
const regExCaptureGroups = patternMatchResults.regExCaptureGroups;
|
|
835
|
-
const regExCaptureGroupsCount = regExCaptureGroups.length;
|
|
836
322
|
|
|
837
|
-
if (!(
|
|
838
|
-
|
|
839
|
-
}
|
|
323
|
+
if (!currentNode.hasFormat('code')) {
|
|
324
|
+
importTextFormatTransformers(currentNode, textFormatTransformersIndex, textMatchTransformers);
|
|
325
|
+
} // Run over remaining text if any
|
|
840
326
|
|
|
841
|
-
const captureGroupDetail = regExCaptureGroups[captureGroupIndex];
|
|
842
327
|
|
|
843
|
-
if (
|
|
844
|
-
|
|
328
|
+
if (remainderNode) {
|
|
329
|
+
importTextFormatTransformers(remainderNode, textFormatTransformersIndex, textMatchTransformers);
|
|
845
330
|
}
|
|
331
|
+
}
|
|
846
332
|
|
|
847
|
-
|
|
333
|
+
function importTextMatchTransformers(textNode_, textMatchTransformers) {
|
|
334
|
+
let textNode = textNode_;
|
|
848
335
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
336
|
+
mainLoop: while (textNode) {
|
|
337
|
+
for (const transformer of textMatchTransformers) {
|
|
338
|
+
const match = textNode.getTextContent().match(transformer.importRegExp);
|
|
852
339
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
currentSelection.formatText(formatTypes[i]);
|
|
340
|
+
if (!match) {
|
|
341
|
+
continue;
|
|
856
342
|
}
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
} // Place caret at end of final capture group.
|
|
860
343
|
|
|
344
|
+
const startIndex = match.index;
|
|
345
|
+
const endIndex = startIndex + match[0].length;
|
|
346
|
+
let replaceNode;
|
|
861
347
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
// Ignore capture group 0, as regEx defaults the 0th one to the entire matched string.
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
348
|
+
if (startIndex === 0) {
|
|
349
|
+
[replaceNode, textNode] = textNode.splitText(endIndex);
|
|
350
|
+
} else {
|
|
351
|
+
[, replaceNode, textNode] = textNode.splitText(startIndex, endIndex);
|
|
352
|
+
}
|
|
870
353
|
|
|
871
|
-
|
|
872
|
-
|
|
354
|
+
transformer.replace(replaceNode, match);
|
|
355
|
+
continue mainLoop;
|
|
356
|
+
}
|
|
873
357
|
|
|
874
|
-
|
|
875
|
-
lexical.$setSelection(newSelection);
|
|
358
|
+
break;
|
|
876
359
|
}
|
|
877
|
-
}
|
|
360
|
+
} // Finds first "<tag>content<tag>" match that is not nested into another tag
|
|
878
361
|
|
|
879
|
-
function createHeadingExport(level) {
|
|
880
|
-
return (node, exportChildren) => {
|
|
881
|
-
return richText.$isHeadingNode(node) && node.getTag() === 'h' + level ? '#'.repeat(level) + ' ' + exportChildren(node) : null;
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
function listExport(node, exportChildren) {
|
|
886
|
-
return list.$isListNode(node) ? processNestedLists(node, exportChildren, 0) : null;
|
|
887
|
-
} // TODO: should be param
|
|
888
362
|
|
|
363
|
+
function findOutermostMatch(textContent, textTransformersIndex) {
|
|
364
|
+
const openTagsMatch = textContent.match(textTransformersIndex.openTagsRegExp);
|
|
889
365
|
|
|
890
|
-
|
|
366
|
+
if (openTagsMatch == null) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
891
369
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
370
|
+
for (const match of openTagsMatch) {
|
|
371
|
+
// Open tags reg exp might capture leading space so removing it
|
|
372
|
+
// before using match to find transformer
|
|
373
|
+
const fullMatchRegExp = textTransformersIndex.fullMatchRegExpByTag[match.replace(/^\s/, '')];
|
|
896
374
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
const firstChild = listItemNode.getFirstChild();
|
|
375
|
+
if (fullMatchRegExp == null) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
901
378
|
|
|
902
|
-
|
|
903
|
-
output.push(processNestedLists(firstChild, exportChildren, depth + 1));
|
|
904
|
-
continue;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
379
|
+
const fullMatch = textContent.match(fullMatchRegExp);
|
|
907
380
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
output.push(indent + prefix + exportChildren(listItemNode));
|
|
911
|
-
index++;
|
|
381
|
+
if (fullMatch != null) {
|
|
382
|
+
return fullMatch;
|
|
912
383
|
}
|
|
913
384
|
}
|
|
914
385
|
|
|
915
|
-
return
|
|
386
|
+
return null;
|
|
916
387
|
}
|
|
917
388
|
|
|
918
|
-
function
|
|
919
|
-
|
|
920
|
-
}
|
|
389
|
+
function createTextFormatTransformersIndex(textTransformers) {
|
|
390
|
+
const transformersByTag = {};
|
|
391
|
+
const fullMatchRegExpByTag = {};
|
|
392
|
+
const openTagsRegExp = [];
|
|
921
393
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
394
|
+
for (const transformer of textTransformers) {
|
|
395
|
+
const {
|
|
396
|
+
tag
|
|
397
|
+
} = transformer;
|
|
398
|
+
transformersByTag[tag] = transformer;
|
|
399
|
+
const tagRegExp = tag.replace(/(\*|\^)/g, '\\$1');
|
|
400
|
+
openTagsRegExp.push(tagRegExp);
|
|
401
|
+
fullMatchRegExpByTag[tag] = new RegExp(`(${tagRegExp})(?![${tagRegExp}\\s])(.*?[^${tagRegExp}\\s])${tagRegExp}(?!${tagRegExp})`);
|
|
925
402
|
}
|
|
926
403
|
|
|
927
|
-
|
|
928
|
-
|
|
404
|
+
return {
|
|
405
|
+
// Reg exp to find open tag + content + close tag
|
|
406
|
+
fullMatchRegExpByTag,
|
|
407
|
+
// Reg exp to find opening tags
|
|
408
|
+
openTagsRegExp: new RegExp('(' + openTagsRegExp.join('|') + ')', 'g'),
|
|
409
|
+
transformersByTag
|
|
410
|
+
};
|
|
929
411
|
}
|
|
930
412
|
|
|
931
413
|
/**
|
|
@@ -936,385 +418,287 @@ function codeBlockExport(node, exportChildren) {
|
|
|
936
418
|
*
|
|
937
419
|
*
|
|
938
420
|
*/
|
|
421
|
+
const PUNCTUATION_OR_SPACE = /[!-/:-@[-`{-~\s]/;
|
|
939
422
|
|
|
940
|
-
function
|
|
941
|
-
|
|
942
|
-
return null;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
const node = selection.anchor.getNode();
|
|
423
|
+
function runElementTransformers(parentNode, anchorNode, anchorOffset, elementTransformers) {
|
|
424
|
+
const grandParentNode = parentNode.getParent();
|
|
946
425
|
|
|
947
|
-
if (!lexical.$
|
|
948
|
-
return
|
|
426
|
+
if (!lexical.$isRootNode(grandParentNode) || parentNode.getFirstChild() !== anchorNode) {
|
|
427
|
+
return false;
|
|
949
428
|
}
|
|
950
429
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
function updateAutoFormatting(editor, scanningContext, createHorizontalRuleNode) {
|
|
958
|
-
editor.update(() => {
|
|
959
|
-
const elementNode = getTextNodeWithOffsetOrThrow(scanningContext).node.getParentOrThrow();
|
|
960
|
-
transformTextNodeForMarkdownCriteria(scanningContext, elementNode, createHorizontalRuleNode);
|
|
961
|
-
}, {
|
|
962
|
-
tag: 'history-push'
|
|
963
|
-
});
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
function getCriteriaWithPatternMatchResults(markdownCriteriaArray, scanningContext) {
|
|
967
|
-
const currentTriggerState = scanningContext.triggerState;
|
|
968
|
-
const count = markdownCriteriaArray.length;
|
|
430
|
+
const textContent = anchorNode.getTextContent(); // Checking for anchorOffset position to prevent any checks for cases when caret is too far
|
|
431
|
+
// from a line start to be a part of block-level markdown trigger.
|
|
432
|
+
//
|
|
433
|
+
// TODO:
|
|
434
|
+
// Can have a quick check if caret is close enough to the beginning of the string (e.g. offset less than 10-20)
|
|
435
|
+
// since otherwise it won't be a markdown shortcut, but tables are exception
|
|
969
436
|
|
|
970
|
-
|
|
971
|
-
|
|
437
|
+
if (textContent[anchorOffset - 1] !== ' ') {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
972
440
|
|
|
973
|
-
|
|
974
|
-
|
|
441
|
+
for (const {
|
|
442
|
+
regExp,
|
|
443
|
+
replace
|
|
444
|
+
} of elementTransformers) {
|
|
445
|
+
const match = textContent.match(regExp);
|
|
975
446
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
447
|
+
if (match && match[0].length === anchorOffset) {
|
|
448
|
+
const nextSiblings = anchorNode.getNextSiblings();
|
|
449
|
+
const [leadingNode, remainderNode] = anchorNode.splitText(anchorOffset);
|
|
450
|
+
leadingNode.remove();
|
|
451
|
+
const siblings = remainderNode ? [remainderNode, ...nextSiblings] : nextSiblings;
|
|
452
|
+
replace(parentNode, siblings, match, false);
|
|
453
|
+
return true;
|
|
982
454
|
}
|
|
983
455
|
}
|
|
984
456
|
|
|
985
|
-
return
|
|
986
|
-
markdownCriteria: null,
|
|
987
|
-
patternMatchResults: null
|
|
988
|
-
};
|
|
457
|
+
return false;
|
|
989
458
|
}
|
|
990
459
|
|
|
991
|
-
function
|
|
992
|
-
let
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
if (textNodeWithOffset === null) {
|
|
997
|
-
return;
|
|
998
|
-
} // Please see the declaration of ScanningContext for a detailed explanation.
|
|
460
|
+
function runTextMatchTransformers(anchorNode, anchorOffset, transformersByTrigger) {
|
|
461
|
+
let textContent = anchorNode.getTextContent();
|
|
462
|
+
const lastChar = textContent[anchorOffset - 1];
|
|
463
|
+
const transformers = transformersByTrigger[lastChar];
|
|
999
464
|
|
|
465
|
+
if (transformers == null) {
|
|
466
|
+
return false;
|
|
467
|
+
} // If typing in the middle of content, remove the tail to do
|
|
468
|
+
// reg exp match up to a string end (caret position)
|
|
1000
469
|
|
|
1001
|
-
const initialScanningContext = getInitialScanningContext(editor, true, textNodeWithOffset, currentTriggerState);
|
|
1002
|
-
const criteriaWithPatternMatchResults = getCriteriaWithPatternMatchResults( // Do not apply paragraph node changes like blockQuote or H1 to listNodes. Also, do not attempt to transform a list into a list using * or -.
|
|
1003
|
-
currentTriggerState.isParentAListItemNode === false ? getAllMarkdownCriteria() : getAllMarkdownCriteriaForTextNodes(), initialScanningContext);
|
|
1004
470
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
scanningContext = initialScanningContext; // Lazy fill-in the particular format criteria and any matching result information.
|
|
1010
|
-
|
|
1011
|
-
scanningContext.markdownCriteria = criteriaWithPatternMatchResults.markdownCriteria;
|
|
1012
|
-
scanningContext.patternMatchResults = criteriaWithPatternMatchResults.patternMatchResults;
|
|
1013
|
-
});
|
|
1014
|
-
return scanningContext;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
function getTriggerState(editorState) {
|
|
1018
|
-
let criteria = null;
|
|
1019
|
-
editorState.read(() => {
|
|
1020
|
-
const selection = lexical.$getSelection();
|
|
1021
|
-
|
|
1022
|
-
if (!lexical.$isRangeSelection(selection) || !selection.isCollapsed()) {
|
|
1023
|
-
return;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
const node = selection.anchor.getNode();
|
|
1027
|
-
const parentNode = node.getParent();
|
|
1028
|
-
const isParentAListItemNode = list.$isListItemNode(parentNode);
|
|
1029
|
-
const hasParentNode = parentNode !== null;
|
|
1030
|
-
criteria = {
|
|
1031
|
-
anchorOffset: selection.anchor.offset,
|
|
1032
|
-
hasParentNode,
|
|
1033
|
-
isCodeBlock: code.$isCodeNode(node),
|
|
1034
|
-
isParentAListItemNode,
|
|
1035
|
-
isSelectionCollapsed: true,
|
|
1036
|
-
isSimpleText: lexical.$isTextNode(node) && node.isSimpleText(),
|
|
1037
|
-
nodeKey: node.getKey(),
|
|
1038
|
-
textContent: node.getTextContent()
|
|
1039
|
-
};
|
|
1040
|
-
});
|
|
1041
|
-
return criteria;
|
|
1042
|
-
}
|
|
1043
|
-
function findScanningContext(editor, currentTriggerState, priorTriggerState) {
|
|
1044
|
-
if (currentTriggerState == null || priorTriggerState == null) {
|
|
1045
|
-
return null;
|
|
471
|
+
if (anchorOffset < textContent.length) {
|
|
472
|
+
textContent = textContent.slice(0, anchorOffset);
|
|
1046
473
|
}
|
|
1047
474
|
|
|
1048
|
-
const
|
|
1049
|
-
|
|
475
|
+
for (const transformer of transformers) {
|
|
476
|
+
const match = textContent.match(transformer.regExp);
|
|
1050
477
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
// trigger which is the trigger string (" ").
|
|
478
|
+
if (match === null) {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
1055
481
|
|
|
1056
|
-
const
|
|
1057
|
-
const
|
|
1058
|
-
|
|
1059
|
-
// We might consider looking for ** + space or __ + space and so on to boost performance
|
|
1060
|
-
// even further. Make sure the patter is driven from the trigger state type.
|
|
482
|
+
const startIndex = match.index;
|
|
483
|
+
const endIndex = startIndex + match[0].length;
|
|
484
|
+
let replaceNode;
|
|
1061
485
|
|
|
1062
|
-
if (
|
|
1063
|
-
|
|
1064
|
-
|
|
486
|
+
if (startIndex === 0) {
|
|
487
|
+
[replaceNode] = anchorNode.splitText(endIndex);
|
|
488
|
+
} else {
|
|
489
|
+
[, replaceNode] = anchorNode.splitText(startIndex, endIndex);
|
|
1065
490
|
}
|
|
491
|
+
|
|
492
|
+
replaceNode.selectNext();
|
|
493
|
+
transformer.replace(replaceNode, match);
|
|
494
|
+
return true;
|
|
1066
495
|
}
|
|
1067
496
|
|
|
1068
|
-
return
|
|
497
|
+
return false;
|
|
1069
498
|
}
|
|
1070
499
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
* LICENSE file in the root directory of this source tree.
|
|
1076
|
-
*
|
|
1077
|
-
*
|
|
1078
|
-
*/
|
|
1079
|
-
function convertStringToLexical(text, editor) {
|
|
1080
|
-
if (!text.length) {
|
|
1081
|
-
return null;
|
|
1082
|
-
}
|
|
500
|
+
function runTextFormatTransformers(editor, anchorNode, anchorOffset, textFormatTransformers) {
|
|
501
|
+
const textContent = anchorNode.getTextContent();
|
|
502
|
+
const closeTagEndIndex = anchorOffset - 1;
|
|
503
|
+
const closeChar = textContent[closeTagEndIndex]; // Quick check if we're possibly at the end of inline markdown style
|
|
1083
504
|
|
|
1084
|
-
const
|
|
1085
|
-
const splitLines = text.split('\n');
|
|
1086
|
-
const splitLinesCount = splitLines.length;
|
|
505
|
+
const matchers = textFormatTransformers[closeChar];
|
|
1087
506
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
nodes.push(lexical.$createParagraphNode().append(lexical.$createTextNode(splitLines[i])));
|
|
1091
|
-
} else {
|
|
1092
|
-
nodes.push(lexical.$createParagraphNode());
|
|
1093
|
-
}
|
|
507
|
+
if (!matchers) {
|
|
508
|
+
return false;
|
|
1094
509
|
}
|
|
1095
510
|
|
|
1096
|
-
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
511
|
+
for (const matcher of matchers) {
|
|
512
|
+
const {
|
|
513
|
+
tag
|
|
514
|
+
} = matcher;
|
|
515
|
+
const tagLength = tag.length;
|
|
516
|
+
const closeTagStartIndex = closeTagEndIndex - tagLength + 1; // If tag is not single char check if rest of it matches with text content
|
|
517
|
+
|
|
518
|
+
if (tagLength > 1) {
|
|
519
|
+
if (!isEqualSubString(textContent, closeTagStartIndex, tag, 0, tagLength)) {
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
} // Space before closing tag cancels inline markdown
|
|
1102
523
|
|
|
1103
|
-
return null;
|
|
1104
|
-
}
|
|
1105
|
-
function convertMarkdownForElementNodes(editor, createHorizontalRuleNode) {
|
|
1106
|
-
// Please see the declaration of ScanningContext for a detailed explanation.
|
|
1107
|
-
const scanningContext = getInitialScanningContext(editor, false, null, null);
|
|
1108
|
-
const root = lexical.$getRoot();
|
|
1109
|
-
let done = false;
|
|
1110
|
-
let startIndex = 0; // Handle the paragraph level markdown.
|
|
1111
524
|
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
const countOfElementNodes = elementNodes.length;
|
|
525
|
+
if (textContent[closeTagStartIndex - 1] === ' ') {
|
|
526
|
+
continue;
|
|
527
|
+
} // Some tags can not be used within words, hence should have newline/space/punctuation after it
|
|
1116
528
|
|
|
1117
|
-
for (let i = startIndex; i < countOfElementNodes; i++) {
|
|
1118
|
-
const elementNode = elementNodes[i];
|
|
1119
529
|
|
|
1120
|
-
|
|
1121
|
-
convertParagraphLevelMarkdown(scanningContext, elementNode, createHorizontalRuleNode);
|
|
1122
|
-
} // Reset the scanning information that relates to the particular element node.
|
|
530
|
+
const afterCloseTagChar = textContent[closeTagEndIndex + 1];
|
|
1123
531
|
|
|
532
|
+
if (matcher.intraword === false && afterCloseTagChar && !PUNCTUATION_OR_SPACE.test(afterCloseTagChar)) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
1124
535
|
|
|
1125
|
-
|
|
536
|
+
const closeNode = anchorNode;
|
|
537
|
+
let openNode = closeNode;
|
|
538
|
+
let openTagStartIndex = getOpenTagStartIndex(textContent, closeTagStartIndex, tag); // Go through text node siblings and search for opening tag
|
|
539
|
+
// if haven't found it within the same text node as closing tag
|
|
1126
540
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
541
|
+
let sibling = openNode;
|
|
542
|
+
|
|
543
|
+
while (openTagStartIndex < 0 && (sibling = sibling.getPreviousSibling())) {
|
|
544
|
+
if (lexical.$isLineBreakNode(sibling)) {
|
|
1131
545
|
break;
|
|
1132
546
|
}
|
|
1133
|
-
}
|
|
1134
|
-
} // while
|
|
1135
547
|
|
|
548
|
+
if (lexical.$isTextNode(sibling)) {
|
|
549
|
+
const siblingTextContent = sibling.getTextContent();
|
|
550
|
+
openNode = sibling;
|
|
551
|
+
openTagStartIndex = getOpenTagStartIndex(siblingTextContent, siblingTextContent.length, tag);
|
|
552
|
+
}
|
|
553
|
+
} // Opening tag is not found
|
|
1136
554
|
|
|
1137
|
-
done = false;
|
|
1138
|
-
startIndex = 0; // Handle the text level markdown.
|
|
1139
555
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
const countOfElementNodes = elementNodes.length;
|
|
556
|
+
if (openTagStartIndex < 0) {
|
|
557
|
+
continue;
|
|
558
|
+
} // No content between opening and closing tag
|
|
1144
559
|
|
|
1145
|
-
for (let i = startIndex; i < countOfElementNodes; i++) {
|
|
1146
|
-
const elementNode = elementNodes[i];
|
|
1147
560
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
561
|
+
if (openNode === closeNode && openTagStartIndex + tagLength === closeTagStartIndex) {
|
|
562
|
+
continue;
|
|
563
|
+
} // Checking longer tags for repeating chars (e.g. *** vs **)
|
|
1151
564
|
|
|
1152
565
|
|
|
1153
|
-
|
|
1154
|
-
}
|
|
1155
|
-
} // while
|
|
566
|
+
const prevOpenNodeText = openNode.getTextContent();
|
|
1156
567
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
const textContent = elementNode.getTextContent(); // Handle paragraph nodes below.
|
|
1161
|
-
|
|
1162
|
-
if (lexical.$isParagraphNode(elementNode)) {
|
|
1163
|
-
const paragraphNode = elementNode;
|
|
1164
|
-
const firstChild = paragraphNode.getFirstChild();
|
|
1165
|
-
const firstChildIsTextNode = lexical.$isTextNode(firstChild); // Handle conversion to code block.
|
|
1166
|
-
|
|
1167
|
-
if (scanningContext.isWithinCodeBlock === true) {
|
|
1168
|
-
if (firstChild != null && firstChildIsTextNode) {
|
|
1169
|
-
// Test if we encounter ending code block.
|
|
1170
|
-
scanningContext.textNodeWithOffset = {
|
|
1171
|
-
node: firstChild,
|
|
1172
|
-
offset: 0
|
|
1173
|
-
};
|
|
1174
|
-
const patternMatchResults = getPatternMatchResultsForCodeBlock(scanningContext, textContent);
|
|
1175
|
-
|
|
1176
|
-
if (patternMatchResults != null) {
|
|
1177
|
-
// Toggle transform to or from code block.
|
|
1178
|
-
scanningContext.patternMatchResults = patternMatchResults;
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
568
|
+
if (openTagStartIndex > 0 && prevOpenNodeText[openTagStartIndex - 1] === closeChar) {
|
|
569
|
+
continue;
|
|
570
|
+
} // Some tags can not be used within words, hence should have newline/space/punctuation before it
|
|
1181
571
|
|
|
1182
|
-
scanningContext.markdownCriteria = getCodeBlockCriteria(); // Perform text transformation here.
|
|
1183
572
|
|
|
1184
|
-
|
|
1185
|
-
return;
|
|
1186
|
-
}
|
|
573
|
+
const beforeOpenTagChar = prevOpenNodeText[openTagStartIndex - 1];
|
|
1187
574
|
|
|
1188
|
-
if (
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
575
|
+
if (matcher.intraword === false && beforeOpenTagChar && !PUNCTUATION_OR_SPACE.test(beforeOpenTagChar)) {
|
|
576
|
+
continue;
|
|
577
|
+
} // Clean text from opening and closing tags (starting from closing tag
|
|
578
|
+
// to prevent any offset shifts if we start from opening one)
|
|
1192
579
|
|
|
1193
|
-
if (!(firstChild != null && firstChildIsTextNode)) {
|
|
1194
|
-
throw Error(`Expect paragraph containing only text nodes.`);
|
|
1195
|
-
}
|
|
1196
580
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
581
|
+
const prevCloseNodeText = closeNode.getTextContent();
|
|
582
|
+
const closeNodeText = prevCloseNodeText.slice(0, closeTagStartIndex) + prevCloseNodeText.slice(closeTagEndIndex + 1);
|
|
583
|
+
closeNode.setTextContent(closeNodeText);
|
|
584
|
+
const openNodeText = openNode === closeNode ? closeNodeText : prevOpenNodeText;
|
|
585
|
+
openNode.setTextContent(openNodeText.slice(0, openTagStartIndex) + openNodeText.slice(openTagStartIndex + tagLength));
|
|
586
|
+
const nextSelection = lexical.$createRangeSelection();
|
|
587
|
+
lexical.$setSelection(nextSelection); // Adjust offset based on deleted chars
|
|
1201
588
|
|
|
1202
|
-
|
|
1203
|
-
|
|
589
|
+
const newOffset = closeTagEndIndex - tagLength * (openNode === closeNode ? 2 : 1) + 1;
|
|
590
|
+
nextSelection.anchor.set(openNode.__key, openTagStartIndex, 'text');
|
|
591
|
+
nextSelection.focus.set(closeNode.__key, newOffset, 'text'); // Apply formatting to selected text
|
|
1204
592
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
593
|
+
for (const format of matcher.format) {
|
|
594
|
+
if (!nextSelection.hasFormat(format)) {
|
|
595
|
+
nextSelection.formatText(format);
|
|
596
|
+
}
|
|
597
|
+
} // Collapse selection up to the focus point
|
|
1208
598
|
|
|
1209
|
-
const patternMatchResults = getPatternMatchResultsForCriteria(criteria, scanningContext, getParentElementNodeOrThrow(scanningContext));
|
|
1210
599
|
|
|
1211
|
-
|
|
1212
|
-
scanningContext.markdownCriteria = criteria;
|
|
1213
|
-
scanningContext.patternMatchResults = patternMatchResults; // Perform text transformation here.
|
|
600
|
+
nextSelection.anchor.set(nextSelection.focus.key, nextSelection.focus.offset, nextSelection.focus.type); // Remove formatting from collapsed selection
|
|
1214
601
|
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
602
|
+
for (const format of matcher.format) {
|
|
603
|
+
if (nextSelection.hasFormat(format)) {
|
|
604
|
+
nextSelection.toggleFormat(format);
|
|
1218
605
|
}
|
|
1219
606
|
}
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
function convertTextLevelMarkdown(scanningContext, elementNode, createHorizontalRuleNode) {
|
|
1224
|
-
const firstChild = elementNode.getFirstChild();
|
|
1225
607
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
convertMarkdownForTextCriteria(scanningContext, elementNode, createHorizontalRuleNode);
|
|
1229
|
-
return;
|
|
1230
|
-
} // Handle the case where the elementNode has child elementNodes like lists.
|
|
1231
|
-
// Since we started at a text import, we don't need to worry about anything but textNodes.
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
1232
610
|
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
1233
613
|
|
|
1234
|
-
|
|
1235
|
-
const
|
|
614
|
+
function getOpenTagStartIndex(string, maxIndex, tag) {
|
|
615
|
+
const tagLength = tag.length;
|
|
1236
616
|
|
|
1237
|
-
for (let i =
|
|
1238
|
-
const
|
|
617
|
+
for (let i = maxIndex; i >= tagLength; i--) {
|
|
618
|
+
const startIndex = i - tagLength;
|
|
1239
619
|
|
|
1240
|
-
if (
|
|
1241
|
-
|
|
1242
|
-
|
|
620
|
+
if (isEqualSubString(string, startIndex, tag, 0, tagLength) && // Space after opening tag cancels transformation
|
|
621
|
+
string[startIndex + tagLength] !== ' ') {
|
|
622
|
+
return startIndex;
|
|
1243
623
|
}
|
|
1244
624
|
}
|
|
1245
|
-
}
|
|
1246
625
|
|
|
1247
|
-
|
|
1248
|
-
|
|
626
|
+
return -1;
|
|
627
|
+
}
|
|
1249
628
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
629
|
+
function isEqualSubString(stringA, aStart, stringB, bStart, length) {
|
|
630
|
+
for (let i = 0; i < length; i++) {
|
|
631
|
+
if (stringA[aStart + i] !== stringB[bStart + i]) {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
1255
635
|
|
|
1256
|
-
|
|
1257
|
-
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
1258
638
|
|
|
1259
|
-
|
|
1260
|
-
|
|
639
|
+
function registerMarkdownShortcuts(editor, transformers = TRANSFORMERS) {
|
|
640
|
+
const byType = transformersByType(transformers);
|
|
641
|
+
const textFormatTransformersIndex = indexBy(byType.textFormat, ({
|
|
642
|
+
tag
|
|
643
|
+
}) => tag[tag.length - 1]);
|
|
644
|
+
const textMatchTransformersIndex = indexBy(byType.textMatch, ({
|
|
645
|
+
trigger
|
|
646
|
+
}) => trigger);
|
|
1261
647
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
648
|
+
const transform = (parentNode, anchorNode, anchorOffset) => {
|
|
649
|
+
if (runElementTransformers(parentNode, anchorNode, anchorOffset, byType.element)) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
1265
652
|
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
}
|
|
653
|
+
if (runTextMatchTransformers(anchorNode, anchorOffset, textMatchTransformersIndex)) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
1270
656
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
offset: lastTextNode.getTextContent().length
|
|
1274
|
-
};
|
|
1275
|
-
}
|
|
657
|
+
runTextFormatTransformers(editor, anchorNode, anchorOffset, textFormatTransformersIndex);
|
|
658
|
+
};
|
|
1276
659
|
|
|
1277
|
-
|
|
660
|
+
return editor.registerUpdateListener(({
|
|
661
|
+
tags,
|
|
662
|
+
dirtyLeaves,
|
|
663
|
+
editorState,
|
|
664
|
+
prevEditorState
|
|
665
|
+
}) => {
|
|
666
|
+
// Ignore updates from undo/redo (as changes already calculated)
|
|
667
|
+
if (tags.has('historic')) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
1278
670
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
scanningContext.patternMatchResults = patternMatchResults; // Perform text transformation here.
|
|
671
|
+
const selection = editorState.read(lexical.$getSelection);
|
|
672
|
+
const prevSelection = prevEditorState.read(lexical.$getSelection);
|
|
1282
673
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
674
|
+
if (!lexical.$isRangeSelection(prevSelection) || !lexical.$isRangeSelection(selection) || !selection.isCollapsed()) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
1286
677
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
return;
|
|
1290
|
-
}
|
|
678
|
+
const anchorKey = selection.anchor.key;
|
|
679
|
+
const anchorOffset = selection.anchor.offset;
|
|
1291
680
|
|
|
1292
|
-
|
|
1293
|
-
// Nothing was changed by this transformation, so move on to the next crieteria.
|
|
1294
|
-
continue;
|
|
1295
|
-
} // The text was changed. Perhaps there is another hit for the same criteria.
|
|
681
|
+
const anchorNode = editorState._nodeMap.get(anchorKey);
|
|
1296
682
|
|
|
683
|
+
if (!lexical.$isTextNode(anchorNode) || !dirtyLeaves.has(anchorKey) || anchorOffset !== 1 && anchorOffset !== prevSelection.anchor.offset + 1) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
1297
686
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
687
|
+
editor.update(() => {
|
|
688
|
+
// Markdown is not available inside code
|
|
689
|
+
if (anchorNode.hasFormat('code')) {
|
|
690
|
+
return;
|
|
1302
691
|
}
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
692
|
|
|
1307
|
-
|
|
1308
|
-
const children = elementNode.getChildren();
|
|
1309
|
-
const countOfChildren = children.length;
|
|
693
|
+
const parentNode = anchorNode.getParent();
|
|
1310
694
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
695
|
+
if (parentNode === null || code.$isCodeNode(parentNode)) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
1316
698
|
|
|
1317
|
-
|
|
699
|
+
transform(parentNode, anchorNode, selection.anchor.offset);
|
|
700
|
+
});
|
|
701
|
+
});
|
|
1318
702
|
}
|
|
1319
703
|
|
|
1320
704
|
/**
|
|
@@ -1325,138 +709,211 @@ function getLastTextNodeInElementNode(elementNode) {
|
|
|
1325
709
|
*
|
|
1326
710
|
*
|
|
1327
711
|
*/
|
|
1328
|
-
function $convertToMarkdownString() {
|
|
1329
|
-
const output = [];
|
|
1330
|
-
const children = lexical.$getRoot().getChildren();
|
|
1331
|
-
|
|
1332
|
-
for (const child of children) {
|
|
1333
|
-
const result = exportTopLevelElementOrDecorator(child);
|
|
1334
712
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
713
|
+
const replaceWithBlock = createNode => {
|
|
714
|
+
return (parentNode, children, match) => {
|
|
715
|
+
const node = createNode(match);
|
|
716
|
+
node.append(...children);
|
|
717
|
+
parentNode.replace(node);
|
|
718
|
+
node.select(0, 0);
|
|
719
|
+
};
|
|
720
|
+
}; // Amount of spaces that define indentation level
|
|
721
|
+
// TODO: should be an option
|
|
1339
722
|
|
|
1340
|
-
return output.join('\n');
|
|
1341
|
-
}
|
|
1342
723
|
|
|
1343
|
-
|
|
1344
|
-
const blockTransformers = getAllMarkdownCriteriaForParagraphs();
|
|
724
|
+
const LIST_INDENT_SIZE = 4;
|
|
1345
725
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
726
|
+
const listReplace = listType => {
|
|
727
|
+
return (parentNode, children, match) => {
|
|
728
|
+
const previousNode = parentNode.getPreviousSibling();
|
|
729
|
+
const listItem = list.$createListItemNode(listType === 'check' ? match[3] === 'x' : undefined);
|
|
1349
730
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
731
|
+
if (list.$isListNode(previousNode) && previousNode.getListType() === listType) {
|
|
732
|
+
previousNode.append(listItem);
|
|
733
|
+
parentNode.remove();
|
|
734
|
+
} else {
|
|
735
|
+
const list$1 = list.$createListNode(listType, listType === 'number' ? Number(match[2]) : undefined);
|
|
736
|
+
list$1.append(listItem);
|
|
737
|
+
parentNode.replace(list$1);
|
|
1353
738
|
}
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
return lexical.$isElementNode(node) ? exportChildren(node) : null;
|
|
1357
|
-
}
|
|
1358
739
|
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
740
|
+
listItem.append(...children);
|
|
741
|
+
listItem.select(0, 0);
|
|
742
|
+
const indent = Math.floor(match[1].length / LIST_INDENT_SIZE);
|
|
1362
743
|
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
output.push('\n');
|
|
1366
|
-
} else if (lexical.$isTextNode(child)) {
|
|
1367
|
-
output.push(exportTextNode(child, child.getTextContent()));
|
|
1368
|
-
} else if (link.$isLinkNode(child)) {
|
|
1369
|
-
const linkContent = `[${child.getTextContent()}](${child.getURL()})`;
|
|
1370
|
-
const firstChild = child.getFirstChild(); // Add text styles only if link has single text node inside. If it's more
|
|
1371
|
-
// then one we either ignore it and have single <a> to cover whole link,
|
|
1372
|
-
// or process them, but then have link cut into multiple <a>.
|
|
1373
|
-
// For now chosing the first option.
|
|
1374
|
-
|
|
1375
|
-
if (child.getChildrenSize() === 1 && lexical.$isTextNode(firstChild)) {
|
|
1376
|
-
output.push(exportTextNode(firstChild, linkContent));
|
|
1377
|
-
} else {
|
|
1378
|
-
output.push(linkContent);
|
|
1379
|
-
}
|
|
1380
|
-
} else if (lexical.$isElementNode(child)) {
|
|
1381
|
-
output.push(exportChildren(child));
|
|
744
|
+
if (indent) {
|
|
745
|
+
listItem.setIndent(indent);
|
|
1382
746
|
}
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
return output.join('');
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
function exportTextNode(node, textContent, parentNode) {
|
|
1389
|
-
let output = textContent;
|
|
1390
|
-
const applied = new Set();
|
|
1391
|
-
const textTransformers = getAllMarkdownCriteriaForTextNodes();
|
|
1392
|
-
|
|
1393
|
-
for (const transformer of textTransformers) {
|
|
1394
|
-
const {
|
|
1395
|
-
exportFormat: format,
|
|
1396
|
-
exportTag: tag,
|
|
1397
|
-
exportTagClose: tagClose = tag
|
|
1398
|
-
} = transformer;
|
|
747
|
+
};
|
|
748
|
+
};
|
|
1399
749
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
750
|
+
const listExport = (listNode, exportChildren, depth) => {
|
|
751
|
+
const output = [];
|
|
752
|
+
const children = listNode.getChildren();
|
|
753
|
+
let index = 0;
|
|
1404
754
|
|
|
1405
|
-
|
|
755
|
+
for (const listItemNode of children) {
|
|
756
|
+
if (list.$isListItemNode(listItemNode)) {
|
|
757
|
+
if (listItemNode.getChildrenSize() === 1) {
|
|
758
|
+
const firstChild = listItemNode.getFirstChild();
|
|
1406
759
|
|
|
1407
|
-
|
|
1408
|
-
|
|
760
|
+
if (list.$isListNode(firstChild)) {
|
|
761
|
+
output.push(listExport(firstChild, exportChildren, depth + 1));
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
1409
764
|
}
|
|
1410
765
|
|
|
1411
|
-
const
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
766
|
+
const indent = ' '.repeat(depth * LIST_INDENT_SIZE);
|
|
767
|
+
const listType = listNode.getListType();
|
|
768
|
+
const prefix = listType === 'number' ? `${listNode.getStart() + index}. ` : listType === 'check' ? `- [${listItemNode.getChecked() ? 'x' : ' '}] ` : '- ';
|
|
769
|
+
output.push(indent + prefix + exportChildren(listItemNode));
|
|
770
|
+
index++;
|
|
1416
771
|
}
|
|
1417
772
|
}
|
|
1418
773
|
|
|
1419
|
-
return output;
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
function getTextSibling(node, backward) {
|
|
1424
|
-
let sibling = backward ? node.getPreviousSibling() : node.getNextSibling();
|
|
1425
|
-
|
|
1426
|
-
if (!sibling) {
|
|
1427
|
-
const parent = node.getParentOrThrow();
|
|
774
|
+
return output.join('\n');
|
|
775
|
+
};
|
|
1428
776
|
|
|
1429
|
-
|
|
1430
|
-
|
|
777
|
+
const HEADING = {
|
|
778
|
+
export: (node, exportChildren) => {
|
|
779
|
+
if (!richText.$isHeadingNode(node)) {
|
|
780
|
+
return null;
|
|
1431
781
|
}
|
|
1432
|
-
}
|
|
1433
782
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
783
|
+
const level = Number(node.getTag().slice(1));
|
|
784
|
+
return '#'.repeat(level) + ' ' + exportChildren(node);
|
|
785
|
+
},
|
|
786
|
+
regExp: /^(#{1,6})\s/,
|
|
787
|
+
replace: replaceWithBlock(match => {
|
|
788
|
+
// $FlowFixMe[incompatible-cast]
|
|
789
|
+
const tag = 'h' + match[1].length;
|
|
790
|
+
return richText.$createHeadingNode(tag);
|
|
791
|
+
}),
|
|
792
|
+
type: 'element'
|
|
793
|
+
};
|
|
794
|
+
const QUOTE = {
|
|
795
|
+
export: (node, exportChildren) => {
|
|
796
|
+
return richText.$isQuoteNode(node) ? '> ' + exportChildren(node) : null;
|
|
797
|
+
},
|
|
798
|
+
regExp: /^>\s/,
|
|
799
|
+
replace: replaceWithBlock(() => richText.$createQuoteNode()),
|
|
800
|
+
type: 'element'
|
|
801
|
+
};
|
|
802
|
+
const CODE = {
|
|
803
|
+
export: node => {
|
|
804
|
+
if (!code.$isCodeNode(node)) {
|
|
805
|
+
return null;
|
|
1447
806
|
}
|
|
1448
807
|
|
|
1449
|
-
|
|
1450
|
-
|
|
808
|
+
const textContent = node.getTextContent();
|
|
809
|
+
return '```' + (node.getLanguage() || '') + (textContent ? '\n' + textContent : '') + '\n' + '```';
|
|
810
|
+
},
|
|
811
|
+
regExp: /^```(\w{1,10})?\s/,
|
|
812
|
+
replace: replaceWithBlock(match => {
|
|
813
|
+
return code.$createCodeNode(match ? match[1] : undefined);
|
|
814
|
+
}),
|
|
815
|
+
type: 'element'
|
|
816
|
+
};
|
|
817
|
+
const UNORDERED_LIST = {
|
|
818
|
+
export: (node, exportChildren) => {
|
|
819
|
+
return list.$isListNode(node) ? listExport(node, exportChildren, 0) : null;
|
|
820
|
+
},
|
|
821
|
+
regExp: /^(\s*)[-*+]\s/,
|
|
822
|
+
replace: listReplace('bullet'),
|
|
823
|
+
type: 'element'
|
|
824
|
+
};
|
|
825
|
+
const CHECK_LIST = {
|
|
826
|
+
export: (node, exportChildren) => {
|
|
827
|
+
return list.$isListNode(node) ? listExport(node, exportChildren, 0) : null;
|
|
828
|
+
},
|
|
829
|
+
regExp: /^(\s*)(?:-\s)?\s?(\[(\s|x)?\])\s/i,
|
|
830
|
+
replace: listReplace('check'),
|
|
831
|
+
type: 'element'
|
|
832
|
+
};
|
|
833
|
+
const ORDERED_LIST = {
|
|
834
|
+
export: (node, exportChildren) => {
|
|
835
|
+
return list.$isListNode(node) ? listExport(node, exportChildren, 0) : null;
|
|
836
|
+
},
|
|
837
|
+
regExp: /^(\s*)(\d{1,})\.\s/,
|
|
838
|
+
replace: listReplace('number'),
|
|
839
|
+
type: 'element'
|
|
840
|
+
};
|
|
841
|
+
const INLINE_CODE = {
|
|
842
|
+
format: ['code'],
|
|
843
|
+
tag: '`',
|
|
844
|
+
type: 'text-format'
|
|
845
|
+
};
|
|
846
|
+
const BOLD_ITALIC_STAR = {
|
|
847
|
+
format: ['bold', 'italic'],
|
|
848
|
+
tag: '***',
|
|
849
|
+
type: 'text-format'
|
|
850
|
+
};
|
|
851
|
+
const BOLD_ITALIC_UNDERSCORE = {
|
|
852
|
+
format: ['bold', 'italic'],
|
|
853
|
+
intraword: false,
|
|
854
|
+
tag: '___',
|
|
855
|
+
type: 'text-format'
|
|
856
|
+
};
|
|
857
|
+
const BOLD_STAR = {
|
|
858
|
+
format: ['bold'],
|
|
859
|
+
tag: '**',
|
|
860
|
+
type: 'text-format'
|
|
861
|
+
};
|
|
862
|
+
const BOLD_UNDERSCORE = {
|
|
863
|
+
format: ['bold'],
|
|
864
|
+
intraword: false,
|
|
865
|
+
tag: '__',
|
|
866
|
+
type: 'text-format'
|
|
867
|
+
};
|
|
868
|
+
const STRIKETHROUGH = {
|
|
869
|
+
format: ['strikethrough'],
|
|
870
|
+
tag: '~~',
|
|
871
|
+
type: 'text-format'
|
|
872
|
+
};
|
|
873
|
+
const ITALIC_STAR = {
|
|
874
|
+
format: ['italic'],
|
|
875
|
+
tag: '*',
|
|
876
|
+
type: 'text-format'
|
|
877
|
+
};
|
|
878
|
+
const ITALIC_UNDERSCORE = {
|
|
879
|
+
format: ['italic'],
|
|
880
|
+
intraword: false,
|
|
881
|
+
tag: '_',
|
|
882
|
+
type: 'text-format'
|
|
883
|
+
}; // Order of text transformers matters:
|
|
884
|
+
//
|
|
885
|
+
// - code should go first as it prevents any transformations inside
|
|
886
|
+
// - then longer tags match (e.g. ** or __ should go before * or _)
|
|
887
|
+
|
|
888
|
+
const LINK = {
|
|
889
|
+
export: (node, exportChildren, exportFormat) => {
|
|
890
|
+
if (!link.$isLinkNode(node)) {
|
|
891
|
+
return null;
|
|
1451
892
|
}
|
|
1452
|
-
}
|
|
1453
893
|
|
|
1454
|
-
|
|
1455
|
-
|
|
894
|
+
const linkContent = `[${node.getTextContent()}](${node.getURL()})`;
|
|
895
|
+
const firstChild = node.getFirstChild(); // Add text styles only if link has single text node inside. If it's more
|
|
896
|
+
// then one we ignore it as markdown does not support nested styles for links
|
|
1456
897
|
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
}
|
|
898
|
+
if (node.getChildrenSize() === 1 && lexical.$isTextNode(firstChild)) {
|
|
899
|
+
return exportFormat(firstChild, linkContent);
|
|
900
|
+
} else {
|
|
901
|
+
return linkContent;
|
|
902
|
+
}
|
|
903
|
+
},
|
|
904
|
+
importRegExp: /(?:\[([^[]+)\])(?:\(([^(]+)\))/,
|
|
905
|
+
regExp: /(?:\[([^[]+)\])(?:\(([^(]+)\))$/,
|
|
906
|
+
replace: (textNode, match) => {
|
|
907
|
+
const [, linkText, linkUrl] = match;
|
|
908
|
+
const linkNode = link.$createLinkNode(linkUrl);
|
|
909
|
+
const linkTextNode = lexical.$createTextNode(linkText);
|
|
910
|
+
linkTextNode.setFormat(textNode.getFormat());
|
|
911
|
+
linkNode.append(linkTextNode);
|
|
912
|
+
textNode.replace(linkNode);
|
|
913
|
+
},
|
|
914
|
+
trigger: ')',
|
|
915
|
+
type: 'text-match'
|
|
916
|
+
};
|
|
1460
917
|
|
|
1461
918
|
/**
|
|
1462
919
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
@@ -1466,36 +923,44 @@ function hasFormat(node, format) {
|
|
|
1466
923
|
*
|
|
1467
924
|
*
|
|
1468
925
|
*/
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
// However, given "#A B", where the user delets "A" should not.
|
|
1474
|
-
let priorTriggerState = null;
|
|
1475
|
-
return editor.registerUpdateListener(({
|
|
1476
|
-
tags
|
|
1477
|
-
}) => {
|
|
1478
|
-
// Examine historic so that we are not running autoformatting within markdown.
|
|
1479
|
-
if (tags.has('historic') === false) {
|
|
1480
|
-
const currentTriggerState = getTriggerState(editor.getEditorState());
|
|
1481
|
-
const scanningContext = currentTriggerState == null ? null : findScanningContext(editor, currentTriggerState, priorTriggerState);
|
|
926
|
+
const ELEMENT_TRANSFORMERS = [HEADING, QUOTE, CODE, UNORDERED_LIST, ORDERED_LIST]; // Order of text format transformers matters:
|
|
927
|
+
//
|
|
928
|
+
// - code should go first as it prevents any transformations inside
|
|
929
|
+
// - then longer tags match (e.g. ** or __ should go before * or _)
|
|
1482
930
|
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
931
|
+
const TEXT_FORMAT_TRANSFORMERS = [INLINE_CODE, BOLD_ITALIC_STAR, BOLD_ITALIC_UNDERSCORE, BOLD_STAR, BOLD_UNDERSCORE, ITALIC_STAR, ITALIC_UNDERSCORE, STRIKETHROUGH];
|
|
932
|
+
const TEXT_MATCH_TRANSFORMERS = [LINK];
|
|
933
|
+
const TRANSFORMERS = [...ELEMENT_TRANSFORMERS, ...TEXT_FORMAT_TRANSFORMERS, ...TEXT_MATCH_TRANSFORMERS];
|
|
1486
934
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
}
|
|
1491
|
-
});
|
|
935
|
+
function $convertFromMarkdownString(markdown, transformers = TRANSFORMERS) {
|
|
936
|
+
const importMarkdown = createMarkdownImport(transformers);
|
|
937
|
+
return importMarkdown(markdown);
|
|
1492
938
|
}
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
939
|
+
|
|
940
|
+
function $convertToMarkdownString(transformers = TRANSFORMERS) {
|
|
941
|
+
const exportMarkdown = createMarkdownExport(transformers);
|
|
942
|
+
return exportMarkdown();
|
|
1497
943
|
}
|
|
1498
944
|
|
|
1499
945
|
exports.$convertFromMarkdownString = $convertFromMarkdownString;
|
|
1500
946
|
exports.$convertToMarkdownString = $convertToMarkdownString;
|
|
947
|
+
exports.BOLD_ITALIC_STAR = BOLD_ITALIC_STAR;
|
|
948
|
+
exports.BOLD_ITALIC_UNDERSCORE = BOLD_ITALIC_UNDERSCORE;
|
|
949
|
+
exports.BOLD_STAR = BOLD_STAR;
|
|
950
|
+
exports.BOLD_UNDERSCORE = BOLD_UNDERSCORE;
|
|
951
|
+
exports.CHECK_LIST = CHECK_LIST;
|
|
952
|
+
exports.CODE = CODE;
|
|
953
|
+
exports.ELEMENT_TRANSFORMERS = ELEMENT_TRANSFORMERS;
|
|
954
|
+
exports.HEADING = HEADING;
|
|
955
|
+
exports.INLINE_CODE = INLINE_CODE;
|
|
956
|
+
exports.ITALIC_STAR = ITALIC_STAR;
|
|
957
|
+
exports.ITALIC_UNDERSCORE = ITALIC_UNDERSCORE;
|
|
958
|
+
exports.LINK = LINK;
|
|
959
|
+
exports.ORDERED_LIST = ORDERED_LIST;
|
|
960
|
+
exports.QUOTE = QUOTE;
|
|
961
|
+
exports.STRIKETHROUGH = STRIKETHROUGH;
|
|
962
|
+
exports.TEXT_FORMAT_TRANSFORMERS = TEXT_FORMAT_TRANSFORMERS;
|
|
963
|
+
exports.TEXT_MATCH_TRANSFORMERS = TEXT_MATCH_TRANSFORMERS;
|
|
964
|
+
exports.TRANSFORMERS = TRANSFORMERS;
|
|
965
|
+
exports.UNORDERED_LIST = UNORDERED_LIST;
|
|
1501
966
|
exports.registerMarkdownShortcuts = registerMarkdownShortcuts;
|