@lexical/markdown 0.8.1 → 0.9.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.
@@ -20,16 +20,20 @@ var link = require('@lexical/link');
20
20
  * LICENSE file in the root directory of this source tree.
21
21
  *
22
22
  */
23
+
23
24
  function indexBy(list, callback) {
24
25
  const index = {};
26
+
25
27
  for (const item of list) {
26
28
  const key = callback(item);
29
+
27
30
  if (index[key]) {
28
31
  index[key].push(item);
29
32
  } else {
30
33
  index[key] = [item];
31
34
  }
32
35
  }
36
+
33
37
  return index;
34
38
  }
35
39
  function transformersByType(transformers) {
@@ -50,30 +54,35 @@ const PUNCTUATION_OR_SPACE = /[!-/:-@[-`{-~\s]/;
50
54
  *
51
55
  */
52
56
  function createMarkdownExport(transformers) {
53
- const byType = transformersByType(transformers);
54
-
55
- // Export only uses text formats that are responsible for single format
57
+ const byType = transformersByType(transformers); // Export only uses text formats that are responsible for single format
56
58
  // e.g. it will filter out *** (bold, italic) and instead use separate ** and *
59
+
57
60
  const textFormatTransformers = byType.textFormat.filter(transformer => transformer.format.length === 1);
58
61
  return () => {
59
62
  const output = [];
60
63
  const children = lexical.$getRoot().getChildren();
64
+
61
65
  for (const child of children) {
62
66
  const result = exportTopLevelElements(child, byType.element, textFormatTransformers, byType.textMatch);
67
+
63
68
  if (result != null) {
64
69
  output.push(result);
65
70
  }
66
71
  }
72
+
67
73
  return output.join('\n\n');
68
74
  };
69
75
  }
76
+
70
77
  function exportTopLevelElements(node, elementTransformers, textTransformersIndex, textMatchTransformers) {
71
78
  for (const transformer of elementTransformers) {
72
79
  const result = transformer.export(node, _node => exportChildren(_node, textTransformersIndex, textMatchTransformers));
80
+
73
81
  if (result != null) {
74
82
  return result;
75
83
  }
76
84
  }
85
+
77
86
  if (lexical.$isElementNode(node)) {
78
87
  return exportChildren(node, textTransformersIndex, textMatchTransformers);
79
88
  } else if (lexical.$isDecoratorNode(node)) {
@@ -82,17 +91,21 @@ function exportTopLevelElements(node, elementTransformers, textTransformersIndex
82
91
  return null;
83
92
  }
84
93
  }
94
+
85
95
  function exportChildren(node, textTransformersIndex, textMatchTransformers) {
86
96
  const output = [];
87
97
  const children = node.getChildren();
98
+
88
99
  mainLoop: for (const child of children) {
89
100
  for (const transformer of textMatchTransformers) {
90
101
  const result = transformer.export(child, parentNode => exportChildren(parentNode, textTransformersIndex, textMatchTransformers), (textNode, textContent) => exportTextFormat(textNode, textContent, textTransformersIndex));
102
+
91
103
  if (result != null) {
92
104
  output.push(result);
93
105
  continue mainLoop;
94
106
  }
95
107
  }
108
+
96
109
  if (lexical.$isLineBreakNode(child)) {
97
110
  output.push('\n');
98
111
  } else if (lexical.$isTextNode(child)) {
@@ -103,8 +116,10 @@ function exportChildren(node, textTransformersIndex, textMatchTransformers) {
103
116
  output.push(child.getTextContent());
104
117
  }
105
118
  }
119
+
106
120
  return output.join('');
107
121
  }
122
+
108
123
  function exportTextFormat(node, textContent, textTransformers) {
109
124
  // This function handles the case of a string looking like this: " foo "
110
125
  // Where it would be invalid markdown to generate: "** foo **"
@@ -113,61 +128,74 @@ function exportTextFormat(node, textContent, textTransformers) {
113
128
  const frozenString = textContent.trim();
114
129
  let output = frozenString;
115
130
  const applied = new Set();
131
+
116
132
  for (const transformer of textTransformers) {
117
133
  const format = transformer.format[0];
118
134
  const tag = transformer.tag;
135
+
119
136
  if (hasFormat(node, format) && !applied.has(format)) {
120
137
  // Multiple tags might be used for the same format (*, _)
121
- applied.add(format);
122
- // Prevent adding opening tag is already opened by the previous sibling
138
+ applied.add(format); // Prevent adding opening tag is already opened by the previous sibling
139
+
123
140
  const previousNode = getTextSibling(node, true);
141
+
124
142
  if (!hasFormat(previousNode, format)) {
125
143
  output = tag + output;
126
- }
144
+ } // Prevent adding closing tag if next sibling will do it
145
+
127
146
 
128
- // Prevent adding closing tag if next sibling will do it
129
147
  const nextNode = getTextSibling(node, false);
148
+
130
149
  if (!hasFormat(nextNode, format)) {
131
150
  output += tag;
132
151
  }
133
152
  }
134
- }
153
+ } // Replace trimmed version of textContent ensuring surrounding whitespace is not modified
135
154
 
136
- // Replace trimmed version of textContent ensuring surrounding whitespace is not modified
137
- return textContent.replace(frozenString, output);
138
- }
139
155
 
140
- // Get next or previous text sibling a text node, including cases
156
+ return textContent.replace(frozenString, output);
157
+ } // Get next or previous text sibling a text node, including cases
141
158
  // when it's a child of inline element (e.g. link)
159
+
160
+
142
161
  function getTextSibling(node, backward) {
143
162
  let sibling = backward ? node.getPreviousSibling() : node.getNextSibling();
163
+
144
164
  if (!sibling) {
145
165
  const parent = node.getParentOrThrow();
166
+
146
167
  if (parent.isInline()) {
147
168
  sibling = backward ? parent.getPreviousSibling() : parent.getNextSibling();
148
169
  }
149
170
  }
171
+
150
172
  while (sibling) {
151
173
  if (lexical.$isElementNode(sibling)) {
152
174
  if (!sibling.isInline()) {
153
175
  break;
154
176
  }
177
+
155
178
  const descendant = backward ? sibling.getLastDescendant() : sibling.getFirstDescendant();
179
+
156
180
  if (lexical.$isTextNode(descendant)) {
157
181
  return descendant;
158
182
  } else {
159
183
  sibling = backward ? sibling.getPreviousSibling() : sibling.getNextSibling();
160
184
  }
161
185
  }
186
+
162
187
  if (lexical.$isTextNode(sibling)) {
163
188
  return sibling;
164
189
  }
190
+
165
191
  if (!lexical.$isElementNode(sibling)) {
166
192
  return null;
167
193
  }
168
194
  }
195
+
169
196
  return null;
170
197
  }
198
+
171
199
  function hasFormat(node, format) {
172
200
  return lexical.$isTextNode(node) && node.hasFormat(format);
173
201
  }
@@ -179,7 +207,6 @@ function hasFormat(node, format) {
179
207
  * LICENSE file in the root directory of this source tree.
180
208
  *
181
209
  */
182
-
183
210
  const CAN_USE_DOM = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined';
184
211
 
185
212
  /**
@@ -194,12 +221,10 @@ CAN_USE_DOM && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
194
221
  CAN_USE_DOM && /^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent);
195
222
  CAN_USE_DOM && 'InputEvent' in window && !documentMode ? 'getTargetRanges' in new window.InputEvent('input') : false;
196
223
  const IS_SAFARI = CAN_USE_DOM && /Version\/[\d.]+.*Safari/.test(navigator.userAgent);
197
- const IS_IOS = CAN_USE_DOM && /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
198
-
199
- // Keep these in case we need to use them in the future.
224
+ const IS_IOS = CAN_USE_DOM && /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; // Keep these in case we need to use them in the future.
200
225
  // export const IS_WINDOWS: boolean = CAN_USE_DOM && /Win/.test(navigator.platform);
201
- const IS_CHROME = CAN_USE_DOM && /^(?=.*Chrome).*/i.test(navigator.userAgent);
202
- // export const canUseTextInputEvent: boolean = CAN_USE_DOM && 'TextEvent' in window && !documentMode;
226
+
227
+ const IS_CHROME = CAN_USE_DOM && /^(?=.*Chrome).*/i.test(navigator.userAgent); // export const canUseTextInputEvent: boolean = CAN_USE_DOM && 'TextEvent' in window && !documentMode;
203
228
 
204
229
  const IS_APPLE_WEBKIT = CAN_USE_DOM && /AppleWebKit\/[\d.]+/.test(navigator.userAgent) && !IS_CHROME;
205
230
 
@@ -220,72 +245,86 @@ function createMarkdownImport(transformers) {
220
245
  const linesLength = lines.length;
221
246
  const root = lexical.$getRoot();
222
247
  root.clear();
248
+
223
249
  for (let i = 0; i < linesLength; i++) {
224
- const lineText = lines[i];
225
- // Codeblocks are processed first as anything inside such block
250
+ const lineText = lines[i]; // Codeblocks are processed first as anything inside such block
226
251
  // is ignored for further processing
227
252
  // TODO:
228
253
  // Abstract it to be dynamic as other transformers (add multiline match option)
254
+
229
255
  const [codeBlockNode, shiftedIndex] = importCodeBlock(lines, i, root);
256
+
230
257
  if (codeBlockNode != null) {
231
258
  i = shiftedIndex;
232
259
  continue;
233
260
  }
234
- importBlocks(lineText, root, byType.element, textFormatTransformersIndex, byType.textMatch);
235
- }
236
261
 
237
- // Removing empty paragraphs as md does not really
262
+ importBlocks(lineText, root, byType.element, textFormatTransformersIndex, byType.textMatch);
263
+ } // Removing empty paragraphs as md does not really
238
264
  // allow empty lines and uses them as dilimiter
265
+
266
+
239
267
  const children = root.getChildren();
268
+
240
269
  for (const child of children) {
241
270
  if (isEmptyParagraph(child)) {
242
271
  child.remove();
243
272
  }
244
273
  }
274
+
245
275
  root.selectEnd();
246
276
  };
247
277
  }
278
+
248
279
  function isEmptyParagraph(node) {
249
280
  if (!lexical.$isParagraphNode(node)) {
250
281
  return false;
251
282
  }
283
+
252
284
  const firstChild = node.getFirstChild();
253
285
  return firstChild == null || node.getChildrenSize() === 1 && lexical.$isTextNode(firstChild) && MARKDOWN_EMPTY_LINE_REG_EXP.test(firstChild.getTextContent());
254
286
  }
287
+
255
288
  function importBlocks(lineText, rootNode, elementTransformers, textFormatTransformersIndex, textMatchTransformers) {
256
289
  const lineTextTrimmed = lineText.trim();
257
290
  const textNode = lexical.$createTextNode(lineTextTrimmed);
258
291
  const elementNode = lexical.$createParagraphNode();
259
292
  elementNode.append(textNode);
260
293
  rootNode.append(elementNode);
294
+
261
295
  for (const {
262
296
  regExp,
263
297
  replace
264
298
  } of elementTransformers) {
265
299
  const match = lineText.match(regExp);
300
+
266
301
  if (match) {
267
302
  textNode.setTextContent(lineText.slice(match[0].length));
268
303
  replace(elementNode, [textNode], match, true);
269
304
  break;
270
305
  }
271
306
  }
272
- importTextFormatTransformers(textNode, textFormatTransformersIndex, textMatchTransformers);
273
307
 
274
- // If no transformer found and we left with original paragraph node
308
+ importTextFormatTransformers(textNode, textFormatTransformersIndex, textMatchTransformers); // If no transformer found and we left with original paragraph node
275
309
  // can check if its content can be appended to the previous node
276
310
  // if it's a paragraph, quote or list
311
+
277
312
  if (elementNode.isAttached() && lineTextTrimmed.length > 0) {
278
313
  const previousNode = elementNode.getPreviousSibling();
314
+
279
315
  if (lexical.$isParagraphNode(previousNode) || richText.$isQuoteNode(previousNode) || list.$isListNode(previousNode)) {
280
316
  let targetNode = previousNode;
317
+
281
318
  if (list.$isListNode(previousNode)) {
282
319
  const lastDescendant = previousNode.getLastDescendant();
320
+
283
321
  if (lastDescendant == null) {
284
322
  targetNode = null;
285
323
  } else {
286
324
  targetNode = utils.$findMatchingParent(lastDescendant, list.$isListItemNode);
287
325
  }
288
326
  }
327
+
289
328
  if (targetNode != null && targetNode.getTextContentSize() > 0) {
290
329
  targetNode.splice(targetNode.getChildrenSize(), 0, [lexical.$createLineBreakNode(), ...elementNode.getChildren()]);
291
330
  elementNode.remove();
@@ -293,13 +332,17 @@ function importBlocks(lineText, rootNode, elementTransformers, textFormatTransfo
293
332
  }
294
333
  }
295
334
  }
335
+
296
336
  function importCodeBlock(lines, startLineIndex, rootNode) {
297
337
  const openMatch = lines[startLineIndex].match(CODE_BLOCK_REG_EXP);
338
+
298
339
  if (openMatch) {
299
340
  let endLineIndex = startLineIndex;
300
341
  const linesLength = lines.length;
342
+
301
343
  while (++endLineIndex < linesLength) {
302
344
  const closeMatch = lines[endLineIndex].match(CODE_BLOCK_REG_EXP);
345
+
303
346
  if (closeMatch) {
304
347
  const codeBlockNode = code.$createCodeNode(openMatch[1]);
305
348
  const textNode = lexical.$createTextNode(lines.slice(startLineIndex + 1, endLineIndex).join('\n'));
@@ -309,133 +352,157 @@ function importCodeBlock(lines, startLineIndex, rootNode) {
309
352
  }
310
353
  }
311
354
  }
312
- return [null, startLineIndex];
313
- }
314
355
 
315
- // Processing text content and replaces text format tags.
356
+ return [null, startLineIndex];
357
+ } // Processing text content and replaces text format tags.
316
358
  // It takes outermost tag match and its content, creates text node with
317
359
  // format based on tag and then recursively executed over node's content
318
360
  //
319
361
  // E.g. for "*Hello **world**!*" string it will create text node with
320
362
  // "Hello **world**!" content and italic format and run recursively over
321
363
  // its content to transform "**world**" part
364
+
365
+
322
366
  function importTextFormatTransformers(textNode, textFormatTransformersIndex, textMatchTransformers) {
323
367
  const textContent = textNode.getTextContent();
324
368
  const match = findOutermostMatch(textContent, textFormatTransformersIndex);
369
+
325
370
  if (!match) {
326
371
  // Once text format processing is done run text match transformers, as it
327
372
  // only can span within single text node (unline formats that can cover multiple nodes)
328
373
  importTextMatchTransformers(textNode, textMatchTransformers);
329
374
  return;
330
375
  }
331
- let currentNode, remainderNode, leadingNode;
332
376
 
333
- // If matching full content there's no need to run splitText and can reuse existing textNode
377
+ let currentNode, remainderNode, leadingNode; // If matching full content there's no need to run splitText and can reuse existing textNode
334
378
  // to update its content and apply format. E.g. for **_Hello_** string after applying bold
335
379
  // format (**) it will reuse the same text node to apply italic (_)
380
+
336
381
  if (match[0] === textContent) {
337
382
  currentNode = textNode;
338
383
  } else {
339
384
  const startIndex = match.index || 0;
340
385
  const endIndex = startIndex + match[0].length;
386
+
341
387
  if (startIndex === 0) {
342
388
  [currentNode, remainderNode] = textNode.splitText(endIndex);
343
389
  } else {
344
390
  [leadingNode, currentNode, remainderNode] = textNode.splitText(startIndex, endIndex);
345
391
  }
346
392
  }
393
+
347
394
  currentNode.setTextContent(match[2]);
348
395
  const transformer = textFormatTransformersIndex.transformersByTag[match[1]];
396
+
349
397
  if (transformer) {
350
398
  for (const format of transformer.format) {
351
399
  if (!currentNode.hasFormat(format)) {
352
400
  currentNode.toggleFormat(format);
353
401
  }
354
402
  }
355
- }
403
+ } // Recursively run over inner text if it's not inline code
404
+
356
405
 
357
- // Recursively run over inner text if it's not inline code
358
406
  if (!currentNode.hasFormat('code')) {
359
407
  importTextFormatTransformers(currentNode, textFormatTransformersIndex, textMatchTransformers);
360
- }
408
+ } // Run over leading/remaining text if any
409
+
361
410
 
362
- // Run over leading/remaining text if any
363
411
  if (leadingNode) {
364
412
  importTextFormatTransformers(leadingNode, textFormatTransformersIndex, textMatchTransformers);
365
413
  }
414
+
366
415
  if (remainderNode) {
367
416
  importTextFormatTransformers(remainderNode, textFormatTransformersIndex, textMatchTransformers);
368
417
  }
369
418
  }
419
+
370
420
  function importTextMatchTransformers(textNode_, textMatchTransformers) {
371
421
  let textNode = textNode_;
422
+
372
423
  mainLoop: while (textNode) {
373
424
  for (const transformer of textMatchTransformers) {
374
425
  const match = textNode.getTextContent().match(transformer.importRegExp);
426
+
375
427
  if (!match) {
376
428
  continue;
377
429
  }
430
+
378
431
  const startIndex = match.index || 0;
379
432
  const endIndex = startIndex + match[0].length;
380
433
  let replaceNode, leftTextNode, rightTextNode;
434
+
381
435
  if (startIndex === 0) {
382
436
  [replaceNode, textNode] = textNode.splitText(endIndex);
383
437
  } else {
384
438
  [leftTextNode, replaceNode, rightTextNode] = textNode.splitText(startIndex, endIndex);
385
439
  }
440
+
386
441
  if (leftTextNode) {
387
442
  importTextMatchTransformers(leftTextNode, textMatchTransformers);
388
443
  }
444
+
389
445
  if (rightTextNode) {
390
446
  textNode = rightTextNode;
391
447
  }
448
+
392
449
  transformer.replace(replaceNode, match);
393
450
  continue mainLoop;
394
451
  }
452
+
395
453
  break;
396
454
  }
397
- }
455
+ } // Finds first "<tag>content<tag>" match that is not nested into another tag
456
+
398
457
 
399
- // Finds first "<tag>content<tag>" match that is not nested into another tag
400
458
  function findOutermostMatch(textContent, textTransformersIndex) {
401
459
  const openTagsMatch = textContent.match(textTransformersIndex.openTagsRegExp);
460
+
402
461
  if (openTagsMatch == null) {
403
462
  return null;
404
463
  }
464
+
405
465
  for (const match of openTagsMatch) {
406
466
  // Open tags reg exp might capture leading space so removing it
407
467
  // before using match to find transformer
408
468
  const tag = match.replace(/^\s/, '');
409
469
  const fullMatchRegExp = textTransformersIndex.fullMatchRegExpByTag[tag];
470
+
410
471
  if (fullMatchRegExp == null) {
411
472
  continue;
412
473
  }
474
+
413
475
  const fullMatch = textContent.match(fullMatchRegExp);
414
476
  const transformer = textTransformersIndex.transformersByTag[tag];
477
+
415
478
  if (fullMatch != null && transformer != null) {
416
479
  if (transformer.intraword !== false) {
417
480
  return fullMatch;
418
- }
419
-
420
- // For non-intraword transformers checking if it's within a word
481
+ } // For non-intraword transformers checking if it's within a word
421
482
  // or surrounded with space/punctuation/newline
483
+
484
+
422
485
  const {
423
486
  index = 0
424
487
  } = fullMatch;
425
488
  const beforeChar = textContent[index - 1];
426
489
  const afterChar = textContent[index + fullMatch[0].length];
490
+
427
491
  if ((!beforeChar || PUNCTUATION_OR_SPACE.test(beforeChar)) && (!afterChar || PUNCTUATION_OR_SPACE.test(afterChar))) {
428
492
  return fullMatch;
429
493
  }
430
494
  }
431
495
  }
496
+
432
497
  return null;
433
498
  }
499
+
434
500
  function createTextFormatTransformersIndex(textTransformers) {
435
501
  const transformersByTag = {};
436
502
  const fullMatchRegExpByTag = {};
437
503
  const openTagsRegExp = [];
438
504
  const escapeRegExp = `(?<![\\\\])`;
505
+
439
506
  for (const transformer of textTransformers) {
440
507
  const {
441
508
  tag
@@ -443,12 +510,14 @@ function createTextFormatTransformersIndex(textTransformers) {
443
510
  transformersByTag[tag] = transformer;
444
511
  const tagRegExp = tag.replace(/(\*|\^|\+)/g, '\\$1');
445
512
  openTagsRegExp.push(tagRegExp);
513
+
446
514
  if (IS_SAFARI || IS_IOS || IS_APPLE_WEBKIT) {
447
515
  fullMatchRegExpByTag[tag] = new RegExp(`(${tagRegExp})(?![${tagRegExp}\\s])(.*?[^${tagRegExp}\\s])${tagRegExp}(?!${tagRegExp})`);
448
516
  } else {
449
517
  fullMatchRegExpByTag[tag] = new RegExp(`(?<![\\\\${tagRegExp}])(${tagRegExp})((\\\\${tagRegExp})?.*?[^${tagRegExp}\\s](\\\\${tagRegExp})?)((?<!\\\\)|(?<=\\\\\\\\))(${tagRegExp})(?![\\\\${tagRegExp}])`);
450
518
  }
451
519
  }
520
+
452
521
  return {
453
522
  // Reg exp to find open tag + content + close tag
454
523
  fullMatchRegExpByTag,
@@ -465,27 +534,31 @@ function createTextFormatTransformersIndex(textTransformers) {
465
534
  * LICENSE file in the root directory of this source tree.
466
535
  *
467
536
  */
537
+
468
538
  function runElementTransformers(parentNode, anchorNode, anchorOffset, elementTransformers) {
469
539
  const grandParentNode = parentNode.getParent();
540
+
470
541
  if (!lexical.$isRootOrShadowRoot(grandParentNode) || parentNode.getFirstChild() !== anchorNode) {
471
542
  return false;
472
543
  }
473
- const textContent = anchorNode.getTextContent();
474
544
 
475
- // Checking for anchorOffset position to prevent any checks for cases when caret is too far
545
+ const textContent = anchorNode.getTextContent(); // Checking for anchorOffset position to prevent any checks for cases when caret is too far
476
546
  // from a line start to be a part of block-level markdown trigger.
477
547
  //
478
548
  // TODO:
479
549
  // Can have a quick check if caret is close enough to the beginning of the string (e.g. offset less than 10-20)
480
550
  // since otherwise it won't be a markdown shortcut, but tables are exception
551
+
481
552
  if (textContent[anchorOffset - 1] !== ' ') {
482
553
  return false;
483
554
  }
555
+
484
556
  for (const {
485
557
  regExp,
486
558
  replace
487
559
  } of elementTransformers) {
488
560
  const match = textContent.match(regExp);
561
+
489
562
  if (match && match[0].length === anchorOffset) {
490
563
  const nextSiblings = anchorNode.getNextSiblings();
491
564
  const [leadingNode, remainderNode] = anchorNode.splitText(anchorOffset);
@@ -495,115 +568,131 @@ function runElementTransformers(parentNode, anchorNode, anchorOffset, elementTra
495
568
  return true;
496
569
  }
497
570
  }
571
+
498
572
  return false;
499
573
  }
574
+
500
575
  function runTextMatchTransformers(anchorNode, anchorOffset, transformersByTrigger) {
501
576
  let textContent = anchorNode.getTextContent();
502
577
  const lastChar = textContent[anchorOffset - 1];
503
578
  const transformers = transformersByTrigger[lastChar];
579
+
504
580
  if (transformers == null) {
505
581
  return false;
506
- }
507
-
508
- // If typing in the middle of content, remove the tail to do
582
+ } // If typing in the middle of content, remove the tail to do
509
583
  // reg exp match up to a string end (caret position)
584
+
585
+
510
586
  if (anchorOffset < textContent.length) {
511
587
  textContent = textContent.slice(0, anchorOffset);
512
588
  }
589
+
513
590
  for (const transformer of transformers) {
514
591
  const match = textContent.match(transformer.regExp);
592
+
515
593
  if (match === null) {
516
594
  continue;
517
595
  }
596
+
518
597
  const startIndex = match.index || 0;
519
598
  const endIndex = startIndex + match[0].length;
520
599
  let replaceNode;
600
+
521
601
  if (startIndex === 0) {
522
602
  [replaceNode] = anchorNode.splitText(endIndex);
523
603
  } else {
524
604
  [, replaceNode] = anchorNode.splitText(startIndex, endIndex);
525
605
  }
606
+
526
607
  replaceNode.selectNext(0, 0);
527
608
  transformer.replace(replaceNode, match);
528
609
  return true;
529
610
  }
611
+
530
612
  return false;
531
613
  }
614
+
532
615
  function runTextFormatTransformers(anchorNode, anchorOffset, textFormatTransformers) {
533
616
  const textContent = anchorNode.getTextContent();
534
617
  const closeTagEndIndex = anchorOffset - 1;
535
- const closeChar = textContent[closeTagEndIndex];
536
- // Quick check if we're possibly at the end of inline markdown style
618
+ const closeChar = textContent[closeTagEndIndex]; // Quick check if we're possibly at the end of inline markdown style
619
+
537
620
  const matchers = textFormatTransformers[closeChar];
621
+
538
622
  if (!matchers) {
539
623
  return false;
540
624
  }
625
+
541
626
  for (const matcher of matchers) {
542
627
  const {
543
628
  tag
544
629
  } = matcher;
545
630
  const tagLength = tag.length;
546
- const closeTagStartIndex = closeTagEndIndex - tagLength + 1;
631
+ const closeTagStartIndex = closeTagEndIndex - tagLength + 1; // If tag is not single char check if rest of it matches with text content
547
632
 
548
- // If tag is not single char check if rest of it matches with text content
549
633
  if (tagLength > 1) {
550
634
  if (!isEqualSubString(textContent, closeTagStartIndex, tag, 0, tagLength)) {
551
635
  continue;
552
636
  }
553
- }
637
+ } // Space before closing tag cancels inline markdown
638
+
554
639
 
555
- // Space before closing tag cancels inline markdown
556
640
  if (textContent[closeTagStartIndex - 1] === ' ') {
557
641
  continue;
558
- }
642
+ } // Some tags can not be used within words, hence should have newline/space/punctuation after it
643
+
559
644
 
560
- // Some tags can not be used within words, hence should have newline/space/punctuation after it
561
645
  const afterCloseTagChar = textContent[closeTagEndIndex + 1];
646
+
562
647
  if (matcher.intraword === false && afterCloseTagChar && !PUNCTUATION_OR_SPACE.test(afterCloseTagChar)) {
563
648
  continue;
564
649
  }
650
+
565
651
  const closeNode = anchorNode;
566
652
  let openNode = closeNode;
567
- let openTagStartIndex = getOpenTagStartIndex(textContent, closeTagStartIndex, tag);
568
-
569
- // Go through text node siblings and search for opening tag
653
+ let openTagStartIndex = getOpenTagStartIndex(textContent, closeTagStartIndex, tag); // Go through text node siblings and search for opening tag
570
654
  // if haven't found it within the same text node as closing tag
655
+
571
656
  let sibling = openNode;
657
+
572
658
  while (openTagStartIndex < 0 && (sibling = sibling.getPreviousSibling())) {
573
659
  if (lexical.$isLineBreakNode(sibling)) {
574
660
  break;
575
661
  }
662
+
576
663
  if (lexical.$isTextNode(sibling)) {
577
664
  const siblingTextContent = sibling.getTextContent();
578
665
  openNode = sibling;
579
666
  openTagStartIndex = getOpenTagStartIndex(siblingTextContent, siblingTextContent.length, tag);
580
667
  }
581
- }
668
+ } // Opening tag is not found
669
+
582
670
 
583
- // Opening tag is not found
584
671
  if (openTagStartIndex < 0) {
585
672
  continue;
586
- }
673
+ } // No content between opening and closing tag
674
+
587
675
 
588
- // No content between opening and closing tag
589
676
  if (openNode === closeNode && openTagStartIndex + tagLength === closeTagStartIndex) {
590
677
  continue;
591
- }
678
+ } // Checking longer tags for repeating chars (e.g. *** vs **)
679
+
592
680
 
593
- // Checking longer tags for repeating chars (e.g. *** vs **)
594
681
  const prevOpenNodeText = openNode.getTextContent();
682
+
595
683
  if (openTagStartIndex > 0 && prevOpenNodeText[openTagStartIndex - 1] === closeChar) {
596
684
  continue;
597
- }
685
+ } // Some tags can not be used within words, hence should have newline/space/punctuation before it
686
+
598
687
 
599
- // Some tags can not be used within words, hence should have newline/space/punctuation before it
600
688
  const beforeOpenTagChar = prevOpenNodeText[openTagStartIndex - 1];
689
+
601
690
  if (matcher.intraword === false && beforeOpenTagChar && !PUNCTUATION_OR_SPACE.test(beforeOpenTagChar)) {
602
691
  continue;
603
- }
604
-
605
- // Clean text from opening and closing tags (starting from closing tag
692
+ } // Clean text from opening and closing tags (starting from closing tag
606
693
  // to prevent any offset shifts if we start from opening one)
694
+
695
+
607
696
  const prevCloseNodeText = closeNode.getTextContent();
608
697
  const closeNodeText = prevCloseNodeText.slice(0, closeTagStartIndex) + prevCloseNodeText.slice(closeTagEndIndex + 1);
609
698
  closeNode.setTextContent(closeNodeText);
@@ -611,55 +700,62 @@ function runTextFormatTransformers(anchorNode, anchorOffset, textFormatTransform
611
700
  openNode.setTextContent(openNodeText.slice(0, openTagStartIndex) + openNodeText.slice(openTagStartIndex + tagLength));
612
701
  const selection = lexical.$getSelection();
613
702
  const nextSelection = lexical.$createRangeSelection();
614
- lexical.$setSelection(nextSelection);
615
- // Adjust offset based on deleted chars
703
+ lexical.$setSelection(nextSelection); // Adjust offset based on deleted chars
704
+
616
705
  const newOffset = closeTagEndIndex - tagLength * (openNode === closeNode ? 2 : 1) + 1;
617
706
  nextSelection.anchor.set(openNode.__key, openTagStartIndex, 'text');
618
- nextSelection.focus.set(closeNode.__key, newOffset, 'text');
707
+ nextSelection.focus.set(closeNode.__key, newOffset, 'text'); // Apply formatting to selected text
619
708
 
620
- // Apply formatting to selected text
621
709
  for (const format of matcher.format) {
622
710
  if (!nextSelection.hasFormat(format)) {
623
711
  nextSelection.formatText(format);
624
712
  }
625
- }
713
+ } // Collapse selection up to the focus point
714
+
626
715
 
627
- // Collapse selection up to the focus point
628
- nextSelection.anchor.set(nextSelection.focus.key, nextSelection.focus.offset, nextSelection.focus.type);
716
+ nextSelection.anchor.set(nextSelection.focus.key, nextSelection.focus.offset, nextSelection.focus.type); // Remove formatting from collapsed selection
629
717
 
630
- // Remove formatting from collapsed selection
631
718
  for (const format of matcher.format) {
632
719
  if (nextSelection.hasFormat(format)) {
633
720
  nextSelection.toggleFormat(format);
634
721
  }
635
722
  }
723
+
636
724
  if (lexical.$isRangeSelection(selection)) {
637
725
  nextSelection.format = selection.format;
638
726
  }
727
+
639
728
  return true;
640
729
  }
730
+
641
731
  return false;
642
732
  }
733
+
643
734
  function getOpenTagStartIndex(string, maxIndex, tag) {
644
735
  const tagLength = tag.length;
736
+
645
737
  for (let i = maxIndex; i >= tagLength; i--) {
646
738
  const startIndex = i - tagLength;
647
- if (isEqualSubString(string, startIndex, tag, 0, tagLength) &&
648
- // Space after opening tag cancels transformation
739
+
740
+ if (isEqualSubString(string, startIndex, tag, 0, tagLength) && // Space after opening tag cancels transformation
649
741
  string[startIndex + tagLength] !== ' ') {
650
742
  return startIndex;
651
743
  }
652
744
  }
745
+
653
746
  return -1;
654
747
  }
748
+
655
749
  function isEqualSubString(stringA, aStart, stringB, bStart, length) {
656
750
  for (let i = 0; i < length; i++) {
657
751
  if (stringA[aStart + i] !== stringB[bStart + i]) {
658
752
  return false;
659
753
  }
660
754
  }
755
+
661
756
  return true;
662
757
  }
758
+
663
759
  function registerMarkdownShortcuts(editor, transformers = TRANSFORMERS) {
664
760
  const byType = transformersByType(transformers);
665
761
  const textFormatTransformersIndex = indexBy(byType.textFormat, ({
@@ -668,10 +764,13 @@ function registerMarkdownShortcuts(editor, transformers = TRANSFORMERS) {
668
764
  const textMatchTransformersIndex = indexBy(byType.textMatch, ({
669
765
  trigger
670
766
  }) => trigger);
767
+
671
768
  for (const transformer of transformers) {
672
769
  const type = transformer.type;
770
+
673
771
  if (type === 'element' || type === 'text-match') {
674
772
  const dependencies = transformer.dependencies;
773
+
675
774
  if (!editor.hasNodes(dependencies)) {
676
775
  {
677
776
  throw Error(`MarkdownShortcuts: missing dependency for transformer. Ensure node dependency is included in editor initial config.`);
@@ -679,15 +778,19 @@ function registerMarkdownShortcuts(editor, transformers = TRANSFORMERS) {
679
778
  }
680
779
  }
681
780
  }
781
+
682
782
  const transform = (parentNode, anchorNode, anchorOffset) => {
683
783
  if (runElementTransformers(parentNode, anchorNode, anchorOffset, byType.element)) {
684
784
  return;
685
785
  }
786
+
686
787
  if (runTextMatchTransformers(anchorNode, anchorOffset, textMatchTransformersIndex)) {
687
788
  return;
688
789
  }
790
+
689
791
  runTextFormatTransformers(anchorNode, anchorOffset, textFormatTransformersIndex);
690
792
  };
793
+
691
794
  return editor.registerUpdateListener(({
692
795
  tags,
693
796
  dirtyLeaves,
@@ -698,26 +801,35 @@ function registerMarkdownShortcuts(editor, transformers = TRANSFORMERS) {
698
801
  if (tags.has('historic')) {
699
802
  return;
700
803
  }
804
+
701
805
  const selection = editorState.read(lexical.$getSelection);
702
806
  const prevSelection = prevEditorState.read(lexical.$getSelection);
807
+
703
808
  if (!lexical.$isRangeSelection(prevSelection) || !lexical.$isRangeSelection(selection) || !selection.isCollapsed()) {
704
809
  return;
705
810
  }
811
+
706
812
  const anchorKey = selection.anchor.key;
707
813
  const anchorOffset = selection.anchor.offset;
814
+
708
815
  const anchorNode = editorState._nodeMap.get(anchorKey);
816
+
709
817
  if (!lexical.$isTextNode(anchorNode) || !dirtyLeaves.has(anchorKey) || anchorOffset !== 1 && anchorOffset !== prevSelection.anchor.offset + 1) {
710
818
  return;
711
819
  }
820
+
712
821
  editor.update(() => {
713
822
  // Markdown is not available inside code
714
823
  if (anchorNode.hasFormat('code')) {
715
824
  return;
716
825
  }
826
+
717
827
  const parentNode = anchorNode.getParent();
828
+
718
829
  if (parentNode === null || code.$isCodeNode(parentNode)) {
719
830
  return;
720
831
  }
832
+
721
833
  transform(parentNode, anchorNode, selection.anchor.offset);
722
834
  });
723
835
  });
@@ -730,6 +842,7 @@ function registerMarkdownShortcuts(editor, transformers = TRANSFORMERS) {
730
842
  * LICENSE file in the root directory of this source tree.
731
843
  *
732
844
  */
845
+
733
846
  const createBlockNode = createNode => {
734
847
  return (parentNode, children, match) => {
735
848
  const node = createNode(match);
@@ -737,15 +850,17 @@ const createBlockNode = createNode => {
737
850
  parentNode.replace(node);
738
851
  node.select(0, 0);
739
852
  };
740
- };
741
-
742
- // Amount of spaces that define indentation level
853
+ }; // Amount of spaces that define indentation level
743
854
  // TODO: should be an option
855
+
856
+
744
857
  const LIST_INDENT_SIZE = 4;
858
+
745
859
  const listReplace = listType => {
746
860
  return (parentNode, children, match) => {
747
861
  const previousNode = parentNode.getPreviousSibling();
748
862
  const listItem = list.$createListItemNode(listType === 'check' ? match[3] === 'x' : undefined);
863
+
749
864
  if (list.$isListNode(previousNode) && previousNode.getListType() === listType) {
750
865
  previousNode.append(listItem);
751
866
  parentNode.remove();
@@ -754,27 +869,33 @@ const listReplace = listType => {
754
869
  list$1.append(listItem);
755
870
  parentNode.replace(list$1);
756
871
  }
872
+
757
873
  listItem.append(...children);
758
874
  listItem.select(0, 0);
759
875
  const indent = Math.floor(match[1].length / LIST_INDENT_SIZE);
876
+
760
877
  if (indent) {
761
878
  listItem.setIndent(indent);
762
879
  }
763
880
  };
764
881
  };
882
+
765
883
  const listExport = (listNode, exportChildren, depth) => {
766
884
  const output = [];
767
885
  const children = listNode.getChildren();
768
886
  let index = 0;
887
+
769
888
  for (const listItemNode of children) {
770
889
  if (list.$isListItemNode(listItemNode)) {
771
890
  if (listItemNode.getChildrenSize() === 1) {
772
891
  const firstChild = listItemNode.getFirstChild();
892
+
773
893
  if (list.$isListNode(firstChild)) {
774
894
  output.push(listExport(firstChild, exportChildren, depth + 1));
775
895
  continue;
776
896
  }
777
897
  }
898
+
778
899
  const indent = ' '.repeat(depth * LIST_INDENT_SIZE);
779
900
  const listType = listNode.getListType();
780
901
  const prefix = listType === 'number' ? `${listNode.getStart() + index}. ` : listType === 'check' ? `- [${listItemNode.getChecked() ? 'x' : ' '}] ` : '- ';
@@ -782,14 +903,17 @@ const listExport = (listNode, exportChildren, depth) => {
782
903
  index++;
783
904
  }
784
905
  }
906
+
785
907
  return output.join('\n');
786
908
  };
909
+
787
910
  const HEADING = {
788
911
  dependencies: [richText.HeadingNode],
789
912
  export: (node, exportChildren) => {
790
913
  if (!richText.$isHeadingNode(node)) {
791
914
  return null;
792
915
  }
916
+
793
917
  const level = Number(node.getTag().slice(1));
794
918
  return '#'.repeat(level) + ' ' + exportChildren(node);
795
919
  },
@@ -806,17 +930,21 @@ const QUOTE = {
806
930
  if (!richText.$isQuoteNode(node)) {
807
931
  return null;
808
932
  }
933
+
809
934
  const lines = exportChildren(node).split('\n');
810
935
  const output = [];
936
+
811
937
  for (const line of lines) {
812
938
  output.push('> ' + line);
813
939
  }
940
+
814
941
  return output.join('\n');
815
942
  },
816
943
  regExp: /^>\s/,
817
944
  replace: (parentNode, children, _match, isImport) => {
818
945
  if (isImport) {
819
946
  const previousNode = parentNode.getPreviousSibling();
947
+
820
948
  if (richText.$isQuoteNode(previousNode)) {
821
949
  previousNode.splice(previousNode.getChildrenSize(), 0, [lexical.$createLineBreakNode(), ...children]);
822
950
  previousNode.select(0, 0);
@@ -824,6 +952,7 @@ const QUOTE = {
824
952
  return;
825
953
  }
826
954
  }
955
+
827
956
  const node = richText.$createQuoteNode();
828
957
  node.append(...children);
829
958
  parentNode.replace(node);
@@ -837,6 +966,7 @@ const CODE = {
837
966
  if (!code.$isCodeNode(node)) {
838
967
  return null;
839
968
  }
969
+
840
970
  const textContent = node.getTextContent();
841
971
  return '```' + (node.getLanguage() || '') + (textContent ? '\n' + textContent : '') + '\n' + '```';
842
972
  },
@@ -920,22 +1050,22 @@ const ITALIC_UNDERSCORE = {
920
1050
  intraword: false,
921
1051
  tag: '_',
922
1052
  type: 'text-format'
923
- };
924
-
925
- // Order of text transformers matters:
1053
+ }; // Order of text transformers matters:
926
1054
  //
927
1055
  // - code should go first as it prevents any transformations inside
928
1056
  // - then longer tags match (e.g. ** or __ should go before * or _)
1057
+
929
1058
  const LINK = {
930
1059
  dependencies: [link.LinkNode],
931
1060
  export: (node, exportChildren, exportFormat) => {
932
1061
  if (!link.$isLinkNode(node)) {
933
1062
  return null;
934
1063
  }
1064
+
935
1065
  const linkContent = `[${node.getTextContent()}](${node.getURL()})`;
936
- const firstChild = node.getFirstChild();
937
- // Add text styles only if link has single text node inside. If it's more
1066
+ const firstChild = node.getFirstChild(); // Add text styles only if link has single text node inside. If it's more
938
1067
  // then one we ignore it as markdown does not support nested styles for links
1068
+
939
1069
  if (node.getChildrenSize() === 1 && lexical.$isTextNode(firstChild)) {
940
1070
  return exportFormat(firstChild, linkContent);
941
1071
  } else {
@@ -957,19 +1087,20 @@ const LINK = {
957
1087
  };
958
1088
 
959
1089
  /** @module @lexical/markdown */
960
- const ELEMENT_TRANSFORMERS = [HEADING, QUOTE, CODE, UNORDERED_LIST, ORDERED_LIST];
961
-
962
- // Order of text format transformers matters:
1090
+ const ELEMENT_TRANSFORMERS = [HEADING, QUOTE, CODE, UNORDERED_LIST, ORDERED_LIST]; // Order of text format transformers matters:
963
1091
  //
964
1092
  // - code should go first as it prevents any transformations inside
965
1093
  // - then longer tags match (e.g. ** or __ should go before * or _)
1094
+
966
1095
  const TEXT_FORMAT_TRANSFORMERS = [INLINE_CODE, BOLD_ITALIC_STAR, BOLD_ITALIC_UNDERSCORE, BOLD_STAR, BOLD_UNDERSCORE, HIGHLIGHT, ITALIC_STAR, ITALIC_UNDERSCORE, STRIKETHROUGH];
967
1096
  const TEXT_MATCH_TRANSFORMERS = [LINK];
968
1097
  const TRANSFORMERS = [...ELEMENT_TRANSFORMERS, ...TEXT_FORMAT_TRANSFORMERS, ...TEXT_MATCH_TRANSFORMERS];
1098
+
969
1099
  function $convertFromMarkdownString(markdown, transformers = TRANSFORMERS) {
970
1100
  const importMarkdown = createMarkdownImport(transformers);
971
1101
  return importMarkdown(markdown);
972
1102
  }
1103
+
973
1104
  function $convertToMarkdownString(transformers = TRANSFORMERS) {
974
1105
  const exportMarkdown = createMarkdownExport(transformers);
975
1106
  return exportMarkdown();
package/package.json CHANGED
@@ -8,18 +8,18 @@
8
8
  "markdown"
9
9
  ],
10
10
  "license": "MIT",
11
- "version": "0.8.1",
11
+ "version": "0.9.0",
12
12
  "main": "LexicalMarkdown.js",
13
13
  "peerDependencies": {
14
- "lexical": "0.8.1"
14
+ "lexical": "0.9.0"
15
15
  },
16
16
  "dependencies": {
17
- "@lexical/utils": "0.8.1",
18
- "@lexical/code": "0.8.1",
19
- "@lexical/text": "0.8.1",
20
- "@lexical/rich-text": "0.8.1",
21
- "@lexical/list": "0.8.1",
22
- "@lexical/link": "0.8.1"
17
+ "@lexical/utils": "0.9.0",
18
+ "@lexical/code": "0.9.0",
19
+ "@lexical/text": "0.9.0",
20
+ "@lexical/rich-text": "0.9.0",
21
+ "@lexical/list": "0.9.0",
22
+ "@lexical/link": "0.9.0"
23
23
  },
24
24
  "repository": {
25
25
  "type": "git",