onchain-lexical-markdown 0.0.1

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.
@@ -0,0 +1,891 @@
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 {CodeNode} from '@lexical/code';
10
+ import {createHeadlessEditor} from '@lexical/headless';
11
+ import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';
12
+ import {$createLinkNode, LinkNode} from '@lexical/link';
13
+ import {ListItemNode, ListNode} from '@lexical/list';
14
+ import {HeadingNode, QuoteNode} from '@lexical/rich-text';
15
+ import {$createTextNode, $getRoot, $insertNodes} from 'lexical';
16
+ import {$createInstanceCodeNode} from 'onchain-lexical-instance';
17
+
18
+ import {
19
+ $convertFromMarkdownString,
20
+ $convertToMarkdownString,
21
+ LINK,
22
+ TextMatchTransformer,
23
+ Transformer,
24
+ TRANSFORMERS,
25
+ } from '../..';
26
+ import {
27
+ CODE,
28
+ MultilineElementTransformer,
29
+ normalizeMarkdown,
30
+ } from '../../MarkdownTransformers';
31
+
32
+ const SIMPLE_INLINE_JSX_MATCHER: TextMatchTransformer = {
33
+ dependencies: [LinkNode],
34
+ getEndIndex(node, match) {
35
+ // Find the closing tag. Count the number of opening and closing tags to find the correct closing tag.
36
+ // For simplicity, this will only count the opening and closing tags without checking for "MyTag" specifically.
37
+ let openedSubStartMatches = 0;
38
+ const start = (match.index ?? 0) + match[0].length;
39
+ let endIndex = start;
40
+ const line = node.getTextContent();
41
+
42
+ for (let i = start; i < line.length; i++) {
43
+ const char = line[i];
44
+ if (char === '<') {
45
+ const nextChar = line[i + 1];
46
+ if (nextChar === '/') {
47
+ if (openedSubStartMatches === 0) {
48
+ endIndex = i + '</MyTag>'.length;
49
+ break;
50
+ }
51
+ openedSubStartMatches--;
52
+ } else {
53
+ openedSubStartMatches++;
54
+ }
55
+ }
56
+ }
57
+ return endIndex;
58
+ },
59
+ importRegExp: /<(MyTag)\s*>/,
60
+ regExp: /__ignore__/,
61
+ replace: (textNode, match) => {
62
+ const linkNode = $createLinkNode('simple-jsx');
63
+
64
+ const textStart = match[0].length + (match.index ?? 0);
65
+ const textEnd =
66
+ (match.index ?? 0) + textNode.getTextContent().length - '</MyTag>'.length;
67
+ const text = match.input?.slice(textStart, textEnd);
68
+
69
+ const linkTextNode = $createTextNode(text);
70
+ linkTextNode.setFormat(textNode.getFormat());
71
+ linkNode.append(linkTextNode);
72
+ textNode.replace(linkNode);
73
+ },
74
+ type: 'text-match',
75
+ };
76
+
77
+ // Matches html within a mdx file
78
+ const MDX_HTML_TRANSFORMER: MultilineElementTransformer = {
79
+ dependencies: [CodeNode],
80
+ export: (node) => {
81
+ if (node.getTextContent().startsWith('From HTML:')) {
82
+ return `<MyComponent>${node
83
+ .getTextContent()
84
+ .replace('From HTML: ', '')}</MyComponent>`;
85
+ }
86
+ return null; // Run next transformer
87
+ },
88
+ regExpEnd: /<\/(\w+)\s*>/,
89
+ regExpStart: /<(\w+)[^>]*>/,
90
+ replace: (rootNode, children, startMatch, endMatch, linesInBetween) => {
91
+ if (!linesInBetween) {
92
+ return false; // Run next transformer. We don't need to support markdown shortcuts for this test
93
+ }
94
+ if (startMatch[1] === 'MyComponent') {
95
+ const codeBlockNode = $createInstanceCodeNode(startMatch[1]);
96
+ const textNode = $createTextNode(
97
+ 'From HTML: ' + linesInBetween.join('\n'),
98
+ );
99
+ codeBlockNode.append(textNode);
100
+ rootNode.append(codeBlockNode);
101
+ return;
102
+ }
103
+ return false; // Run next transformer
104
+ },
105
+ type: 'multiline-element',
106
+ };
107
+
108
+ const CODE_TAG_COUNTER_EXAMPLE: MultilineElementTransformer = {
109
+ dependencies: CODE.dependencies,
110
+ export: CODE.export,
111
+ handleImportAfterStartMatch({lines, rootNode, startLineIndex, startMatch}) {
112
+ const regexpEndRegex: RegExp | undefined = /[ \t]*```$/;
113
+
114
+ const isEndOptional = false;
115
+
116
+ let endLineIndex = startLineIndex;
117
+ const linesLength = lines.length;
118
+
119
+ let openedSubStartMatches = 0;
120
+
121
+ // check every single line for the closing match. It could also be on the same line as the opening match.
122
+ while (endLineIndex < linesLength) {
123
+ const potentialSubStartMatch =
124
+ lines[endLineIndex].match(/^[ \t]*```(\w+)?/);
125
+
126
+ const endMatch = regexpEndRegex
127
+ ? lines[endLineIndex].match(regexpEndRegex)
128
+ : null;
129
+
130
+ if (potentialSubStartMatch) {
131
+ if (endMatch) {
132
+ if ((potentialSubStartMatch.index ?? 0) < (endMatch.index ?? 0)) {
133
+ openedSubStartMatches++;
134
+ }
135
+ } else {
136
+ openedSubStartMatches++;
137
+ }
138
+ }
139
+
140
+ if (endMatch) {
141
+ openedSubStartMatches--;
142
+ }
143
+
144
+ if (!endMatch || openedSubStartMatches > 0) {
145
+ if (
146
+ !isEndOptional ||
147
+ (isEndOptional && endLineIndex < linesLength - 1) // Optional end, but didn't reach the end of the document yet => continue searching for potential closing match
148
+ ) {
149
+ endLineIndex++;
150
+ continue; // Search next line for closing match
151
+ }
152
+ }
153
+
154
+ // Now, check if the closing match matched is the same as the opening match.
155
+ // If it is, we need to continue searching for the actual closing match.
156
+ if (
157
+ endMatch &&
158
+ startLineIndex === endLineIndex &&
159
+ endMatch.index === startMatch.index
160
+ ) {
161
+ endLineIndex++;
162
+ continue; // Search next line for closing match
163
+ }
164
+
165
+ // At this point, we have found the closing match. Next: calculate the lines in between open and closing match
166
+ // This should not include the matches themselves, and be split up by lines
167
+ const linesInBetween: string[] = [];
168
+
169
+ if (endMatch && startLineIndex === endLineIndex) {
170
+ linesInBetween.push(
171
+ lines[startLineIndex].slice(
172
+ startMatch[0].length,
173
+ -endMatch[0].length,
174
+ ),
175
+ );
176
+ } else {
177
+ for (let i = startLineIndex; i <= endLineIndex; i++) {
178
+ if (i === startLineIndex) {
179
+ const text = lines[i].slice(startMatch[0].length);
180
+ linesInBetween.push(text); // Also include empty text
181
+ } else if (i === endLineIndex && endMatch) {
182
+ const text = lines[i].slice(0, -endMatch[0].length);
183
+ linesInBetween.push(text); // Also include empty text
184
+ } else {
185
+ linesInBetween.push(lines[i]);
186
+ }
187
+ }
188
+ }
189
+
190
+ if (
191
+ CODE.replace(
192
+ rootNode,
193
+ null,
194
+ startMatch,
195
+ endMatch,
196
+ linesInBetween,
197
+ true,
198
+ ) !== false
199
+ ) {
200
+ // Return here. This $importMultiline function is run line by line and should only process a single multiline element at a time.
201
+ return [true, endLineIndex];
202
+ }
203
+
204
+ // The replace function returned false, despite finding the matching open and close tags => this transformer does not want to handle it.
205
+ // Thus, we continue letting the remaining transformers handle the passed lines of text from the beginning
206
+ break;
207
+ }
208
+
209
+ // No multiline transformer handled this line successfully
210
+ return [false, startLineIndex];
211
+ },
212
+ regExpStart: CODE.regExpStart,
213
+ replace: CODE.replace,
214
+ type: 'multiline-element',
215
+ };
216
+
217
+ describe('Markdown', () => {
218
+ type Input = Array<{
219
+ html: string;
220
+ md: string;
221
+ skipExport?: true;
222
+ skipImport?: true;
223
+ shouldPreserveNewLines?: true;
224
+ shouldMergeAdjacentLines?: true | false;
225
+ customTransformers?: Transformer[];
226
+ mdAfterExport?: string;
227
+ }>;
228
+
229
+ const URL = 'https://lexical.dev';
230
+
231
+ const IMPORT_AND_EXPORT: Input = [
232
+ {
233
+ html: '<h1><span style="white-space: pre-wrap;">Hello world</span></h1>',
234
+ md: '# Hello world',
235
+ },
236
+ {
237
+ html: '<h2><span style="white-space: pre-wrap;">Hello world</span></h2>',
238
+ md: '## Hello world',
239
+ },
240
+ {
241
+ html: '<h3><span style="white-space: pre-wrap;">Hello world</span></h3>',
242
+ md: '### Hello world',
243
+ },
244
+ {
245
+ html: '<h4><span style="white-space: pre-wrap;">Hello world</span></h4>',
246
+ md: '#### Hello world',
247
+ },
248
+ {
249
+ html: '<h5><span style="white-space: pre-wrap;">Hello world</span></h5>',
250
+ md: '##### Hello world',
251
+ },
252
+ {
253
+ html: '<h6><span style="white-space: pre-wrap;">Hello world</span></h6>',
254
+ md: '###### Hello world',
255
+ },
256
+ {
257
+ // Multiline paragraphs: https://spec.commonmark.org/dingus/?text=Hello%0Aworld%0A!
258
+ html: '<p><span style="white-space: pre-wrap;">Helloworld!</span></p>',
259
+ md: ['Hello', 'world', '!'].join('\n'),
260
+ shouldMergeAdjacentLines: true,
261
+ skipExport: true,
262
+ },
263
+ {
264
+ // Multiline paragraphs
265
+ // TO-DO: It would be nice to support also hard line breaks (<br>) as \ or double spaces
266
+ // See https://spec.commonmark.org/0.31.2/#hard-line-breaks.
267
+ // Example: '<p><span style="white-space: pre-wrap;">Hello\\\nworld\\\n!</span></p>',
268
+ html: '<p><span style="white-space: pre-wrap;">Hello<br>world<br>!</span></p>',
269
+ md: ['Hello', 'world', '!'].join('\n'),
270
+ skipImport: true,
271
+ },
272
+ {
273
+ html: '<blockquote><span style="white-space: pre-wrap;">Hello</span><br><span style="white-space: pre-wrap;">world!</span></blockquote>',
274
+ md: '> Hello\n> world!',
275
+ },
276
+ // TO-DO: <br> should be preserved
277
+ // {
278
+ // html: '<ul><li value="1"><span style="white-space: pre-wrap;">Hello</span></li><li value="2"><span style="white-space: pre-wrap;">world<br>!<br>!</span></li></ul>',
279
+ // md: '- Hello\n- world<br>!<br>!',
280
+ // skipImport: true,
281
+ // },
282
+ {
283
+ // Multiline list items: https://spec.commonmark.org/dingus/?text=-%20Hello%0A-%20world%0A!%0A!
284
+ html: '<ul><li value="1"><span style="white-space: pre-wrap;">Hello</span></li><li value="2"><span style="white-space: pre-wrap;">world!!</span></li></ul>',
285
+ md: '- Hello\n- world\n!\n!',
286
+ shouldMergeAdjacentLines: true,
287
+ skipExport: true,
288
+ },
289
+ {
290
+ html: '<ul><li value="1"><span style="white-space: pre-wrap;">Hello</span></li><li value="2"><span style="white-space: pre-wrap;">world</span></li></ul>',
291
+ md: '- Hello\n- world',
292
+ },
293
+ {
294
+ html: '<ul><li value="1"><span style="white-space: pre-wrap;">Level 1</span></li><li value="2"><ul><li value="1"><span style="white-space: pre-wrap;">Level 2</span></li><li value="2"><ul><li value="1"><span style="white-space: pre-wrap;">Level 3</span></li></ul></li></ul></li></ul><p><span style="white-space: pre-wrap;">Hello world</span></p>',
295
+ md: '- Level 1\n - Level 2\n - Level 3\n\nHello world',
296
+ },
297
+ // List indentation with tabs, Import only: export will use " " only for one level of indentation
298
+ {
299
+ html: '<ul><li value="1"><span style="white-space: pre-wrap;">Level 1</span></li><li value="2"><ul><li value="1"><span style="white-space: pre-wrap;">Level 2</span></li><li value="2"><ul><li value="1"><span style="white-space: pre-wrap;">Level 3</span></li></ul></li></ul></li></ul><p><span style="white-space: pre-wrap;">Hello world</span></p>',
300
+ md: '- Level 1\n\t- Level 2\n \t - Level 3\n\nHello world',
301
+ skipExport: true,
302
+ },
303
+ {
304
+ // Import only: export will use "-" instead of "*"
305
+ html: '<ul><li value="1"><span style="white-space: pre-wrap;">Level 1</span></li><li value="2"><ul><li value="1"><span style="white-space: pre-wrap;">Level 2</span></li><li value="2"><ul><li value="1"><span style="white-space: pre-wrap;">Level 3</span></li></ul></li></ul></li></ul><p><span style="white-space: pre-wrap;">Hello world</span></p>',
306
+ md: '* Level 1\n * Level 2\n * Level 3\n\nHello world',
307
+ skipExport: true,
308
+ },
309
+ {
310
+ html: '<ol><li value="1"><span style="white-space: pre-wrap;">Hello</span></li><li value="2"><span style="white-space: pre-wrap;">world</span></li></ol>',
311
+ md: '1. Hello\n2. world',
312
+ },
313
+ {
314
+ html: '<ol start="25"><li value="25"><span style="white-space: pre-wrap;">Hello</span></li><li value="26"><span style="white-space: pre-wrap;">world</span></li></ol>',
315
+ md: '25. Hello\n26. world',
316
+ },
317
+ {
318
+ html: '<p><i><em style="white-space: pre-wrap;">Hello</em></i><span style="white-space: pre-wrap;"> world</span></p>',
319
+ md: '*Hello* world',
320
+ },
321
+ {
322
+ html: '<p><b><strong style="white-space: pre-wrap;">Hello</strong></b><span style="white-space: pre-wrap;"> world</span></p>',
323
+ md: '**Hello** world',
324
+ },
325
+ {
326
+ html: '<p><i><b><strong style="white-space: pre-wrap;">Hello</strong></b></i><span style="white-space: pre-wrap;"> world</span></p>',
327
+ md: '***Hello*** world',
328
+ },
329
+ {
330
+ html: '<p><code spellcheck="false" style="white-space: pre-wrap;"><span>Hello</span></code><span style="white-space: pre-wrap;"> world</span></p>',
331
+ md: '`Hello` world',
332
+ },
333
+ {
334
+ html: '<p><s><span style="white-space: pre-wrap;">Hello</span></s><span style="white-space: pre-wrap;"> world</span></p>',
335
+ md: '~~Hello~~ world',
336
+ },
337
+ {
338
+ html: '<p><code spellcheck="false" style="white-space: pre-wrap;"><span>hello$</span></code></p>',
339
+ md: '`hello$`',
340
+ },
341
+ {
342
+ html: '<p><code spellcheck="false" style="white-space: pre-wrap;"><span>$$hello</span></code></p>',
343
+ md: '`$$hello`',
344
+ },
345
+ {
346
+ html: '<p><a href="https://lexical.dev"><span style="white-space: pre-wrap;">Hello</span></a><span style="white-space: pre-wrap;"> world</span></p>',
347
+ md: '[Hello](https://lexical.dev) world',
348
+ },
349
+ {
350
+ html: '<p><a href="https://lexical.dev" title="Hello world"><span style="white-space: pre-wrap;">Hello</span></a><span style="white-space: pre-wrap;"> world</span></p>',
351
+ md: '[Hello](https://lexical.dev "Hello world") world',
352
+ },
353
+ {
354
+ html: '<p><a href="https://lexical.dev" title="Title with \\&quot; escaped character"><span style="white-space: pre-wrap;">Hello</span></a><span style="white-space: pre-wrap;"> world</span></p>',
355
+ md: '[Hello](https://lexical.dev "Title with \\" escaped character") world',
356
+ },
357
+ {
358
+ html: '<p><span style="white-space: pre-wrap;">Hello </span><s><i><b><strong style="white-space: pre-wrap;">world</strong></b></i></s><span style="white-space: pre-wrap;">!</span></p>',
359
+ md: 'Hello ***~~world~~***!',
360
+ },
361
+ {
362
+ html: '<p><b><strong style="white-space: pre-wrap;">Hello </strong></b><s><b><strong style="white-space: pre-wrap;">world</strong></b></s><span style="white-space: pre-wrap;">!</span></p>',
363
+ md: '**Hello ~~world~~**!',
364
+ mdAfterExport: '**Hello&#32;~~world~~**!',
365
+ },
366
+ {
367
+ html: '<p><s><b><strong style="white-space: pre-wrap;">Hello </strong></b></s><s><i><b><strong style="white-space: pre-wrap;">world</strong></b></i></s><s><span style="white-space: pre-wrap;">!</span></s></p>',
368
+ md: '**~~Hello *world*~~**~~!~~',
369
+ mdAfterExport: '**~~Hello&#32;*world*~~**~~!~~',
370
+ },
371
+ {
372
+ html: '<p><i><em style="white-space: pre-wrap;">Hello </em></i><i><b><strong style="white-space: pre-wrap;">world</strong></b></i><i><em style="white-space: pre-wrap;">!</em></i></p>',
373
+ md: '*Hello **world**!*',
374
+ mdAfterExport: '*Hello&#32;**world**!*',
375
+ },
376
+ {
377
+ html: '<p><span style="white-space: pre-wrap;">helloworld</span></p>',
378
+ md: 'hello\nworld',
379
+ shouldMergeAdjacentLines: true,
380
+ skipExport: true,
381
+ },
382
+ {
383
+ html: '<p><span style="white-space: pre-wrap;">hello</span><br><span style="white-space: pre-wrap;">world</span></p>',
384
+ md: 'hello\nworld',
385
+ shouldMergeAdjacentLines: false,
386
+ },
387
+ {
388
+ html: '<p><span style="white-space: pre-wrap;">hello</span></p><p><span style="white-space: pre-wrap;">world</span></p>',
389
+ md: 'hello\nworld',
390
+ shouldPreserveNewLines: true,
391
+ },
392
+ {
393
+ html: '<h1><span style="white-space: pre-wrap;">Hello</span></h1><p><br></p><p><br></p><p><br></p><p><b><strong style="white-space: pre-wrap;">world</strong></b><span style="white-space: pre-wrap;">!</span></p>',
394
+ md: '# Hello\n\n\n\n**world**!',
395
+ shouldPreserveNewLines: true,
396
+ },
397
+ {
398
+ html: '<h1><span style="white-space: pre-wrap;">Hello</span></h1><p><span style="white-space: pre-wrap;">hi</span></p><p><br></p><p><b><strong style="white-space: pre-wrap;">world</strong></b></p><p><br></p><p><span style="white-space: pre-wrap;">hi</span></p><blockquote><span style="white-space: pre-wrap;">hello</span><br><span style="white-space: pre-wrap;">hello</span></blockquote><p><br></p><h1><span style="white-space: pre-wrap;">hi</span></h1><p><br></p><p><span style="white-space: pre-wrap;">hi</span></p>',
399
+ md: '# Hello\nhi\n\n**world**\n\nhi\n> hello\n> hello\n\n# hi\n\nhi',
400
+ shouldPreserveNewLines: true,
401
+ },
402
+ {
403
+ // Import only: export will use * instead of _ due to registered transformers order
404
+ html: '<p><i><em style="white-space: pre-wrap;">Hello</em></i><span style="white-space: pre-wrap;"> world</span></p>',
405
+ md: '_Hello_ world',
406
+ skipExport: true,
407
+ },
408
+ {
409
+ // Import only: export will use * instead of _ due to registered transformers order
410
+ html: '<p><b><strong style="white-space: pre-wrap;">Hello</strong></b><span style="white-space: pre-wrap;"> world</span></p>',
411
+ md: '__Hello__ world',
412
+ skipExport: true,
413
+ },
414
+ {
415
+ // Import only: export will use * instead of _ due to registered transformers order
416
+ html: '<p><i><b><strong style="white-space: pre-wrap;">Hello</strong></b></i><span style="white-space: pre-wrap;"> world</span></p>',
417
+ md: '___Hello___ world',
418
+ skipExport: true,
419
+ },
420
+ {
421
+ // Import only: export will use * instead of _ due to registered transformers order
422
+ html: '<p><span style="white-space: pre-wrap;">Hello </span><s><i><b><strong style="white-space: pre-wrap;">world</strong></b></i></s><span style="white-space: pre-wrap;">!</span></p>',
423
+ md: 'Hello ~~__*world*__~~!',
424
+ skipExport: true,
425
+ },
426
+ {
427
+ html: '<pre spellcheck="false"><span style="white-space: pre-wrap;">Single line Code</span></pre>',
428
+ md: '```Single line Code```', // Ensure that "Single" is not read as the language by the code transformer. It should only be read as the language if there is a multi-line code block
429
+ skipExport: true, // Export will fail, as the code transformer will add new lines to the code block to make it multi-line. This is expected though, as the lexical code block is a block node and cannot be inline.
430
+ },
431
+ {
432
+ html: '<pre spellcheck="false" data-language="javascript" data-highlight-language="javascript"><span style="white-space: pre-wrap;">Incomplete tag</span></pre>',
433
+ md: '```javascript Incomplete tag',
434
+ skipExport: true,
435
+ },
436
+ {
437
+ html:
438
+ '<pre spellcheck="false" data-language="javascript" data-highlight-language="javascript"><span style="white-space: pre-wrap;">Incomplete multiline\n' +
439
+ '\n' +
440
+ 'Tag</span></pre>',
441
+ md: '```javascript Incomplete multiline\n\nTag',
442
+ skipExport: true,
443
+ },
444
+ {
445
+ html: '<pre spellcheck="false"><span style="white-space: pre-wrap;">Code</span></pre>',
446
+ md: '```\nCode\n```',
447
+ },
448
+ {
449
+ html: '<pre spellcheck="false" data-language="javascript" data-highlight-language="javascript"><span style="white-space: pre-wrap;">Code</span></pre>',
450
+ md: '```javascript\nCode\n```',
451
+ },
452
+ {
453
+ // Should always preserve language in md but keep data-highlight-language only for supported languages
454
+ html: '<pre spellcheck="false" data-language="unknown"><span style="white-space: pre-wrap;">Code</span></pre>',
455
+ md: '```unknown\nCode\n```',
456
+ },
457
+ {
458
+ // Import only: prefix tabs will be removed for export
459
+ html: '<pre spellcheck="false"><span style="white-space: pre-wrap;">Code</span></pre>',
460
+ md: '\t```\nCode\n```',
461
+ skipExport: true,
462
+ },
463
+ {
464
+ // Import only: prefix spaces will be removed for export
465
+ html: '<pre spellcheck="false"><span style="white-space: pre-wrap;">Code</span></pre>',
466
+ md: ' ```\nCode\n```',
467
+ skipExport: true,
468
+ },
469
+ {
470
+ html: `<h3><span style="white-space: pre-wrap;">Code blocks</span></h3><pre spellcheck="false" data-language="javascript" data-highlight-language="javascript"><span style="white-space: pre-wrap;">1 + 1 = 2;</span></pre>`,
471
+ md: `### Code blocks
472
+
473
+ \`\`\`javascript
474
+ 1 + 1 = 2;
475
+ \`\`\``,
476
+ },
477
+ {
478
+ // Import only: extra empty lines will be removed for export
479
+ html: '<p><span style="white-space: pre-wrap;">Hello</span></p><p><span style="white-space: pre-wrap;">world</span></p>',
480
+ md: ['Hello', '', '', '', 'world'].join('\n'),
481
+ skipExport: true,
482
+ },
483
+ {
484
+ // https://spec.commonmark.org/dingus/?text=%3E%20Hello%0Aworld%0A!
485
+ html: '<blockquote><span style="white-space: pre-wrap;">Helloworld!</span></blockquote>',
486
+ md: '> Hello\nworld\n!',
487
+ shouldMergeAdjacentLines: true,
488
+ skipExport: true,
489
+ },
490
+ {
491
+ // Import only: ensures that left side of splitText is processed for text match transformers
492
+ html: '<p><span style="white-space: pre-wrap;">Hello </span><a href="https://lexical.dev"><span style="white-space: pre-wrap;">world</span></a><span style="white-space: pre-wrap;">! Hello </span><mark style="white-space: pre-wrap;"><span>$world$</span></mark><span style="white-space: pre-wrap;">! </span><a href="https://lexical.dev"><span style="white-space: pre-wrap;">Hello</span></a><span style="white-space: pre-wrap;"> world! Hello </span><mark style="white-space: pre-wrap;"><span>$world$</span></mark><span style="white-space: pre-wrap;">!</span></p>',
493
+ md: `Hello [world](${URL})! Hello $world$! [Hello](${URL}) world! Hello $world$!`,
494
+ skipExport: true,
495
+ },
496
+ {
497
+ // Export only: import will use $...$ to transform <span /> to <mark /> due to HIGHLIGHT_TEXT_MATCH_IMPORT
498
+ html: "<p><span style='white-space: pre-wrap;'>$$H$&e$`l$'l$o$</span></p>",
499
+ md: "$$H$&e$\\`l$'l$o$",
500
+ skipImport: true,
501
+ },
502
+ {
503
+ customTransformers: [MDX_HTML_TRANSFORMER],
504
+ html: '<p><span style="white-space: pre-wrap;">Some HTML in mdx:</span></p><pre spellcheck="false" data-language="MyComponent"><span style="white-space: pre-wrap;">From HTML: Some Text</span></pre>',
505
+ md: 'Some HTML in mdx:\n\n<MyComponent>Some Text</MyComponent>',
506
+ shouldMergeAdjacentLines: true,
507
+ },
508
+ {
509
+ customTransformers: [MDX_HTML_TRANSFORMER],
510
+ html: '<p><span style="white-space: pre-wrap;">Some HTML in mdx:</span></p><pre spellcheck="false" data-language="MyComponent"><span style="white-space: pre-wrap;">From HTML: Line 1Some Text</span></pre>',
511
+ md: 'Some HTML in mdx:\n\n<MyComponent>Line 1\nSome Text</MyComponent>',
512
+ shouldMergeAdjacentLines: true,
513
+ skipExport: true,
514
+ },
515
+ {
516
+ customTransformers: [CODE_TAG_COUNTER_EXAMPLE],
517
+ // Ensure special ``` code block supports nested code blocks
518
+ html: '<pre spellcheck="false" data-language="ts" data-highlight-language="ts"><span style="white-space: pre-wrap;">Code\n```ts\nSub Code\n```</span></pre>',
519
+ md: '```ts\nCode\n```ts\nSub Code\n```\n```',
520
+ skipExport: true,
521
+ },
522
+ {
523
+ customTransformers: [SIMPLE_INLINE_JSX_MATCHER],
524
+ html: '<p><span style="white-space: pre-wrap;">Hello </span><a href="simple-jsx"><span style="white-space: pre-wrap;">One &lt;MyTag&gt;Two&lt;/MyTag&gt;</span></a><span style="white-space: pre-wrap;"> there</span></p>',
525
+ md: 'Hello <MyTag>One <MyTag>Two</MyTag></MyTag> there',
526
+ skipExport: true,
527
+ },
528
+ {
529
+ html: '<p><a href="https://lexical.dev"><span style="white-space: pre-wrap;">text</span></a></p>',
530
+ md: '[text](https://lexical.dev)',
531
+ },
532
+ {
533
+ html: '<p><code spellcheck="false" style="white-space: pre-wrap;"><span>text</span></code></p>',
534
+ md: '`text`',
535
+ },
536
+ {
537
+ html: '<p><a href="https://lexical.dev"><code spellcheck="false" style="white-space: pre-wrap;"><span>text</span></code></a></p>',
538
+ md: '[`text`](https://lexical.dev)',
539
+ },
540
+ {
541
+ html: '<p><b><strong style="white-space: pre-wrap;">Bold</strong></b><span style="white-space: pre-wrap;"> </span><a href="https://lexical.dev"><code spellcheck="false" style="white-space: pre-wrap;"><span>text</span></code></a><span style="white-space: pre-wrap;"> </span><b><strong style="white-space: pre-wrap;">Bold 2</strong></b></p>',
542
+ md: '**Bold** [`text`](https://lexical.dev) **Bold 2**',
543
+ },
544
+ {
545
+ html: '<p><b><strong style="white-space: pre-wrap;">Bold</strong></b><span style="white-space: pre-wrap;"> </span><a href="https://lexical.dev"><code spellcheck="false" style="white-space: pre-wrap;"><span>text</span></code><span style="white-space: pre-wrap;"> </span><b><strong style="white-space: pre-wrap;">Bold 2</strong></b></a><span style="white-space: pre-wrap;"> </span><b><strong style="white-space: pre-wrap;">Bold 3</strong></b></p>',
546
+ md: '**Bold** [`text` **Bold 2**](https://lexical.dev) **Bold 3**',
547
+ },
548
+ {
549
+ html: '<p><b><strong style="white-space: pre-wrap;">Bold</strong></b><span style="white-space: pre-wrap;"> </span><a href="https://lexical.dev"><code spellcheck="false" style="white-space: pre-wrap;"><span>text **Bold in code**</span></code></a><span style="white-space: pre-wrap;"> </span><b><strong style="white-space: pre-wrap;">Bold 3</strong></b></p>',
550
+ md: '**Bold** [`text **Bold in code**`](https://lexical.dev) **Bold 3**',
551
+ },
552
+ {
553
+ html: '<p><span style="white-space: pre-wrap;">Text </span><b><strong style="white-space: pre-wrap;">boldstart </strong></b><a href="https://lexical.dev"><b><strong style="white-space: pre-wrap;">text</strong></b></a><b><strong style="white-space: pre-wrap;"> boldend</strong></b><span style="white-space: pre-wrap;"> text</span></p>',
554
+ md: 'Text **boldstart [text](https://lexical.dev) boldend** text',
555
+ mdAfterExport:
556
+ 'Text **boldstart&#32;[text](https://lexical.dev)&#32;boldend** text',
557
+ },
558
+ {
559
+ html: '<p><span style="white-space: pre-wrap;">Text </span><b><strong style="white-space: pre-wrap;">boldstart </strong></b><a href="https://lexical.dev"><b><code spellcheck="false" style="white-space: pre-wrap;"><strong>text</strong></code></b></a><b><strong style="white-space: pre-wrap;"> boldend</strong></b><span style="white-space: pre-wrap;"> text</span></p>',
560
+ md: 'Text **boldstart [`text`](https://lexical.dev) boldend** text',
561
+ mdAfterExport:
562
+ 'Text **boldstart&#32;[`text`](https://lexical.dev)&#32;boldend** text',
563
+ },
564
+ {
565
+ html: '<p><span style="white-space: pre-wrap;">It </span><s><i><b><strong style="white-space: pre-wrap;">works </strong></b></i></s><a href="https://lexical.io"><s><i><b><strong style="white-space: pre-wrap;">with links</strong></b></i></s></a><span style="white-space: pre-wrap;"> too</span></p>',
566
+ md: 'It ~~___works [with links](https://lexical.io)___~~ too',
567
+ mdAfterExport:
568
+ 'It ***~~works&#32;[with links](https://lexical.io)~~*** too',
569
+ },
570
+ {
571
+ html: '<p><span style="white-space: pre-wrap;">It </span><s><i><b><strong style="white-space: pre-wrap;">works </strong></b></i></s><a href="https://lexical.io"><s><i><b><strong style="white-space: pre-wrap;">with links</strong></b></i></s></a><s><i><b><strong style="white-space: pre-wrap;"> too</strong></b></i></s><span style="white-space: pre-wrap;">!</span></p>',
572
+ md: 'It ~~___works [with links](https://lexical.io) too___~~!',
573
+ mdAfterExport:
574
+ 'It ***~~works&#32;[with links](https://lexical.io)&#32;too~~***!',
575
+ },
576
+ {
577
+ html: '<p><a href="https://lexical.dev"><span style="white-space: pre-wrap;">link</span></a><a href="https://lexical.dev"><span style="white-space: pre-wrap;">link2</span></a></p>',
578
+ md: '[link](https://lexical.dev)[link2](https://lexical.dev)',
579
+ },
580
+ {
581
+ html: '<p><b><code spellcheck="false" style="white-space: pre-wrap;"><strong>Bold Code</strong></code></b></p>',
582
+ md: '**`Bold Code`**',
583
+ },
584
+ {
585
+ html: '<p><span style="white-space: pre-wrap;">This is a backslash: \\</span></p>',
586
+ md: 'This is a backslash: \\\\',
587
+ },
588
+ {
589
+ html: '<p><span style="white-space: pre-wrap;">This is an asterisk: *</span></p>',
590
+ md: 'This is an asterisk: \\*',
591
+ },
592
+ {
593
+ html: '<p><span style="white-space: pre-wrap;">Backtick and asterisk: `**`</span></p>',
594
+ md: 'Backtick and asterisk: \\`\\*\\*\\`',
595
+ },
596
+ {
597
+ html: '<p><b><strong style="white-space: pre-wrap;">Backtick and asterisk: `**`</strong></b></p>',
598
+ md: '**Backtick and asterisk: \\`\\*\\*\\`**',
599
+ },
600
+ {
601
+ html: '<p><b><strong style="white-space: pre-wrap;">*test*</strong></b></p>',
602
+ md: '**\\*test\\***',
603
+ },
604
+ {
605
+ html: '<p><b><strong style="white-space: pre-wrap;">some bold text with an escaped star: *</strong></b><span style="white-space: pre-wrap;"> normal text</span></p>',
606
+ md: '**some bold text with an escaped star: \\*** normal text',
607
+ },
608
+ {
609
+ html: '<p><span style="white-space: pre-wrap;">*This text should </span><b><strong style="white-space: pre-wrap;">not</strong></b><span style="white-space: pre-wrap;"> be italic*</span></p>',
610
+ md: '\\*This text should **not** be italic*',
611
+ mdAfterExport: '\\*This text should **not** be italic\\*',
612
+ },
613
+ {
614
+ html: '<p><span style="white-space: pre-wrap;">*some text*</span></p>',
615
+ md: '\\*some text*',
616
+ mdAfterExport: '\\*some text\\*',
617
+ },
618
+ {
619
+ html: '<p><a href="https://lexical.dev"><span style="white-space: pre-wrap;">text </span><b><strong style="white-space: pre-wrap;">bold</strong></b><span style="white-space: pre-wrap;"> *normal*</span></a></p>',
620
+ md: '[text **bold** \\*normal\\*](https://lexical.dev)',
621
+ },
622
+ {
623
+ html: '<p><span style="white-space: pre-wrap;">*Hello* world</span></p>',
624
+ md: '\\*Hello\\* world',
625
+ },
626
+ {
627
+ html: '<p><b><strong style="white-space: pre-wrap;">&nbsp;</strong></b></p>',
628
+ md: '**&#160;**',
629
+ },
630
+ ];
631
+
632
+ const HIGHLIGHT_TEXT_MATCH_IMPORT: TextMatchTransformer = {
633
+ ...LINK,
634
+ importRegExp: /\$([^$]+?)\$/,
635
+ replace: (textNode) => {
636
+ textNode.setFormat('highlight');
637
+ },
638
+ };
639
+
640
+ for (const {
641
+ html,
642
+ md,
643
+ skipImport,
644
+ shouldPreserveNewLines,
645
+ shouldMergeAdjacentLines,
646
+ customTransformers,
647
+ } of IMPORT_AND_EXPORT) {
648
+ if (skipImport) {
649
+ continue;
650
+ }
651
+
652
+ it(`can import "${md.replace(/\n/g, '\\n')}"`, () => {
653
+ const editor = createHeadlessEditor({
654
+ nodes: [
655
+ HeadingNode,
656
+ ListNode,
657
+ ListItemNode,
658
+ QuoteNode,
659
+ CodeNode,
660
+ LinkNode,
661
+ ],
662
+ });
663
+
664
+ editor.update(
665
+ () =>
666
+ $convertFromMarkdownString(
667
+ md,
668
+ [
669
+ ...(customTransformers || []),
670
+ ...TRANSFORMERS,
671
+ HIGHLIGHT_TEXT_MATCH_IMPORT,
672
+ ],
673
+ undefined,
674
+ shouldPreserveNewLines,
675
+ shouldMergeAdjacentLines,
676
+ ),
677
+ {
678
+ discrete: true,
679
+ },
680
+ );
681
+
682
+ expect(
683
+ editor.getEditorState().read(() => $generateHtmlFromNodes(editor)),
684
+ ).toBe(html);
685
+ });
686
+ }
687
+
688
+ for (const {
689
+ html,
690
+ md,
691
+ skipExport,
692
+ shouldPreserveNewLines,
693
+ customTransformers,
694
+ mdAfterExport,
695
+ } of IMPORT_AND_EXPORT) {
696
+ if (skipExport) {
697
+ continue;
698
+ }
699
+
700
+ it(`can export "${md.replace(/\n/g, '\\n')}"`, () => {
701
+ const editor = createHeadlessEditor({
702
+ nodes: [
703
+ HeadingNode,
704
+ ListNode,
705
+ ListItemNode,
706
+ QuoteNode,
707
+ CodeNode,
708
+ LinkNode,
709
+ ],
710
+ });
711
+
712
+ editor.update(
713
+ () => {
714
+ const parser = new DOMParser();
715
+ const dom = parser.parseFromString(html, 'text/html');
716
+ const nodes = $generateNodesFromDOM(editor, dom);
717
+ $getRoot().select();
718
+ $insertNodes(nodes);
719
+ },
720
+ {
721
+ discrete: true,
722
+ },
723
+ );
724
+
725
+ expect(
726
+ editor
727
+ .getEditorState()
728
+ .read(() =>
729
+ $convertToMarkdownString(
730
+ [...(customTransformers || []), ...TRANSFORMERS],
731
+ undefined,
732
+ shouldPreserveNewLines,
733
+ ),
734
+ ),
735
+ ).toBe(mdAfterExport ?? md);
736
+ });
737
+ }
738
+ });
739
+
740
+ describe('normalizeMarkdown - shouldMergeAdjacentLines = true', () => {
741
+ it('should combine lines separated by a single \n unless they are in a codeblock', () => {
742
+ const markdown = `
743
+ A1
744
+ A2
745
+
746
+ A3
747
+
748
+ \`\`\`md
749
+ B1
750
+ B2
751
+
752
+ B3
753
+ \`\`\`
754
+
755
+ C1
756
+ C2
757
+
758
+ C3
759
+
760
+ \`\`\`js
761
+ D1
762
+ D2
763
+
764
+ D3
765
+ \`\`\`
766
+
767
+ \`\`\`single line code\`\`\`
768
+
769
+ E1
770
+ E2
771
+
772
+ E3
773
+ `;
774
+ expect(normalizeMarkdown(markdown, true)).toBe(`
775
+ A1A2
776
+
777
+ A3
778
+
779
+ \`\`\`md
780
+ B1
781
+ B2
782
+
783
+ B3
784
+ \`\`\`
785
+
786
+ C1C2
787
+
788
+ C3
789
+
790
+ \`\`\`js
791
+ D1
792
+ D2
793
+
794
+ D3
795
+ \`\`\`
796
+
797
+ \`\`\`single line code\`\`\`
798
+
799
+ E1E2
800
+
801
+ E3
802
+ `);
803
+ });
804
+
805
+ it('tables', () => {
806
+ const markdown = `
807
+ | a | b |
808
+ | --- | --- |
809
+ | c | d |
810
+ `;
811
+ expect(normalizeMarkdown(markdown, true)).toBe(markdown);
812
+ });
813
+ });
814
+
815
+ describe('normalizeMarkdown - shouldMergeAdjacentLines = false', () => {
816
+ it('should not combine lines separated by a single \n', () => {
817
+ const markdown = `
818
+ A1
819
+ A2
820
+
821
+ A3
822
+
823
+ \`\`\`md
824
+ B1
825
+ B2
826
+
827
+ B3
828
+ \`\`\`
829
+
830
+ C1
831
+ C2
832
+
833
+ C3
834
+
835
+ \`\`\`js
836
+ D1
837
+ D2
838
+
839
+ D3
840
+ \`\`\`
841
+
842
+ \`\`\`single line code\`\`\`
843
+
844
+ E1
845
+ E2
846
+
847
+ E3
848
+ `;
849
+ expect(normalizeMarkdown(markdown, false)).toBe(`
850
+ A1
851
+ A2
852
+
853
+ A3
854
+
855
+ \`\`\`md
856
+ B1
857
+ B2
858
+
859
+ B3
860
+ \`\`\`
861
+
862
+ C1
863
+ C2
864
+
865
+ C3
866
+
867
+ \`\`\`js
868
+ D1
869
+ D2
870
+
871
+ D3
872
+ \`\`\`
873
+
874
+ \`\`\`single line code\`\`\`
875
+
876
+ E1
877
+ E2
878
+
879
+ E3
880
+ `);
881
+ });
882
+
883
+ it('tables', () => {
884
+ const markdown = `
885
+ | a | b |
886
+ | --- | --- |
887
+ | c | d |
888
+ `;
889
+ expect(normalizeMarkdown(markdown, false)).toBe(markdown);
890
+ });
891
+ });