@lexical/markdown 0.2.5 → 0.2.6
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 +83 -6
- package/LexicalMarkdown.dev.js +669 -1226
- package/LexicalMarkdown.js.flow +83 -6
- package/LexicalMarkdown.prod.js +20 -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,390 @@ 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
|
|
542
|
-
|
|
543
|
-
const prevElement = element.getPreviousSibling();
|
|
544
|
-
|
|
545
|
-
if (list.$isListNode(prevElement) && prevElement.getTag() === tag) {
|
|
546
|
-
prevElement.append(listItem);
|
|
547
|
-
element.remove();
|
|
548
|
-
} else {
|
|
549
|
-
const list$1 = list.$createListNode(tag, start);
|
|
550
|
-
list$1.append(listItem);
|
|
551
|
-
element.replace(list$1);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
if (indent) {
|
|
555
|
-
listItem.setIndent(indent);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
function transformTextNodeForMarkdownCriteria(scanningContext, elementNode, createHorizontalRuleNode) {
|
|
560
|
-
if (scanningContext.markdownCriteria.requiresParagraphStart === true) {
|
|
561
|
-
transformTextNodeForElementNode(elementNode, scanningContext, createHorizontalRuleNode);
|
|
562
|
-
} else {
|
|
563
|
-
transformTextNodeForText(scanningContext, elementNode);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
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)
|
|
566
149
|
|
|
567
|
-
function transformTextNodeForElementNode(elementNode, scanningContext, createHorizontalRuleNode) {
|
|
568
|
-
if (scanningContext.textNodeWithOffset != null) {
|
|
569
|
-
const textNodeWithOffset = getTextNodeWithOffsetOrThrow(scanningContext);
|
|
570
150
|
|
|
571
|
-
|
|
572
|
-
|
|
151
|
+
function getTextSibling(node, backward) {
|
|
152
|
+
let sibling = backward ? node.getPreviousSibling() : node.getNextSibling();
|
|
573
153
|
|
|
574
|
-
|
|
154
|
+
if (!sibling) {
|
|
155
|
+
const parent = node.getParentOrThrow();
|
|
575
156
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
textNode.remove();
|
|
579
|
-
}
|
|
157
|
+
if (parent.isInline()) {
|
|
158
|
+
sibling = backward ? parent.getPreviousSibling() : parent.getNextSibling();
|
|
580
159
|
}
|
|
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
160
|
}
|
|
594
|
-
}
|
|
595
161
|
|
|
596
|
-
|
|
597
|
-
|
|
162
|
+
while (sibling) {
|
|
163
|
+
if (lexical.$isElementNode(sibling)) {
|
|
164
|
+
if (!sibling.isInline()) {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
598
167
|
|
|
599
|
-
|
|
600
|
-
const formatting = getTextFormatType(markdownCriteria.markdownFormatKind);
|
|
168
|
+
const descendant = backward ? sibling.getLastDescendant() : sibling.getFirstDescendant();
|
|
601
169
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
170
|
+
if (lexical.$isTextNode(descendant)) {
|
|
171
|
+
return descendant;
|
|
172
|
+
} else {
|
|
173
|
+
sibling = backward ? sibling.getPreviousSibling() : sibling.getNextSibling();
|
|
174
|
+
}
|
|
605
175
|
}
|
|
606
176
|
|
|
607
|
-
if (
|
|
608
|
-
|
|
177
|
+
if (lexical.$isTextNode(sibling)) {
|
|
178
|
+
return sibling;
|
|
609
179
|
}
|
|
610
180
|
}
|
|
611
|
-
}
|
|
612
181
|
|
|
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);
|
|
182
|
+
return null;
|
|
639
183
|
}
|
|
640
184
|
|
|
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();
|
|
185
|
+
function hasFormat(node, format) {
|
|
186
|
+
return lexical.$isTextNode(node) && node.hasFormat(format);
|
|
685
187
|
}
|
|
686
188
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
191
|
+
*
|
|
192
|
+
* This source code is licensed under the MIT license found in the
|
|
193
|
+
* LICENSE file in the root directory of this source tree.
|
|
194
|
+
*
|
|
195
|
+
*
|
|
196
|
+
*/
|
|
197
|
+
const CODE_BLOCK_REG_EXP = /^```(\w{1,10})?\s?$/;
|
|
198
|
+
function createMarkdownImport(transformers) {
|
|
199
|
+
const byType = transformersByType(transformers);
|
|
200
|
+
const textFormatTransformersIndex = createTextFormatTransformersIndex(byType.textFormat);
|
|
201
|
+
return markdownString => {
|
|
202
|
+
const lines = markdownString.split('\n');
|
|
203
|
+
const linesLength = lines.length;
|
|
204
|
+
const root = lexical.$getRoot();
|
|
205
|
+
root.clear();
|
|
698
206
|
|
|
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
|
-
}
|
|
207
|
+
for (let i = 0; i < linesLength; i++) {
|
|
208
|
+
const lineText = lines[i]; // Codeblocks are processed first as anything inside such block
|
|
209
|
+
// is ignored for further processing
|
|
210
|
+
// TODO:
|
|
211
|
+
// Abstract it to be dynamic as other transformers (add multiline match option)
|
|
712
212
|
|
|
713
|
-
|
|
714
|
-
{
|
|
715
|
-
return ['italic', 'bold'];
|
|
716
|
-
}
|
|
213
|
+
const [codeBlockNode, shiftedIndex] = importCodeBlock(lines, i, root);
|
|
717
214
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
215
|
+
if (codeBlockNode != null) {
|
|
216
|
+
i = shiftedIndex;
|
|
217
|
+
continue;
|
|
721
218
|
}
|
|
722
219
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
return ['strikethrough', 'bold'];
|
|
726
|
-
}
|
|
727
|
-
}
|
|
220
|
+
importBlocks(lineText, root, byType.element, textFormatTransformersIndex, byType.textMatch);
|
|
221
|
+
}
|
|
728
222
|
|
|
729
|
-
|
|
223
|
+
root.selectEnd();
|
|
224
|
+
};
|
|
730
225
|
}
|
|
731
226
|
|
|
732
|
-
function
|
|
733
|
-
const
|
|
734
|
-
const
|
|
735
|
-
|
|
227
|
+
function importBlocks(lineText, rootNode, elementTransformers, textFormatTransformersIndex, textMatchTransformers) {
|
|
228
|
+
const textNode = lexical.$createTextNode(lineText);
|
|
229
|
+
const elementNode = lexical.$createParagraphNode();
|
|
230
|
+
elementNode.append(textNode);
|
|
231
|
+
rootNode.append(elementNode);
|
|
736
232
|
|
|
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
|
-
}
|
|
233
|
+
for (const {
|
|
234
|
+
regExp,
|
|
235
|
+
replace
|
|
236
|
+
} of elementTransformers) {
|
|
237
|
+
const match = lineText.match(regExp);
|
|
755
238
|
|
|
756
|
-
|
|
757
|
-
|
|
239
|
+
if (match) {
|
|
240
|
+
textNode.setTextContent(lineText.slice(match[0].length));
|
|
241
|
+
replace(elementNode, [textNode], match, true);
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
758
244
|
}
|
|
759
245
|
|
|
760
|
-
|
|
761
|
-
selection.anchor.set(anchorTextNodeWithOffset.node.getKey(), anchorTextNodeWithOffset.offset, 'text');
|
|
762
|
-
selection.focus.set(focusTextNodeWithOffset.node.getKey(), focusTextNodeWithOffset.offset, 'text');
|
|
763
|
-
return selection;
|
|
246
|
+
importTextFormatTransformers(textNode, textFormatTransformersIndex, textMatchTransformers);
|
|
764
247
|
}
|
|
765
248
|
|
|
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();
|
|
249
|
+
function importCodeBlock(lines, startLineIndex, rootNode) {
|
|
250
|
+
const openMatch = lines[startLineIndex].match(CODE_BLOCK_REG_EXP);
|
|
774
251
|
|
|
775
|
-
|
|
776
|
-
|
|
252
|
+
if (openMatch) {
|
|
253
|
+
let endLineIndex = startLineIndex;
|
|
254
|
+
const linesLength = lines.length;
|
|
777
255
|
|
|
778
|
-
|
|
779
|
-
const
|
|
256
|
+
while (++endLineIndex < linesLength) {
|
|
257
|
+
const closeMatch = lines[endLineIndex].match(CODE_BLOCK_REG_EXP);
|
|
780
258
|
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
if (i <= focusCaptureGroupIndex) {
|
|
789
|
-
runningLength += captureGroupDetail.text.length;
|
|
790
|
-
captureGroupDetail.text = '';
|
|
791
|
-
}
|
|
259
|
+
if (closeMatch) {
|
|
260
|
+
const codeBlockNode = code.$createCodeNode(openMatch[1]);
|
|
261
|
+
const textNode = lexical.$createTextNode(lines.slice(startLineIndex + 1, endLineIndex).join('\n'));
|
|
262
|
+
codeBlockNode.append(textNode);
|
|
263
|
+
rootNode.append(codeBlockNode);
|
|
264
|
+
return [codeBlockNode, endLineIndex];
|
|
792
265
|
}
|
|
793
266
|
}
|
|
794
267
|
}
|
|
795
|
-
}
|
|
796
268
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
269
|
+
return [null, startLineIndex];
|
|
270
|
+
} // Processing text content and replaces text format tags.
|
|
271
|
+
// It takes outermost tag match and its content, creates text node with
|
|
272
|
+
// format based on tag and then recursively executed over node's content
|
|
273
|
+
//
|
|
274
|
+
// E.g. for "*Hello **world**!*" string it will create text node with
|
|
275
|
+
// "Hello **world**!" content and italic format and run recursively over
|
|
276
|
+
// its content to transform "**world**" part
|
|
277
|
+
|
|
801
278
|
|
|
802
|
-
|
|
279
|
+
function importTextFormatTransformers(textNode, textFormatTransformersIndex, textMatchTransformers) {
|
|
280
|
+
const textContent = textNode.getTextContent();
|
|
281
|
+
const match = findOutermostMatch(textContent, textFormatTransformersIndex);
|
|
282
|
+
|
|
283
|
+
if (!match) {
|
|
284
|
+
// Once text format processing is done run text match transformers, as it
|
|
285
|
+
// only can span within single text node (unline formats that can cover multiple nodes)
|
|
286
|
+
importTextMatchTransformers(textNode, textMatchTransformers);
|
|
803
287
|
return;
|
|
804
288
|
}
|
|
805
289
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
text
|
|
810
|
-
};
|
|
811
|
-
const newSelection = createSelectionWithCaptureGroups(captureGroupIndex, captureGroupIndex, false, false, scanningContext, parentElementNode);
|
|
290
|
+
let currentNode, remainderNode; // If matching full content there's no need to run splitText and can reuse existing textNode
|
|
291
|
+
// to update its content and apply format. E.g. for **_Hello_** string after applying bold
|
|
292
|
+
// format (**) it will reuse the same text node to apply italic (_)
|
|
812
293
|
|
|
813
|
-
if (
|
|
814
|
-
|
|
815
|
-
|
|
294
|
+
if (match[0] === textContent) {
|
|
295
|
+
currentNode = textNode;
|
|
296
|
+
} else {
|
|
297
|
+
const startIndex = match.index;
|
|
298
|
+
const endIndex = startIndex + match[0].length;
|
|
816
299
|
|
|
817
|
-
if (
|
|
818
|
-
|
|
300
|
+
if (startIndex === 0) {
|
|
301
|
+
[currentNode, remainderNode] = textNode.splitText(endIndex);
|
|
302
|
+
} else {
|
|
303
|
+
[, currentNode, remainderNode] = textNode.splitText(startIndex, endIndex);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
819
306
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
const newGroupCount = regExCaptureGroups.length;
|
|
307
|
+
currentNode.setTextContent(match[2]);
|
|
308
|
+
const transformer = textFormatTransformersIndex.transformersByTag[match[1]];
|
|
823
309
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
310
|
+
if (transformer) {
|
|
311
|
+
for (const format of transformer.format) {
|
|
312
|
+
if (!currentNode.hasFormat(format)) {
|
|
313
|
+
currentNode.toggleFormat(format);
|
|
827
314
|
}
|
|
828
315
|
}
|
|
829
|
-
}
|
|
830
|
-
}
|
|
316
|
+
} // Recursively run over inner text if it's not inline code
|
|
831
317
|
|
|
832
|
-
function formatTextInCaptureGroupIndex(formatTypes, captureGroupIndex, scanningContext, parentElementNode) {
|
|
833
|
-
const patternMatchResults = scanningContext.patternMatchResults;
|
|
834
|
-
const regExCaptureGroups = patternMatchResults.regExCaptureGroups;
|
|
835
|
-
const regExCaptureGroupsCount = regExCaptureGroups.length;
|
|
836
318
|
|
|
837
|
-
if (!(
|
|
838
|
-
|
|
839
|
-
}
|
|
319
|
+
if (!currentNode.hasFormat('code')) {
|
|
320
|
+
importTextFormatTransformers(currentNode, textFormatTransformersIndex, textMatchTransformers);
|
|
321
|
+
} // Run over remaining text if any
|
|
840
322
|
|
|
841
|
-
const captureGroupDetail = regExCaptureGroups[captureGroupIndex];
|
|
842
323
|
|
|
843
|
-
if (
|
|
844
|
-
|
|
324
|
+
if (remainderNode) {
|
|
325
|
+
importTextFormatTransformers(remainderNode, textFormatTransformersIndex, textMatchTransformers);
|
|
845
326
|
}
|
|
327
|
+
}
|
|
846
328
|
|
|
847
|
-
|
|
329
|
+
function importTextMatchTransformers(textNode_, textMatchTransformers) {
|
|
330
|
+
let textNode = textNode_;
|
|
848
331
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
332
|
+
mainLoop: while (textNode) {
|
|
333
|
+
for (const transformer of textMatchTransformers) {
|
|
334
|
+
const match = textNode.getTextContent().match(transformer.importRegExp);
|
|
852
335
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
currentSelection.formatText(formatTypes[i]);
|
|
336
|
+
if (!match) {
|
|
337
|
+
continue;
|
|
856
338
|
}
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
} // Place caret at end of final capture group.
|
|
860
339
|
|
|
340
|
+
const startIndex = match.index;
|
|
341
|
+
const endIndex = startIndex + match[0].length;
|
|
342
|
+
let replaceNode;
|
|
861
343
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
// Ignore capture group 0, as regEx defaults the 0th one to the entire matched string.
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
344
|
+
if (startIndex === 0) {
|
|
345
|
+
[replaceNode, textNode] = textNode.splitText(endIndex);
|
|
346
|
+
} else {
|
|
347
|
+
[, replaceNode, textNode] = textNode.splitText(startIndex, endIndex);
|
|
348
|
+
}
|
|
870
349
|
|
|
871
|
-
|
|
872
|
-
|
|
350
|
+
transformer.replace(replaceNode, match);
|
|
351
|
+
continue mainLoop;
|
|
352
|
+
}
|
|
873
353
|
|
|
874
|
-
|
|
875
|
-
lexical.$setSelection(newSelection);
|
|
354
|
+
break;
|
|
876
355
|
}
|
|
877
|
-
}
|
|
356
|
+
} // Finds first "<tag>content<tag>" match that is not nested into another tag
|
|
878
357
|
|
|
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
358
|
|
|
359
|
+
function findOutermostMatch(textContent, textTransformersIndex) {
|
|
360
|
+
const openTagsMatch = textContent.match(textTransformersIndex.openTagsRegExp);
|
|
889
361
|
|
|
890
|
-
|
|
362
|
+
if (openTagsMatch == null) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
891
365
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
366
|
+
for (const match of openTagsMatch) {
|
|
367
|
+
// Open tags reg exp might capture leading space so removing it
|
|
368
|
+
// before using match to find transformer
|
|
369
|
+
const fullMatchRegExp = textTransformersIndex.fullMatchRegExpByTag[match.replace(/^\s/, '')];
|
|
896
370
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
const firstChild = listItemNode.getFirstChild();
|
|
371
|
+
if (fullMatchRegExp == null) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
901
374
|
|
|
902
|
-
|
|
903
|
-
output.push(processNestedLists(firstChild, exportChildren, depth + 1));
|
|
904
|
-
continue;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
375
|
+
const fullMatch = textContent.match(fullMatchRegExp);
|
|
907
376
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
output.push(indent + prefix + exportChildren(listItemNode));
|
|
911
|
-
index++;
|
|
377
|
+
if (fullMatch != null) {
|
|
378
|
+
return fullMatch;
|
|
912
379
|
}
|
|
913
380
|
}
|
|
914
381
|
|
|
915
|
-
return
|
|
382
|
+
return null;
|
|
916
383
|
}
|
|
917
384
|
|
|
918
|
-
function
|
|
919
|
-
|
|
920
|
-
}
|
|
385
|
+
function createTextFormatTransformersIndex(textTransformers) {
|
|
386
|
+
const transformersByTag = {};
|
|
387
|
+
const fullMatchRegExpByTag = {};
|
|
388
|
+
const openTagsRegExp = [];
|
|
921
389
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
390
|
+
for (const transformer of textTransformers) {
|
|
391
|
+
const {
|
|
392
|
+
tag
|
|
393
|
+
} = transformer;
|
|
394
|
+
transformersByTag[tag] = transformer;
|
|
395
|
+
const tagRegExp = tag.replace(/(\*|\^)/g, '\\$1');
|
|
396
|
+
openTagsRegExp.push(tagRegExp);
|
|
397
|
+
fullMatchRegExpByTag[tag] = new RegExp(`(${tagRegExp})(?![${tagRegExp}\\s])(.*?[^${tagRegExp}\\s])${tagRegExp}(?!${tagRegExp})`);
|
|
925
398
|
}
|
|
926
399
|
|
|
927
|
-
|
|
928
|
-
|
|
400
|
+
return {
|
|
401
|
+
// Reg exp to find open tag + content + close tag
|
|
402
|
+
fullMatchRegExpByTag,
|
|
403
|
+
// Reg exp to find opening tags
|
|
404
|
+
openTagsRegExp: new RegExp('(' + openTagsRegExp.join('|') + ')', 'g'),
|
|
405
|
+
transformersByTag
|
|
406
|
+
};
|
|
929
407
|
}
|
|
930
408
|
|
|
931
409
|
/**
|
|
@@ -937,384 +415,271 @@ function codeBlockExport(node, exportChildren) {
|
|
|
937
415
|
*
|
|
938
416
|
*/
|
|
939
417
|
|
|
940
|
-
function
|
|
941
|
-
|
|
942
|
-
return null;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
const node = selection.anchor.getNode();
|
|
418
|
+
function runElementTransformers(parentNode, anchorNode, anchorOffset, elementTransformers) {
|
|
419
|
+
const grandParentNode = parentNode.getParent();
|
|
946
420
|
|
|
947
|
-
if (!lexical.$
|
|
948
|
-
return
|
|
421
|
+
if (!lexical.$isRootNode(grandParentNode) || parentNode.getFirstChild() !== anchorNode) {
|
|
422
|
+
return false;
|
|
949
423
|
}
|
|
950
424
|
|
|
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;
|
|
425
|
+
const textContent = anchorNode.getTextContent(); // Checking for anchorOffset position to prevent any checks for cases when caret is too far
|
|
426
|
+
// from a line start to be a part of block-level markdown trigger.
|
|
427
|
+
//
|
|
428
|
+
// TODO:
|
|
429
|
+
// Can have a quick check if caret is close enough to the beginning of the string (e.g. offset less than 10-20)
|
|
430
|
+
// since otherwise it won't be a markdown shortcut, but tables are exception
|
|
969
431
|
|
|
970
|
-
|
|
971
|
-
|
|
432
|
+
if (textContent[anchorOffset - 1] !== ' ') {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
972
435
|
|
|
973
|
-
|
|
974
|
-
|
|
436
|
+
for (const {
|
|
437
|
+
regExp,
|
|
438
|
+
replace
|
|
439
|
+
} of elementTransformers) {
|
|
440
|
+
const match = textContent.match(regExp);
|
|
975
441
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
442
|
+
if (match && match[0].length === anchorOffset) {
|
|
443
|
+
const nextSiblings = anchorNode.getNextSiblings();
|
|
444
|
+
const [leadingNode, remainderNode] = anchorNode.splitText(anchorOffset);
|
|
445
|
+
leadingNode.remove();
|
|
446
|
+
const siblings = remainderNode ? [remainderNode, ...nextSiblings] : nextSiblings;
|
|
447
|
+
replace(parentNode, siblings, match, false);
|
|
448
|
+
return true;
|
|
982
449
|
}
|
|
983
450
|
}
|
|
984
451
|
|
|
985
|
-
return
|
|
986
|
-
markdownCriteria: null,
|
|
987
|
-
patternMatchResults: null
|
|
988
|
-
};
|
|
452
|
+
return false;
|
|
989
453
|
}
|
|
990
454
|
|
|
991
|
-
function
|
|
992
|
-
let
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
if (textNodeWithOffset === null) {
|
|
997
|
-
return;
|
|
998
|
-
} // Please see the declaration of ScanningContext for a detailed explanation.
|
|
999
|
-
|
|
455
|
+
function runTextMatchTransformers(anchorNode, anchorOffset, transformersByTrigger) {
|
|
456
|
+
let textContent = anchorNode.getTextContent();
|
|
457
|
+
const lastChar = textContent[anchorOffset - 1];
|
|
458
|
+
const transformers = transformersByTrigger[lastChar];
|
|
1000
459
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
460
|
+
if (transformers == null) {
|
|
461
|
+
return false;
|
|
462
|
+
} // If typing in the middle of content, remove the tail to do
|
|
463
|
+
// reg exp match up to a string end (caret position)
|
|
1004
464
|
|
|
1005
|
-
if (criteriaWithPatternMatchResults.markdownCriteria === null || criteriaWithPatternMatchResults.patternMatchResults === null) {
|
|
1006
|
-
return;
|
|
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
465
|
|
|
1017
|
-
|
|
1018
|
-
|
|
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;
|
|
466
|
+
if (anchorOffset < textContent.length) {
|
|
467
|
+
textContent = textContent.slice(0, anchorOffset);
|
|
1046
468
|
}
|
|
1047
469
|
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
for (let ti = 0; ti < triggerCount; ti++) {
|
|
1052
|
-
const triggerString = triggerArray[ti].triggerString; // The below checks needs to execute relativey quickly, so perform the light-weight ones first.
|
|
1053
|
-
// The substr check is a quick way to avoid autoformat parsing in that it looks for the autoformat
|
|
1054
|
-
// trigger which is the trigger string (" ").
|
|
1055
|
-
|
|
1056
|
-
const triggerStringLength = triggerString.length;
|
|
1057
|
-
const currentTextContentLength = currentTriggerState.textContent.length;
|
|
1058
|
-
const triggerOffset = currentTriggerState.anchorOffset - triggerStringLength; // Todo: these checks help w/ performance, yet we can do more.
|
|
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.
|
|
470
|
+
for (const transformer of transformers) {
|
|
471
|
+
const match = textContent.match(transformer.regExp);
|
|
1061
472
|
|
|
1062
|
-
if (
|
|
1063
|
-
|
|
1064
|
-
return null;
|
|
473
|
+
if (match === null) {
|
|
474
|
+
continue;
|
|
1065
475
|
}
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
return findScanningContextWithValidMatch(editor, currentTriggerState);
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
/**
|
|
1072
|
-
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
1073
|
-
*
|
|
1074
|
-
* This source code is licensed under the MIT license found in the
|
|
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
|
-
}
|
|
1083
476
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
477
|
+
const startIndex = match.index;
|
|
478
|
+
const endIndex = startIndex + match[0].length;
|
|
479
|
+
let replaceNode;
|
|
1087
480
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
nodes.push(lexical.$createParagraphNode().append(lexical.$createTextNode(splitLines[i])));
|
|
481
|
+
if (startIndex === 0) {
|
|
482
|
+
[replaceNode] = anchorNode.splitText(endIndex);
|
|
1091
483
|
} else {
|
|
1092
|
-
|
|
484
|
+
[, replaceNode] = anchorNode.splitText(startIndex, endIndex);
|
|
1093
485
|
}
|
|
1094
|
-
}
|
|
1095
486
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
root.append(...nodes);
|
|
1100
|
-
return root;
|
|
487
|
+
replaceNode.selectNext();
|
|
488
|
+
transformer.replace(replaceNode, match);
|
|
489
|
+
return true;
|
|
1101
490
|
}
|
|
1102
491
|
|
|
1103
|
-
return
|
|
492
|
+
return false;
|
|
1104
493
|
}
|
|
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
494
|
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
495
|
+
function runTextFormatTransformers(editor, anchorNode, anchorOffset, textFormatTransformers) {
|
|
496
|
+
const textContent = anchorNode.getTextContent();
|
|
497
|
+
const closeTagEndIndex = anchorOffset - 1;
|
|
498
|
+
const closeChar = textContent[closeTagEndIndex]; // Quick check if we're possibly at the end of inline markdown style
|
|
1116
499
|
|
|
1117
|
-
|
|
1118
|
-
const elementNode = elementNodes[i];
|
|
500
|
+
const matchers = textFormatTransformers[closeChar];
|
|
1119
501
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
502
|
+
if (!matchers) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
1123
505
|
|
|
506
|
+
for (const matcher of matchers) {
|
|
507
|
+
const {
|
|
508
|
+
tag
|
|
509
|
+
} = matcher;
|
|
510
|
+
const tagLength = tag.length;
|
|
511
|
+
const closeTagStartIndex = closeTagEndIndex - tagLength + 1; // If tag is not single char check if rest of it matches with text content
|
|
512
|
+
|
|
513
|
+
if (tagLength > 1) {
|
|
514
|
+
if (!isEqualSubString(textContent, closeTagStartIndex, tag, 0, tagLength)) {
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
} // Space before closing tag cancels inline markdown
|
|
1124
518
|
|
|
1125
|
-
resetScanningContext(scanningContext);
|
|
1126
519
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
startIndex = i;
|
|
1130
|
-
done = false;
|
|
1131
|
-
break;
|
|
1132
|
-
}
|
|
520
|
+
if (textContent[closeTagStartIndex - 1] === ' ') {
|
|
521
|
+
continue;
|
|
1133
522
|
}
|
|
1134
|
-
} // while
|
|
1135
|
-
|
|
1136
523
|
|
|
1137
|
-
|
|
1138
|
-
|
|
524
|
+
const closeNode = anchorNode;
|
|
525
|
+
let openNode = closeNode;
|
|
526
|
+
let openTagStartIndex = getOpenTagStartIndex(textContent, closeTagStartIndex, tag); // Go through text node siblings and search for opening tag
|
|
527
|
+
// if haven't found it within the same text node as closing tag
|
|
1139
528
|
|
|
1140
|
-
|
|
1141
|
-
done = true;
|
|
1142
|
-
const elementNodes = root.getChildren();
|
|
1143
|
-
const countOfElementNodes = elementNodes.length;
|
|
529
|
+
let sibling = openNode;
|
|
1144
530
|
|
|
1145
|
-
|
|
1146
|
-
|
|
531
|
+
while (openTagStartIndex < 0 && (sibling = sibling.getPreviousSibling())) {
|
|
532
|
+
if (lexical.$isLineBreakNode(sibling)) {
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
1147
535
|
|
|
1148
|
-
if (lexical.$
|
|
1149
|
-
|
|
1150
|
-
|
|
536
|
+
if (lexical.$isTextNode(sibling)) {
|
|
537
|
+
const siblingTextContent = sibling.getTextContent();
|
|
538
|
+
openNode = sibling;
|
|
539
|
+
openTagStartIndex = getOpenTagStartIndex(siblingTextContent, siblingTextContent.length, tag);
|
|
540
|
+
}
|
|
541
|
+
} // Opening tag is not found
|
|
1151
542
|
|
|
1152
543
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
544
|
+
if (openTagStartIndex < 0) {
|
|
545
|
+
continue;
|
|
546
|
+
} // No content between opening and closing tag
|
|
1156
547
|
|
|
1157
|
-
}
|
|
1158
548
|
|
|
1159
|
-
|
|
1160
|
-
|
|
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
|
-
}
|
|
549
|
+
if (openNode === closeNode && openTagStartIndex + tagLength === closeTagStartIndex) {
|
|
550
|
+
continue;
|
|
551
|
+
} // Checking longer tags for repeating chars (e.g. *** vs **)
|
|
1181
552
|
|
|
1182
|
-
scanningContext.markdownCriteria = getCodeBlockCriteria(); // Perform text transformation here.
|
|
1183
553
|
|
|
1184
|
-
|
|
1185
|
-
return;
|
|
1186
|
-
}
|
|
554
|
+
const prevOpenNodeText = openNode.getTextContent();
|
|
1187
555
|
|
|
1188
|
-
if (
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
556
|
+
if (openTagStartIndex > 0 && prevOpenNodeText[openTagStartIndex - 1] === closeChar) {
|
|
557
|
+
continue;
|
|
558
|
+
} // Clean text from opening and closing tags (starting from closing tag
|
|
559
|
+
// to prevent any offset shifts if we start from opening one)
|
|
1192
560
|
|
|
1193
|
-
if (!(firstChild != null && firstChildIsTextNode)) {
|
|
1194
|
-
throw Error(`Expect paragraph containing only text nodes.`);
|
|
1195
|
-
}
|
|
1196
561
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
562
|
+
const prevCloseNodeText = closeNode.getTextContent();
|
|
563
|
+
const closeNodeText = prevCloseNodeText.slice(0, closeTagStartIndex) + prevCloseNodeText.slice(closeTagEndIndex + 1);
|
|
564
|
+
closeNode.setTextContent(closeNodeText);
|
|
565
|
+
const openNodeText = openNode === closeNode ? closeNodeText : prevOpenNodeText;
|
|
566
|
+
openNode.setTextContent(openNodeText.slice(0, openTagStartIndex) + openNodeText.slice(openTagStartIndex + tagLength));
|
|
567
|
+
const nextSelection = lexical.$createRangeSelection();
|
|
568
|
+
lexical.$setSelection(nextSelection); // Adjust offset based on deleted chars
|
|
1201
569
|
|
|
1202
|
-
|
|
1203
|
-
|
|
570
|
+
const newOffset = closeTagEndIndex - tagLength * (openNode === closeNode ? 2 : 1) + 1;
|
|
571
|
+
nextSelection.anchor.set(openNode.__key, openTagStartIndex, 'text');
|
|
572
|
+
nextSelection.focus.set(closeNode.__key, newOffset, 'text'); // Apply formatting to selected text
|
|
1204
573
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
574
|
+
for (const format of matcher.format) {
|
|
575
|
+
if (!nextSelection.hasFormat(format)) {
|
|
576
|
+
nextSelection.formatText(format);
|
|
577
|
+
}
|
|
578
|
+
} // Collapse selection up to the focus point
|
|
1208
579
|
|
|
1209
|
-
const patternMatchResults = getPatternMatchResultsForCriteria(criteria, scanningContext, getParentElementNodeOrThrow(scanningContext));
|
|
1210
580
|
|
|
1211
|
-
|
|
1212
|
-
scanningContext.markdownCriteria = criteria;
|
|
1213
|
-
scanningContext.patternMatchResults = patternMatchResults; // Perform text transformation here.
|
|
581
|
+
nextSelection.anchor.set(nextSelection.focus.key, nextSelection.focus.offset, nextSelection.focus.type); // Remove formatting from collapsed selection
|
|
1214
582
|
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
583
|
+
for (const format of matcher.format) {
|
|
584
|
+
if (nextSelection.hasFormat(format)) {
|
|
585
|
+
nextSelection.toggleFormat(format);
|
|
1218
586
|
}
|
|
1219
587
|
}
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
588
|
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
if (lexical.$isTextNode(firstChild)) {
|
|
1227
|
-
// This function will convert all text nodes within the elementNode.
|
|
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.
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
1232
591
|
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
1233
594
|
|
|
1234
|
-
|
|
1235
|
-
const
|
|
595
|
+
function getOpenTagStartIndex(string, maxIndex, tag) {
|
|
596
|
+
const tagLength = tag.length;
|
|
1236
597
|
|
|
1237
|
-
for (let i =
|
|
1238
|
-
const
|
|
598
|
+
for (let i = maxIndex; i >= tagLength; i--) {
|
|
599
|
+
const startIndex = i - tagLength;
|
|
1239
600
|
|
|
1240
|
-
if (
|
|
1241
|
-
|
|
1242
|
-
|
|
601
|
+
if (isEqualSubString(string, startIndex, tag, 0, tagLength) && // Space after opening tag cancels transformation
|
|
602
|
+
string[startIndex + tagLength] !== ' ') {
|
|
603
|
+
return startIndex;
|
|
1243
604
|
}
|
|
1244
605
|
}
|
|
1245
|
-
}
|
|
1246
606
|
|
|
1247
|
-
|
|
1248
|
-
|
|
607
|
+
return -1;
|
|
608
|
+
}
|
|
1249
609
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
610
|
+
function isEqualSubString(stringA, aStart, stringB, bStart, length) {
|
|
611
|
+
for (let i = 0; i < length; i++) {
|
|
612
|
+
if (stringA[aStart + i] !== stringB[bStart + i]) {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
1255
616
|
|
|
1256
|
-
|
|
1257
|
-
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
1258
619
|
|
|
1259
|
-
|
|
1260
|
-
|
|
620
|
+
function registerMarkdownShortcuts(editor, transformers) {
|
|
621
|
+
const byType = transformersByType(transformers);
|
|
622
|
+
const textFormatTransformersIndex = indexBy(byType.textFormat, ({
|
|
623
|
+
tag
|
|
624
|
+
}) => tag[tag.length - 1]);
|
|
625
|
+
const textMatchTransformersIndex = indexBy(byType.textMatch, ({
|
|
626
|
+
trigger
|
|
627
|
+
}) => trigger);
|
|
1261
628
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
629
|
+
const transform = (parentNode, anchorNode, anchorOffset) => {
|
|
630
|
+
if (runElementTransformers(parentNode, anchorNode, anchorOffset, byType.element)) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
1265
633
|
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
}
|
|
634
|
+
if (runTextMatchTransformers(anchorNode, anchorOffset, textMatchTransformersIndex)) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
1270
637
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
offset: lastTextNode.getTextContent().length
|
|
1274
|
-
};
|
|
1275
|
-
}
|
|
638
|
+
runTextFormatTransformers(editor, anchorNode, anchorOffset, textFormatTransformersIndex);
|
|
639
|
+
};
|
|
1276
640
|
|
|
1277
|
-
|
|
641
|
+
return editor.registerUpdateListener(({
|
|
642
|
+
tags,
|
|
643
|
+
dirtyLeaves,
|
|
644
|
+
editorState,
|
|
645
|
+
prevEditorState
|
|
646
|
+
}) => {
|
|
647
|
+
// Ignore updates from undo/redo (as changes already calculated)
|
|
648
|
+
if (tags.has('historic')) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
1278
651
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
scanningContext.patternMatchResults = patternMatchResults; // Perform text transformation here.
|
|
652
|
+
const selection = editorState.read(lexical.$getSelection);
|
|
653
|
+
const prevSelection = prevEditorState.read(lexical.$getSelection);
|
|
1282
654
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
655
|
+
if (!lexical.$isRangeSelection(prevSelection) || !lexical.$isRangeSelection(selection) || !selection.isCollapsed()) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
1286
658
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
return;
|
|
1290
|
-
}
|
|
659
|
+
const anchorKey = selection.anchor.key;
|
|
660
|
+
const anchorOffset = selection.anchor.offset;
|
|
1291
661
|
|
|
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.
|
|
662
|
+
const anchorNode = editorState._nodeMap.get(anchorKey);
|
|
1296
663
|
|
|
664
|
+
if (!lexical.$isTextNode(anchorNode) || !dirtyLeaves.has(anchorKey) || anchorOffset !== 1 && anchorOffset !== prevSelection.anchor.offset + 1) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
1297
667
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
668
|
+
editor.update(() => {
|
|
669
|
+
// Markdown is not available inside code
|
|
670
|
+
if (anchorNode.hasFormat('code')) {
|
|
671
|
+
return;
|
|
1302
672
|
}
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
673
|
|
|
1307
|
-
|
|
1308
|
-
const children = elementNode.getChildren();
|
|
1309
|
-
const countOfChildren = children.length;
|
|
674
|
+
const parentNode = anchorNode.getParent();
|
|
1310
675
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
676
|
+
if (parentNode === null || code.$isCodeNode(parentNode)) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
1316
679
|
|
|
1317
|
-
|
|
680
|
+
transform(parentNode, anchorNode, selection.anchor.offset);
|
|
681
|
+
});
|
|
682
|
+
});
|
|
1318
683
|
}
|
|
1319
684
|
|
|
1320
685
|
/**
|
|
@@ -1325,138 +690,208 @@ function getLastTextNodeInElementNode(elementNode) {
|
|
|
1325
690
|
*
|
|
1326
691
|
*
|
|
1327
692
|
*/
|
|
1328
|
-
function $convertToMarkdownString() {
|
|
1329
|
-
const output = [];
|
|
1330
|
-
const children = lexical.$getRoot().getChildren();
|
|
1331
693
|
|
|
1332
|
-
|
|
1333
|
-
|
|
694
|
+
const replaceWithBlock = createNode => {
|
|
695
|
+
return (parentNode, children, match) => {
|
|
696
|
+
const node = createNode(match);
|
|
697
|
+
node.append(...children);
|
|
698
|
+
parentNode.replace(node);
|
|
699
|
+
node.select(0, 0);
|
|
700
|
+
};
|
|
701
|
+
}; // Amount of spaces that define indentation level
|
|
702
|
+
// TODO: should be an option
|
|
1334
703
|
|
|
1335
|
-
if (result != null) {
|
|
1336
|
-
output.push(result);
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
704
|
|
|
1340
|
-
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
function exportTopLevelElementOrDecorator(node) {
|
|
1344
|
-
const blockTransformers = getAllMarkdownCriteriaForParagraphs();
|
|
705
|
+
const LIST_INDENT_SIZE = 4;
|
|
1345
706
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
707
|
+
const listReplace = listType => {
|
|
708
|
+
return (parentNode, children, match) => {
|
|
709
|
+
const previousNode = parentNode.getPreviousSibling();
|
|
710
|
+
const listItem = list.$createListItemNode(listType === 'check' ? match[3] === 'x' : undefined);
|
|
1349
711
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
712
|
+
if (list.$isListNode(previousNode) && previousNode.getListType() === listType) {
|
|
713
|
+
previousNode.append(listItem);
|
|
714
|
+
parentNode.remove();
|
|
715
|
+
} else {
|
|
716
|
+
const list$1 = list.$createListNode(listType, listType === 'number' ? Number(match[2]) : undefined);
|
|
717
|
+
list$1.append(listItem);
|
|
718
|
+
parentNode.replace(list$1);
|
|
1353
719
|
}
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
return lexical.$isElementNode(node) ? exportChildren(node) : null;
|
|
1357
|
-
}
|
|
1358
720
|
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
721
|
+
listItem.append(...children);
|
|
722
|
+
listItem.select(0, 0);
|
|
723
|
+
const indent = Math.floor(match[1].length / LIST_INDENT_SIZE);
|
|
1362
724
|
|
|
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));
|
|
725
|
+
if (indent) {
|
|
726
|
+
listItem.setIndent(indent);
|
|
1382
727
|
}
|
|
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;
|
|
728
|
+
};
|
|
729
|
+
};
|
|
1399
730
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
731
|
+
const listExport = (listNode, exportChildren, depth) => {
|
|
732
|
+
const output = [];
|
|
733
|
+
const children = listNode.getChildren();
|
|
734
|
+
let index = 0;
|
|
1404
735
|
|
|
1405
|
-
|
|
736
|
+
for (const listItemNode of children) {
|
|
737
|
+
if (list.$isListItemNode(listItemNode)) {
|
|
738
|
+
if (listItemNode.getChildrenSize() === 1) {
|
|
739
|
+
const firstChild = listItemNode.getFirstChild();
|
|
1406
740
|
|
|
1407
|
-
|
|
1408
|
-
|
|
741
|
+
if (list.$isListNode(firstChild)) {
|
|
742
|
+
output.push(listExport(firstChild, exportChildren, depth + 1));
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
1409
745
|
}
|
|
1410
746
|
|
|
1411
|
-
const
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
747
|
+
const indent = ' '.repeat(depth * LIST_INDENT_SIZE);
|
|
748
|
+
const listType = listNode.getListType();
|
|
749
|
+
const prefix = listType === 'number' ? `${listNode.getStart() + index}. ` : listType === 'check' ? `- [${listItemNode.getChecked() ? 'x' : ' '}] ` : '- ';
|
|
750
|
+
output.push(indent + prefix + exportChildren(listItemNode));
|
|
751
|
+
index++;
|
|
1416
752
|
}
|
|
1417
753
|
}
|
|
1418
754
|
|
|
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();
|
|
755
|
+
return output.join('\n');
|
|
756
|
+
};
|
|
1428
757
|
|
|
1429
|
-
|
|
1430
|
-
|
|
758
|
+
const HEADING = {
|
|
759
|
+
export: (node, exportChildren) => {
|
|
760
|
+
if (!richText.$isHeadingNode(node)) {
|
|
761
|
+
return null;
|
|
1431
762
|
}
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
while (sibling) {
|
|
1435
|
-
if (lexical.$isElementNode(sibling)) {
|
|
1436
|
-
if (!sibling.isInline()) {
|
|
1437
|
-
break;
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
const descendant = backward ? sibling.getLastDescendant() : sibling.getFirstDescendant();
|
|
1441
763
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
764
|
+
const level = Number(node.getTag().slice(1));
|
|
765
|
+
return '#'.repeat(level) + ' ' + exportChildren(node);
|
|
766
|
+
},
|
|
767
|
+
regExp: /^(#{1,6})\s/,
|
|
768
|
+
replace: replaceWithBlock(match => {
|
|
769
|
+
// $FlowFixMe[incompatible-cast]
|
|
770
|
+
const tag = 'h' + match[1].length;
|
|
771
|
+
return richText.$createHeadingNode(tag);
|
|
772
|
+
}),
|
|
773
|
+
type: 'element'
|
|
774
|
+
};
|
|
775
|
+
const QUOTE = {
|
|
776
|
+
export: (node, exportChildren) => {
|
|
777
|
+
return richText.$isQuoteNode(node) ? '> ' + exportChildren(node) : null;
|
|
778
|
+
},
|
|
779
|
+
regExp: /^>\s/,
|
|
780
|
+
replace: replaceWithBlock(() => richText.$createQuoteNode()),
|
|
781
|
+
type: 'element'
|
|
782
|
+
};
|
|
783
|
+
const CODE = {
|
|
784
|
+
export: node => {
|
|
785
|
+
if (!code.$isCodeNode(node)) {
|
|
786
|
+
return null;
|
|
1447
787
|
}
|
|
1448
788
|
|
|
1449
|
-
|
|
1450
|
-
|
|
789
|
+
const textContent = node.getTextContent();
|
|
790
|
+
return '```' + (node.getLanguage() || '') + (textContent ? '\n' + textContent : '') + '\n' + '```';
|
|
791
|
+
},
|
|
792
|
+
regExp: /^```(\w{1,10})?\s/,
|
|
793
|
+
replace: replaceWithBlock(match => {
|
|
794
|
+
return code.$createCodeNode(match ? match[1] : undefined);
|
|
795
|
+
}),
|
|
796
|
+
type: 'element'
|
|
797
|
+
};
|
|
798
|
+
const UNORDERED_LIST = {
|
|
799
|
+
export: (node, exportChildren) => {
|
|
800
|
+
return list.$isListNode(node) ? listExport(node, exportChildren, 0) : null;
|
|
801
|
+
},
|
|
802
|
+
regExp: /^(\s*)[-*+]\s/,
|
|
803
|
+
replace: listReplace('bullet'),
|
|
804
|
+
type: 'element'
|
|
805
|
+
};
|
|
806
|
+
const CHECK_LIST = {
|
|
807
|
+
export: (node, exportChildren) => {
|
|
808
|
+
return list.$isListNode(node) ? listExport(node, exportChildren, 0) : null;
|
|
809
|
+
},
|
|
810
|
+
regExp: /^(\s*)(?:-\s)?\s?(\[(\s|x)?\])\s/i,
|
|
811
|
+
replace: listReplace('check'),
|
|
812
|
+
type: 'element'
|
|
813
|
+
};
|
|
814
|
+
const ORDERED_LIST = {
|
|
815
|
+
export: (node, exportChildren) => {
|
|
816
|
+
return list.$isListNode(node) ? listExport(node, exportChildren, 0) : null;
|
|
817
|
+
},
|
|
818
|
+
regExp: /^(\s*)(\d{1,})\.\s/,
|
|
819
|
+
replace: listReplace('number'),
|
|
820
|
+
type: 'element'
|
|
821
|
+
};
|
|
822
|
+
const INLINE_CODE = {
|
|
823
|
+
format: ['code'],
|
|
824
|
+
tag: '`',
|
|
825
|
+
type: 'text-format'
|
|
826
|
+
};
|
|
827
|
+
const BOLD_ITALIC_STAR = {
|
|
828
|
+
format: ['bold', 'italic'],
|
|
829
|
+
tag: '***',
|
|
830
|
+
type: 'text-format'
|
|
831
|
+
};
|
|
832
|
+
const BOLD_ITALIC_UNDERSCORE = {
|
|
833
|
+
format: ['bold', 'italic'],
|
|
834
|
+
tag: '___',
|
|
835
|
+
type: 'text-format'
|
|
836
|
+
};
|
|
837
|
+
const BOLD_STAR = {
|
|
838
|
+
format: ['bold'],
|
|
839
|
+
tag: '**',
|
|
840
|
+
type: 'text-format'
|
|
841
|
+
};
|
|
842
|
+
const BOLD_UNDERSCORE = {
|
|
843
|
+
format: ['bold'],
|
|
844
|
+
tag: '__',
|
|
845
|
+
type: 'text-format'
|
|
846
|
+
};
|
|
847
|
+
const STRIKETHROUGH = {
|
|
848
|
+
format: ['strikethrough'],
|
|
849
|
+
tag: '~~',
|
|
850
|
+
type: 'text-format'
|
|
851
|
+
};
|
|
852
|
+
const ITALIC_STAR = {
|
|
853
|
+
format: ['italic'],
|
|
854
|
+
tag: '*',
|
|
855
|
+
type: 'text-format'
|
|
856
|
+
};
|
|
857
|
+
const ITALIC_UNDERSCORE = {
|
|
858
|
+
format: ['italic'],
|
|
859
|
+
tag: '_',
|
|
860
|
+
type: 'text-format'
|
|
861
|
+
}; // Order of text transformers matters:
|
|
862
|
+
//
|
|
863
|
+
// - code should go first as it prevents any transformations inside
|
|
864
|
+
// - then longer tags match (e.g. ** or __ should go before * or _)
|
|
865
|
+
|
|
866
|
+
const LINK = {
|
|
867
|
+
export: (node, exportChildren, exportFormat) => {
|
|
868
|
+
if (!link.$isLinkNode(node)) {
|
|
869
|
+
return null;
|
|
1451
870
|
}
|
|
1452
|
-
}
|
|
1453
871
|
|
|
1454
|
-
|
|
1455
|
-
|
|
872
|
+
const linkContent = `[${node.getTextContent()}](${node.getURL()})`;
|
|
873
|
+
const firstChild = node.getFirstChild(); // Add text styles only if link has single text node inside. If it's more
|
|
874
|
+
// then one we ignore it as markdown does not support nested styles for links
|
|
1456
875
|
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
}
|
|
876
|
+
if (node.getChildrenSize() === 1 && lexical.$isTextNode(firstChild)) {
|
|
877
|
+
return exportFormat(firstChild, linkContent);
|
|
878
|
+
} else {
|
|
879
|
+
return linkContent;
|
|
880
|
+
}
|
|
881
|
+
},
|
|
882
|
+
importRegExp: /(?:\[([^[]+)\])(?:\(([^(]+)\))/,
|
|
883
|
+
regExp: /(?:\[([^[]+)\])(?:\(([^(]+)\))$/,
|
|
884
|
+
replace: (textNode, match) => {
|
|
885
|
+
const [, linkText, linkUrl] = match;
|
|
886
|
+
const linkNode = link.$createLinkNode(linkUrl);
|
|
887
|
+
const linkTextNode = lexical.$createTextNode(linkText);
|
|
888
|
+
linkTextNode.setFormat(textNode.getFormat());
|
|
889
|
+
linkNode.append(linkTextNode);
|
|
890
|
+
textNode.replace(linkNode);
|
|
891
|
+
},
|
|
892
|
+
trigger: ')',
|
|
893
|
+
type: 'text-match'
|
|
894
|
+
};
|
|
1460
895
|
|
|
1461
896
|
/**
|
|
1462
897
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
@@ -1466,36 +901,44 @@ function hasFormat(node, format) {
|
|
|
1466
901
|
*
|
|
1467
902
|
*
|
|
1468
903
|
*/
|
|
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);
|
|
904
|
+
const ELEMENT_TRANSFORMERS = [HEADING, QUOTE, CODE, UNORDERED_LIST, ORDERED_LIST]; // Order of text format transformers matters:
|
|
905
|
+
//
|
|
906
|
+
// - code should go first as it prevents any transformations inside
|
|
907
|
+
// - then longer tags match (e.g. ** or __ should go before * or _)
|
|
1482
908
|
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
909
|
+
const TEXT_FORMAT_TRANSFORMERS = [INLINE_CODE, BOLD_ITALIC_STAR, BOLD_ITALIC_UNDERSCORE, BOLD_STAR, BOLD_UNDERSCORE, ITALIC_STAR, ITALIC_UNDERSCORE, STRIKETHROUGH];
|
|
910
|
+
const TEXT_MATCH_TRANSFORMERS = [LINK];
|
|
911
|
+
const TRANSFORMERS = [...ELEMENT_TRANSFORMERS, ...TEXT_FORMAT_TRANSFORMERS, ...TEXT_MATCH_TRANSFORMERS];
|
|
1486
912
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
}
|
|
1491
|
-
});
|
|
913
|
+
function $convertFromMarkdownString(markdown, transformers = TRANSFORMERS) {
|
|
914
|
+
const importMarkdown = createMarkdownImport(transformers);
|
|
915
|
+
return importMarkdown(markdown);
|
|
1492
916
|
}
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
917
|
+
|
|
918
|
+
function $convertToMarkdownString(transformers = TRANSFORMERS) {
|
|
919
|
+
const exportMarkdown = createMarkdownExport(transformers);
|
|
920
|
+
return exportMarkdown();
|
|
1497
921
|
}
|
|
1498
922
|
|
|
1499
923
|
exports.$convertFromMarkdownString = $convertFromMarkdownString;
|
|
1500
924
|
exports.$convertToMarkdownString = $convertToMarkdownString;
|
|
925
|
+
exports.BOLD_ITALIC_STAR = BOLD_ITALIC_STAR;
|
|
926
|
+
exports.BOLD_ITALIC_UNDERSCORE = BOLD_ITALIC_UNDERSCORE;
|
|
927
|
+
exports.BOLD_STAR = BOLD_STAR;
|
|
928
|
+
exports.BOLD_UNDERSCORE = BOLD_UNDERSCORE;
|
|
929
|
+
exports.CHECK_LIST = CHECK_LIST;
|
|
930
|
+
exports.CODE = CODE;
|
|
931
|
+
exports.ELEMENT_TRANSFORMERS = ELEMENT_TRANSFORMERS;
|
|
932
|
+
exports.HEADING = HEADING;
|
|
933
|
+
exports.INLINE_CODE = INLINE_CODE;
|
|
934
|
+
exports.ITALIC_STAR = ITALIC_STAR;
|
|
935
|
+
exports.ITALIC_UNDERSCORE = ITALIC_UNDERSCORE;
|
|
936
|
+
exports.LINK = LINK;
|
|
937
|
+
exports.ORDERED_LIST = ORDERED_LIST;
|
|
938
|
+
exports.QUOTE = QUOTE;
|
|
939
|
+
exports.STRIKETHROUGH = STRIKETHROUGH;
|
|
940
|
+
exports.TEXT_FORMAT_TRANSFORMERS = TEXT_FORMAT_TRANSFORMERS;
|
|
941
|
+
exports.TEXT_MATCH_TRANSFORMERS = TEXT_MATCH_TRANSFORMERS;
|
|
942
|
+
exports.TRANSFORMERS = TRANSFORMERS;
|
|
943
|
+
exports.UNORDERED_LIST = UNORDERED_LIST;
|
|
1501
944
|
exports.registerMarkdownShortcuts = registerMarkdownShortcuts;
|