@lexical/markdown 0.44.1-nightly.20260519.0 → 0.45.1-dev.0
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.dev.js → dist/LexicalMarkdown.dev.js} +1 -1
- package/{LexicalMarkdown.dev.mjs → dist/LexicalMarkdown.dev.mjs} +1 -1
- package/dist/LexicalMarkdown.prod.js +9 -0
- package/dist/LexicalMarkdown.prod.mjs +9 -0
- package/package.json +37 -22
- package/src/MarkdownExport.ts +627 -0
- package/src/MarkdownImport.ts +363 -0
- package/src/MarkdownShortcuts.ts +677 -0
- package/src/MarkdownTransformers.ts +962 -0
- package/src/importTextFormatTransformer.ts +389 -0
- package/src/importTextMatchTransformer.ts +110 -0
- package/src/importTextTransformers.ts +141 -0
- package/src/index.ts +138 -0
- package/src/utils.ts +472 -0
- package/LexicalMarkdown.prod.js +0 -9
- package/LexicalMarkdown.prod.mjs +0 -9
- /package/{LexicalMarkdown.js → dist/LexicalMarkdown.js} +0 -0
- /package/{LexicalMarkdown.js.flow → dist/LexicalMarkdown.js.flow} +0 -0
- /package/{LexicalMarkdown.mjs → dist/LexicalMarkdown.mjs} +0 -0
- /package/{LexicalMarkdown.node.mjs → dist/LexicalMarkdown.node.mjs} +0 -0
- /package/{MarkdownExport.d.ts → dist/MarkdownExport.d.ts} +0 -0
- /package/{MarkdownImport.d.ts → dist/MarkdownImport.d.ts} +0 -0
- /package/{MarkdownShortcuts.d.ts → dist/MarkdownShortcuts.d.ts} +0 -0
- /package/{MarkdownTransformers.d.ts → dist/MarkdownTransformers.d.ts} +0 -0
- /package/{importTextFormatTransformer.d.ts → dist/importTextFormatTransformer.d.ts} +0 -0
- /package/{importTextMatchTransformer.d.ts → dist/importTextMatchTransformer.d.ts} +0 -0
- /package/{importTextTransformers.d.ts → dist/importTextTransformers.d.ts} +0 -0
- /package/{index.d.ts → dist/index.d.ts} +0 -0
- /package/{utils.d.ts → dist/utils.d.ts} +0 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {TextFormatTransformersIndex} from './MarkdownImport';
|
|
10
|
+
import type {TextFormatTransformer} from './MarkdownTransformers';
|
|
11
|
+
import type {TextNode} from 'lexical';
|
|
12
|
+
|
|
13
|
+
import {PUNCTUATION, WHITESPACE} from './utils';
|
|
14
|
+
|
|
15
|
+
interface Delimiter {
|
|
16
|
+
index: number;
|
|
17
|
+
char: string;
|
|
18
|
+
length: number;
|
|
19
|
+
originalLength: number;
|
|
20
|
+
canOpen: boolean;
|
|
21
|
+
canClose: boolean;
|
|
22
|
+
active: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function findOutermostTextFormatTransformer(
|
|
26
|
+
textNode: TextNode,
|
|
27
|
+
textFormatTransformersIndex: TextFormatTransformersIndex,
|
|
28
|
+
): {
|
|
29
|
+
startIndex: number;
|
|
30
|
+
endIndex: number;
|
|
31
|
+
transformer: TextFormatTransformer;
|
|
32
|
+
match: RegExpMatchArray;
|
|
33
|
+
} | null {
|
|
34
|
+
const textContent = textNode.getTextContent();
|
|
35
|
+
|
|
36
|
+
// Find code span first. Emphasis delimiters inside inline elements (e.g., code spans)
|
|
37
|
+
// should not be processed. Currently only code spans are handled; other inline elements
|
|
38
|
+
// (e.g., links, raw HTML) may need similar treatment in the future.
|
|
39
|
+
const codeRegex = textFormatTransformersIndex.fullMatchRegExpByTag['`'];
|
|
40
|
+
const codeTransformer = textFormatTransformersIndex.transformersByTag['`'];
|
|
41
|
+
|
|
42
|
+
const excludeRanges: Array<{start: number; end: number}> = [];
|
|
43
|
+
let codeMatch = null;
|
|
44
|
+
if (codeRegex && codeTransformer) {
|
|
45
|
+
const globalRegex = new RegExp(codeRegex.source, 'g');
|
|
46
|
+
const matches = Array.from(textContent.matchAll(globalRegex));
|
|
47
|
+
|
|
48
|
+
for (const match of matches) {
|
|
49
|
+
// Group 1 captures the character preceding the opening backtick (or an
|
|
50
|
+
// empty string when the span starts at position 0). Offset past it so
|
|
51
|
+
// startIndex points to the backtick itself.
|
|
52
|
+
const startIndex = match.index! + match[1].length;
|
|
53
|
+
const endIndex = match.index! + match[0].length;
|
|
54
|
+
|
|
55
|
+
if (!codeMatch) {
|
|
56
|
+
codeMatch = {
|
|
57
|
+
content: match[3],
|
|
58
|
+
endIndex,
|
|
59
|
+
startIndex,
|
|
60
|
+
tag: '`',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
excludeRanges.push({
|
|
65
|
+
end: endIndex,
|
|
66
|
+
start: startIndex,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const delimiters = scanDelimiters(
|
|
72
|
+
textContent,
|
|
73
|
+
textFormatTransformersIndex,
|
|
74
|
+
excludeRanges,
|
|
75
|
+
);
|
|
76
|
+
const emphasisMatch =
|
|
77
|
+
delimiters.length > 0
|
|
78
|
+
? processEmphasis(textContent, delimiters, textFormatTransformersIndex)
|
|
79
|
+
: null;
|
|
80
|
+
|
|
81
|
+
let resultMatch = null;
|
|
82
|
+
let resultTransformer = null;
|
|
83
|
+
|
|
84
|
+
if (codeMatch && emphasisMatch) {
|
|
85
|
+
if (
|
|
86
|
+
emphasisMatch.startIndex <= codeMatch.startIndex &&
|
|
87
|
+
emphasisMatch.endIndex >= codeMatch.endIndex
|
|
88
|
+
) {
|
|
89
|
+
resultMatch = emphasisMatch;
|
|
90
|
+
resultTransformer =
|
|
91
|
+
textFormatTransformersIndex.transformersByTag[emphasisMatch.tag];
|
|
92
|
+
} else {
|
|
93
|
+
resultMatch = codeMatch;
|
|
94
|
+
resultTransformer = codeTransformer;
|
|
95
|
+
}
|
|
96
|
+
} else if (codeMatch) {
|
|
97
|
+
resultMatch = codeMatch;
|
|
98
|
+
resultTransformer = codeTransformer;
|
|
99
|
+
} else if (emphasisMatch) {
|
|
100
|
+
resultMatch = emphasisMatch;
|
|
101
|
+
resultTransformer =
|
|
102
|
+
textFormatTransformersIndex.transformersByTag[emphasisMatch.tag];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!resultMatch || !resultTransformer) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const regexMatch: RegExpMatchArray = [
|
|
110
|
+
textContent.slice(resultMatch.startIndex, resultMatch.endIndex),
|
|
111
|
+
resultMatch.tag,
|
|
112
|
+
resultMatch.content,
|
|
113
|
+
];
|
|
114
|
+
regexMatch.index = resultMatch.startIndex;
|
|
115
|
+
regexMatch.input = textContent;
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
endIndex: resultMatch.endIndex,
|
|
119
|
+
match: regexMatch,
|
|
120
|
+
startIndex: resultMatch.startIndex,
|
|
121
|
+
transformer: resultTransformer,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function scanDelimiters(
|
|
126
|
+
text: string,
|
|
127
|
+
transformersIndex: TextFormatTransformersIndex,
|
|
128
|
+
excludeRanges: Array<{start: number; end: number}> = [],
|
|
129
|
+
): Delimiter[] {
|
|
130
|
+
const delimiters: Delimiter[] = [];
|
|
131
|
+
const delimiterChars = new Set(
|
|
132
|
+
Object.keys(transformersIndex.transformersByTag)
|
|
133
|
+
.filter(tag => tag[0] !== '`')
|
|
134
|
+
.map(tag => tag[0]),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const isEscaped = (index: number): boolean => {
|
|
138
|
+
let count = 0;
|
|
139
|
+
for (let i = index - 1; i >= 0 && text[i] === '\\'; i--) {
|
|
140
|
+
count++;
|
|
141
|
+
}
|
|
142
|
+
return count % 2 === 1;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const isInExcludedRange = (index: number): boolean => {
|
|
146
|
+
return excludeRanges.some(
|
|
147
|
+
range => index >= range.start && index < range.end,
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
let i = 0;
|
|
152
|
+
while (i < text.length) {
|
|
153
|
+
const char = text[i];
|
|
154
|
+
|
|
155
|
+
if (!delimiterChars.has(char) || isEscaped(i) || isInExcludedRange(i)) {
|
|
156
|
+
i++;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let len = 1;
|
|
161
|
+
while (i + len < text.length && text[i + len] === char) {
|
|
162
|
+
len++;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const canOpen = canEmphasis(char, text, i, len, true);
|
|
166
|
+
const canClose = canEmphasis(char, text, i, len, false);
|
|
167
|
+
|
|
168
|
+
if (canOpen || canClose) {
|
|
169
|
+
delimiters.push({
|
|
170
|
+
active: true,
|
|
171
|
+
canClose,
|
|
172
|
+
canOpen,
|
|
173
|
+
char,
|
|
174
|
+
index: i,
|
|
175
|
+
length: len,
|
|
176
|
+
originalLength: len,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
i += len;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return delimiters;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function processEmphasis(
|
|
187
|
+
text: string,
|
|
188
|
+
delimiters: Delimiter[],
|
|
189
|
+
transformersIndex: TextFormatTransformersIndex,
|
|
190
|
+
): {
|
|
191
|
+
startIndex: number;
|
|
192
|
+
endIndex: number;
|
|
193
|
+
tag: string;
|
|
194
|
+
content: string;
|
|
195
|
+
} | null {
|
|
196
|
+
const openersBottom: Record<string, number> = {};
|
|
197
|
+
let currentPos = 0;
|
|
198
|
+
let result = null;
|
|
199
|
+
|
|
200
|
+
while (currentPos < delimiters.length) {
|
|
201
|
+
const closer = delimiters[currentPos];
|
|
202
|
+
|
|
203
|
+
if (!closer.active || !closer.canClose || closer.length === 0) {
|
|
204
|
+
currentPos++;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const bottomKey = `${closer.char}${closer.canOpen}`;
|
|
209
|
+
const bottom = openersBottom[bottomKey] ?? -1;
|
|
210
|
+
let foundOpener = false;
|
|
211
|
+
|
|
212
|
+
for (let openIdx = currentPos - 1; openIdx > bottom; openIdx--) {
|
|
213
|
+
const opener = delimiters[openIdx];
|
|
214
|
+
|
|
215
|
+
if (
|
|
216
|
+
!opener.active ||
|
|
217
|
+
!opener.canOpen ||
|
|
218
|
+
opener.length === 0 ||
|
|
219
|
+
opener.char !== closer.char
|
|
220
|
+
) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Rule of 3
|
|
225
|
+
if (opener.canClose || closer.canOpen) {
|
|
226
|
+
const sum = opener.originalLength + closer.originalLength;
|
|
227
|
+
if (
|
|
228
|
+
sum % 3 === 0 &&
|
|
229
|
+
opener.originalLength % 3 !== 0 &&
|
|
230
|
+
closer.originalLength % 3 !== 0
|
|
231
|
+
) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const maxLen = Math.min(opener.length, closer.length);
|
|
237
|
+
const matchedTag = Object.keys(transformersIndex.transformersByTag)
|
|
238
|
+
.filter(t => t[0] === opener.char && t.length <= maxLen)
|
|
239
|
+
.sort((a, b) => b.length - a.length)[0];
|
|
240
|
+
|
|
241
|
+
if (!matchedTag) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
foundOpener = true;
|
|
246
|
+
const matchLen = matchedTag.length;
|
|
247
|
+
const match = {
|
|
248
|
+
content: text.slice(opener.index + opener.length, closer.index),
|
|
249
|
+
endIndex: closer.index + matchLen,
|
|
250
|
+
startIndex: opener.index + (opener.length - matchLen),
|
|
251
|
+
tag: matchedTag,
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
if (
|
|
255
|
+
!result ||
|
|
256
|
+
match.startIndex < result.startIndex ||
|
|
257
|
+
(match.startIndex === result.startIndex &&
|
|
258
|
+
match.endIndex > result.endIndex)
|
|
259
|
+
) {
|
|
260
|
+
result = match;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (let j = openIdx + 1; j < currentPos; j++) {
|
|
264
|
+
delimiters[j].active = false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
opener.length -= matchLen;
|
|
268
|
+
closer.length -= matchLen;
|
|
269
|
+
opener.active = opener.length > 0;
|
|
270
|
+
|
|
271
|
+
if (closer.length > 0) {
|
|
272
|
+
closer.index += matchLen;
|
|
273
|
+
} else {
|
|
274
|
+
closer.active = false;
|
|
275
|
+
currentPos++;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!foundOpener) {
|
|
282
|
+
openersBottom[bottomKey] = currentPos - 1;
|
|
283
|
+
if (!closer.canOpen) {
|
|
284
|
+
closer.active = false;
|
|
285
|
+
}
|
|
286
|
+
currentPos++;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function canEmphasis(
|
|
294
|
+
char: string,
|
|
295
|
+
text: string,
|
|
296
|
+
index: number,
|
|
297
|
+
length: number,
|
|
298
|
+
isOpen: boolean,
|
|
299
|
+
): boolean {
|
|
300
|
+
if (!isFlanking(text, index, length, isOpen)) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
if (char === '*') {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
if (char === '_') {
|
|
307
|
+
if (!isFlanking(text, index, length, !isOpen)) {
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
const adjacentChar = isOpen ? text[index - 1] : text[index + length];
|
|
311
|
+
return adjacentChar !== undefined && PUNCTUATION.test(adjacentChar);
|
|
312
|
+
}
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function isFlanking(
|
|
317
|
+
text: string,
|
|
318
|
+
index: number,
|
|
319
|
+
length: number,
|
|
320
|
+
isLeft: boolean,
|
|
321
|
+
): boolean {
|
|
322
|
+
const charBefore = text[index - 1];
|
|
323
|
+
const charAfter = text[index + length];
|
|
324
|
+
|
|
325
|
+
const [primary, secondary] = isLeft
|
|
326
|
+
? [charAfter, charBefore]
|
|
327
|
+
: [charBefore, charAfter];
|
|
328
|
+
|
|
329
|
+
if (primary === undefined || WHITESPACE.test(primary)) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
if (!PUNCTUATION.test(primary)) {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
return (
|
|
336
|
+
secondary === undefined ||
|
|
337
|
+
WHITESPACE.test(secondary) ||
|
|
338
|
+
PUNCTUATION.test(secondary)
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function importTextFormatTransformer(
|
|
343
|
+
textNode: TextNode,
|
|
344
|
+
startIndex: number,
|
|
345
|
+
endIndex: number,
|
|
346
|
+
transformer: TextFormatTransformer,
|
|
347
|
+
match: RegExpMatchArray,
|
|
348
|
+
): {
|
|
349
|
+
transformedNode: TextNode;
|
|
350
|
+
nodeBefore: TextNode | undefined; // If split
|
|
351
|
+
nodeAfter: TextNode | undefined; // If split
|
|
352
|
+
} {
|
|
353
|
+
const textContent = textNode.getTextContent();
|
|
354
|
+
|
|
355
|
+
// No text matches - we can safely process the text format match
|
|
356
|
+
let transformedNode, nodeAfter, nodeBefore;
|
|
357
|
+
|
|
358
|
+
// If matching full content there's no need to run splitText and can reuse existing textNode
|
|
359
|
+
// to update its content and apply format. E.g. for **_Hello_** string after applying bold
|
|
360
|
+
// format (**) it will reuse the same text node to apply italic (_)
|
|
361
|
+
if (match[0] === textContent) {
|
|
362
|
+
transformedNode = textNode;
|
|
363
|
+
} else {
|
|
364
|
+
if (startIndex === 0) {
|
|
365
|
+
[transformedNode, nodeAfter] = textNode.splitText(endIndex);
|
|
366
|
+
} else {
|
|
367
|
+
[nodeBefore, transformedNode, nodeAfter] = textNode.splitText(
|
|
368
|
+
startIndex,
|
|
369
|
+
endIndex,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
transformedNode.setTextContent(match[2]);
|
|
375
|
+
|
|
376
|
+
if (transformer) {
|
|
377
|
+
for (const format of transformer.format) {
|
|
378
|
+
if (!transformedNode.hasFormat(format)) {
|
|
379
|
+
transformedNode.toggleFormat(format);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
nodeAfter: nodeAfter,
|
|
386
|
+
nodeBefore: nodeBefore,
|
|
387
|
+
transformedNode: transformedNode,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import type {TextMatchTransformer} from './MarkdownTransformers';
|
|
9
|
+
|
|
10
|
+
import {type TextNode} from 'lexical';
|
|
11
|
+
|
|
12
|
+
export function findOutermostTextMatchTransformer(
|
|
13
|
+
textNode_: TextNode,
|
|
14
|
+
textMatchTransformers: Array<TextMatchTransformer>,
|
|
15
|
+
): {
|
|
16
|
+
startIndex: number;
|
|
17
|
+
endIndex: number;
|
|
18
|
+
transformer: TextMatchTransformer;
|
|
19
|
+
match: RegExpMatchArray;
|
|
20
|
+
} | null {
|
|
21
|
+
const textNode = textNode_;
|
|
22
|
+
|
|
23
|
+
let foundMatchStartIndex: number | undefined = undefined;
|
|
24
|
+
let foundMatchEndIndex: number | undefined = undefined;
|
|
25
|
+
let foundMatchTransformer: TextMatchTransformer | undefined = undefined;
|
|
26
|
+
let foundMatch: RegExpMatchArray | undefined = undefined;
|
|
27
|
+
|
|
28
|
+
for (const transformer of textMatchTransformers) {
|
|
29
|
+
if (!transformer.replace || !transformer.importRegExp) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const match = textNode.getTextContent().match(transformer.importRegExp);
|
|
33
|
+
|
|
34
|
+
if (!match) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const startIndex = match.index || 0;
|
|
39
|
+
const endIndex = transformer.getEndIndex
|
|
40
|
+
? transformer.getEndIndex(textNode, match)
|
|
41
|
+
: startIndex + match[0].length;
|
|
42
|
+
|
|
43
|
+
if (endIndex === false) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
foundMatchStartIndex === undefined ||
|
|
49
|
+
foundMatchEndIndex === undefined ||
|
|
50
|
+
// Wraps previous match or is strictly before it.
|
|
51
|
+
(startIndex < foundMatchStartIndex &&
|
|
52
|
+
(endIndex > foundMatchEndIndex || endIndex <= foundMatchStartIndex))
|
|
53
|
+
) {
|
|
54
|
+
foundMatchStartIndex = startIndex;
|
|
55
|
+
foundMatchEndIndex = endIndex;
|
|
56
|
+
foundMatchTransformer = transformer;
|
|
57
|
+
foundMatch = match;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
foundMatchStartIndex === undefined ||
|
|
63
|
+
foundMatchEndIndex === undefined ||
|
|
64
|
+
foundMatchTransformer === undefined ||
|
|
65
|
+
foundMatch === undefined
|
|
66
|
+
) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
endIndex: foundMatchEndIndex,
|
|
72
|
+
match: foundMatch,
|
|
73
|
+
startIndex: foundMatchStartIndex,
|
|
74
|
+
transformer: foundMatchTransformer,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function importFoundTextMatchTransformer(
|
|
79
|
+
textNode: TextNode,
|
|
80
|
+
startIndex: number,
|
|
81
|
+
endIndex: number,
|
|
82
|
+
transformer: TextMatchTransformer,
|
|
83
|
+
match: RegExpMatchArray,
|
|
84
|
+
): {
|
|
85
|
+
transformedNode?: TextNode;
|
|
86
|
+
nodeBefore: TextNode | undefined; // If split
|
|
87
|
+
nodeAfter: TextNode | undefined; // If split
|
|
88
|
+
} | null {
|
|
89
|
+
let transformedNode, nodeAfter, nodeBefore;
|
|
90
|
+
|
|
91
|
+
if (startIndex === 0) {
|
|
92
|
+
[transformedNode, nodeAfter] = textNode.splitText(endIndex);
|
|
93
|
+
} else {
|
|
94
|
+
[nodeBefore, transformedNode, nodeAfter] = textNode.splitText(
|
|
95
|
+
startIndex,
|
|
96
|
+
endIndex,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!transformer.replace) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const potentialTransformedNode = transformer.replace(transformedNode, match);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
nodeAfter,
|
|
107
|
+
nodeBefore,
|
|
108
|
+
transformedNode: potentialTransformedNode || undefined,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import type {TextFormatTransformersIndex} from './MarkdownImport';
|
|
9
|
+
import type {TextMatchTransformer} from './MarkdownTransformers';
|
|
10
|
+
|
|
11
|
+
import {$isTextNode, type LexicalNode, type TextNode} from 'lexical';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
findOutermostTextFormatTransformer,
|
|
15
|
+
importTextFormatTransformer,
|
|
16
|
+
} from './importTextFormatTransformer';
|
|
17
|
+
import {
|
|
18
|
+
findOutermostTextMatchTransformer,
|
|
19
|
+
importFoundTextMatchTransformer,
|
|
20
|
+
} from './importTextMatchTransformer';
|
|
21
|
+
import {unescapeText} from './utils';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns true if the node can contain transformable markdown.
|
|
25
|
+
* Code nodes cannot contain transformable markdown.
|
|
26
|
+
* For example, `code **bold**` should not be transformed to
|
|
27
|
+
* <code>code <strong>bold</strong></code>.
|
|
28
|
+
*/
|
|
29
|
+
export function canContainTransformableMarkdown(
|
|
30
|
+
node: LexicalNode | undefined,
|
|
31
|
+
): node is TextNode {
|
|
32
|
+
return $isTextNode(node) && !node.hasFormat('code');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Handles applying both text format and text match transformers.
|
|
37
|
+
* It finds the outermost text format or text match and applies it,
|
|
38
|
+
* then recursively calls itself to apply the next outermost transformer,
|
|
39
|
+
* until there are no more transformers to apply.
|
|
40
|
+
*/
|
|
41
|
+
export function importTextTransformers(
|
|
42
|
+
textNode: TextNode,
|
|
43
|
+
textFormatTransformersIndex: TextFormatTransformersIndex,
|
|
44
|
+
textMatchTransformers: Array<TextMatchTransformer>,
|
|
45
|
+
) {
|
|
46
|
+
let foundTextFormat = findOutermostTextFormatTransformer(
|
|
47
|
+
textNode,
|
|
48
|
+
textFormatTransformersIndex,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
let foundTextMatch = findOutermostTextMatchTransformer(
|
|
52
|
+
textNode,
|
|
53
|
+
textMatchTransformers,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (foundTextFormat && foundTextMatch) {
|
|
57
|
+
// Find the outermost transformer
|
|
58
|
+
if (
|
|
59
|
+
(foundTextFormat.startIndex <= foundTextMatch.startIndex &&
|
|
60
|
+
foundTextFormat.endIndex >= foundTextMatch.endIndex) ||
|
|
61
|
+
// foundTextMatch is not contained within foundTextFormat
|
|
62
|
+
foundTextMatch.startIndex > foundTextFormat.endIndex
|
|
63
|
+
) {
|
|
64
|
+
// foundTextFormat wraps foundTextMatch - apply foundTextFormat by setting foundTextMatch to null
|
|
65
|
+
foundTextMatch = null;
|
|
66
|
+
} else {
|
|
67
|
+
// foundTextMatch wraps foundTextFormat - apply foundTextMatch by setting foundTextFormat to null
|
|
68
|
+
foundTextFormat = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (foundTextFormat) {
|
|
73
|
+
const result = importTextFormatTransformer(
|
|
74
|
+
textNode,
|
|
75
|
+
foundTextFormat.startIndex,
|
|
76
|
+
foundTextFormat.endIndex,
|
|
77
|
+
foundTextFormat.transformer,
|
|
78
|
+
foundTextFormat.match,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (canContainTransformableMarkdown(result.nodeAfter)) {
|
|
82
|
+
importTextTransformers(
|
|
83
|
+
result.nodeAfter,
|
|
84
|
+
textFormatTransformersIndex,
|
|
85
|
+
textMatchTransformers,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (canContainTransformableMarkdown(result.nodeBefore)) {
|
|
89
|
+
importTextTransformers(
|
|
90
|
+
result.nodeBefore,
|
|
91
|
+
textFormatTransformersIndex,
|
|
92
|
+
textMatchTransformers,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (canContainTransformableMarkdown(result.transformedNode)) {
|
|
96
|
+
importTextTransformers(
|
|
97
|
+
result.transformedNode,
|
|
98
|
+
textFormatTransformersIndex,
|
|
99
|
+
textMatchTransformers,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
} else if (foundTextMatch) {
|
|
103
|
+
const result = importFoundTextMatchTransformer(
|
|
104
|
+
textNode,
|
|
105
|
+
foundTextMatch.startIndex,
|
|
106
|
+
foundTextMatch.endIndex,
|
|
107
|
+
foundTextMatch.transformer,
|
|
108
|
+
foundTextMatch.match,
|
|
109
|
+
);
|
|
110
|
+
if (!result) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (canContainTransformableMarkdown(result.nodeAfter)) {
|
|
115
|
+
importTextTransformers(
|
|
116
|
+
result.nodeAfter,
|
|
117
|
+
textFormatTransformersIndex,
|
|
118
|
+
textMatchTransformers,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (canContainTransformableMarkdown(result.nodeBefore)) {
|
|
122
|
+
importTextTransformers(
|
|
123
|
+
result.nodeBefore,
|
|
124
|
+
textFormatTransformersIndex,
|
|
125
|
+
textMatchTransformers,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (canContainTransformableMarkdown(result.transformedNode)) {
|
|
129
|
+
importTextTransformers(
|
|
130
|
+
result.transformedNode,
|
|
131
|
+
textFormatTransformersIndex,
|
|
132
|
+
textMatchTransformers,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Handle escape characters
|
|
138
|
+
const textContent = textNode.getTextContent();
|
|
139
|
+
const unescapedText = unescapeText(textContent);
|
|
140
|
+
textNode.setTextContent(unescapedText);
|
|
141
|
+
}
|