@lexical/code 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.
@@ -5,7 +5,7 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  *
7
7
  */
8
- import { type EditorConfig, type LexicalNode, type NodeKey, type SerializedTextNode, type Spread, TextNode, ElementNode } from 'lexical';
8
+ import type { EditorConfig, LexicalNode, NodeKey, SerializedTextNode, Spread } from 'lexical';
9
9
  import 'prismjs/components/prism-clike';
10
10
  import 'prismjs/components/prism-javascript';
11
11
  import 'prismjs/components/prism-markup';
@@ -20,6 +20,7 @@ import 'prismjs/components/prism-swift';
20
20
  import 'prismjs/components/prism-typescript';
21
21
  import 'prismjs/components/prism-java';
22
22
  import 'prismjs/components/prism-cpp';
23
+ import { ElementNode, TextNode } from 'lexical';
23
24
  export declare const DEFAULT_CODE_LANGUAGE = "javascript";
24
25
  declare type SerializedCodeHighlightNode = Spread<{
25
26
  highlightType: string | null | undefined;
@@ -31,50 +31,60 @@ var lexical = require('lexical');
31
31
  * LICENSE file in the root directory of this source tree.
32
32
  *
33
33
  */
34
+
34
35
  const mapToPrismLanguage = language => {
35
36
  // eslint-disable-next-line no-prototype-builtins
36
37
  return language != null && Prism.languages.hasOwnProperty(language) ? language : undefined;
37
38
  };
39
+
38
40
  function hasChildDOMNodeTag(node, tagName) {
39
41
  for (const child of node.childNodes) {
40
42
  if (utils.isHTMLElement(child) && child.tagName === tagName) {
41
43
  return true;
42
44
  }
45
+
43
46
  hasChildDOMNodeTag(child, tagName);
44
47
  }
48
+
45
49
  return false;
46
50
  }
47
- const LANGUAGE_DATA_ATTRIBUTE = 'data-highlight-language';
48
51
 
52
+ const LANGUAGE_DATA_ATTRIBUTE = 'data-highlight-language';
49
53
  /** @noInheritDoc */
54
+
50
55
  class CodeNode extends lexical.ElementNode {
51
56
  /** @internal */
52
-
53
57
  static getType() {
54
58
  return 'code';
55
59
  }
60
+
56
61
  static clone(node) {
57
62
  return new CodeNode(node.__language, node.__key);
58
63
  }
64
+
59
65
  constructor(language, key) {
60
66
  super(key);
61
67
  this.__language = mapToPrismLanguage(language);
62
- }
68
+ } // View
69
+
63
70
 
64
- // View
65
71
  createDOM(config) {
66
72
  const element = document.createElement('code');
67
73
  utils.addClassNamesToElement(element, config.theme.code);
68
74
  element.setAttribute('spellcheck', 'false');
69
75
  const language = this.getLanguage();
76
+
70
77
  if (language) {
71
78
  element.setAttribute(LANGUAGE_DATA_ATTRIBUTE, language);
72
79
  }
80
+
73
81
  return element;
74
82
  }
83
+
75
84
  updateDOM(prevNode, dom, config) {
76
85
  const language = this.__language;
77
86
  const prevLanguage = prevNode.__language;
87
+
78
88
  if (language) {
79
89
  if (language !== prevLanguage) {
80
90
  dom.setAttribute(LANGUAGE_DATA_ATTRIBUTE, language);
@@ -82,8 +92,10 @@ class CodeNode extends lexical.ElementNode {
82
92
  } else if (prevLanguage) {
83
93
  dom.removeAttribute(LANGUAGE_DATA_ATTRIBUTE);
84
94
  }
95
+
85
96
  return false;
86
97
  }
98
+
87
99
  static importDOM() {
88
100
  return {
89
101
  // Typically <pre> is used for code blocks, and <code> for inline code styles
@@ -105,26 +117,29 @@ class CodeNode extends lexical.ElementNode {
105
117
  priority: 0
106
118
  }),
107
119
  table: node => {
108
- const table = node;
109
- // domNode is a <table> since we matched it by nodeName
120
+ const table = node; // domNode is a <table> since we matched it by nodeName
121
+
110
122
  if (isGitHubCodeTable(table)) {
111
123
  return {
112
124
  conversion: convertTableElement,
113
125
  priority: 3
114
126
  };
115
127
  }
128
+
116
129
  return null;
117
130
  },
118
131
  td: node => {
119
132
  // element is a <td> since we matched it by nodeName
120
133
  const td = node;
121
134
  const table = td.closest('table');
135
+
122
136
  if (isGitHubCodeCell(td)) {
123
137
  return {
124
138
  conversion: convertTableCellElement,
125
139
  priority: 3
126
140
  };
127
141
  }
142
+
128
143
  if (table && isGitHubCodeTable(table)) {
129
144
  // Return a no-op if it's a table cell in a code table, but not a code line.
130
145
  // Otherwise it'll fall back to the T
@@ -133,22 +148,26 @@ class CodeNode extends lexical.ElementNode {
133
148
  priority: 3
134
149
  };
135
150
  }
151
+
136
152
  return null;
137
153
  },
138
154
  tr: node => {
139
155
  // element is a <tr> since we matched it by nodeName
140
156
  const tr = node;
141
157
  const table = tr.closest('table');
158
+
142
159
  if (table && isGitHubCodeTable(table)) {
143
160
  return {
144
161
  conversion: convertCodeNoop,
145
162
  priority: 3
146
163
  };
147
164
  }
165
+
148
166
  return null;
149
167
  }
150
168
  };
151
169
  }
170
+
152
171
  static importJSON(serializedNode) {
153
172
  const node = $createCodeNode(serializedNode.language);
154
173
  node.setFormat(serializedNode.format);
@@ -156,38 +175,42 @@ class CodeNode extends lexical.ElementNode {
156
175
  node.setDirection(serializedNode.direction);
157
176
  return node;
158
177
  }
178
+
159
179
  exportJSON() {
160
- return {
161
- ...super.exportJSON(),
180
+ return { ...super.exportJSON(),
162
181
  language: this.getLanguage(),
163
182
  type: 'code',
164
183
  version: 1
165
184
  };
166
- }
185
+ } // Mutation
186
+
167
187
 
168
- // Mutation
169
188
  insertNewAfter(selection, restoreSelection = true) {
170
189
  const children = this.getChildren();
171
190
  const childrenLength = children.length;
191
+
172
192
  if (childrenLength >= 2 && children[childrenLength - 1].getTextContent() === '\n' && children[childrenLength - 2].getTextContent() === '\n' && selection.isCollapsed() && selection.anchor.key === this.__key && selection.anchor.offset === childrenLength) {
173
193
  children[childrenLength - 1].remove();
174
194
  children[childrenLength - 2].remove();
175
195
  const newElement = lexical.$createParagraphNode();
176
196
  this.insertAfter(newElement, restoreSelection);
177
197
  return newElement;
178
- }
179
-
180
- // If the selection is within the codeblock, find all leading tabs and
198
+ } // If the selection is within the codeblock, find all leading tabs and
181
199
  // spaces of the current line. Create a new line that has all those
182
200
  // tabs and spaces, such that leading indentation is preserved.
201
+
202
+
183
203
  const anchor = selection.anchor.getNode();
184
204
  const firstNode = getFirstCodeHighlightNodeOfLine(anchor);
205
+
185
206
  if (firstNode != null) {
186
207
  let leadingWhitespace = 0;
187
208
  const firstNodeText = firstNode.getTextContent();
209
+
188
210
  while (leadingWhitespace < firstNodeText.length && /[\t ]/.test(firstNodeText[leadingWhitespace])) {
189
211
  leadingWhitespace += 1;
190
212
  }
213
+
191
214
  if (leadingWhitespace > 0) {
192
215
  const whitespace = firstNodeText.substring(0, leadingWhitespace);
193
216
  const indentedChild = $createCodeHighlightNode(whitespace);
@@ -197,18 +220,24 @@ class CodeNode extends lexical.ElementNode {
197
220
  return indentedChild;
198
221
  }
199
222
  }
223
+
200
224
  return null;
201
225
  }
226
+
202
227
  canInsertTab() {
203
228
  const selection = lexical.$getSelection();
229
+
204
230
  if (!lexical.$isRangeSelection(selection) || !selection.isCollapsed()) {
205
231
  return false;
206
232
  }
233
+
207
234
  return true;
208
235
  }
236
+
209
237
  canIndent() {
210
238
  return false;
211
239
  }
240
+
212
241
  collapseAtStart() {
213
242
  const paragraph = lexical.$createParagraphNode();
214
243
  const children = this.getChildren();
@@ -216,13 +245,16 @@ class CodeNode extends lexical.ElementNode {
216
245
  this.replace(paragraph);
217
246
  return true;
218
247
  }
248
+
219
249
  setLanguage(language) {
220
250
  const writable = this.getWritable();
221
251
  writable.__language = mapToPrismLanguage(language);
222
252
  }
253
+
223
254
  getLanguage() {
224
255
  return this.getLatest().__language;
225
256
  }
257
+
226
258
  }
227
259
  function $createCodeNode(language) {
228
260
  return lexical.$applyNodeReplacement(new CodeNode(language));
@@ -230,44 +262,53 @@ function $createCodeNode(language) {
230
262
  function $isCodeNode(node) {
231
263
  return node instanceof CodeNode;
232
264
  }
265
+
233
266
  function convertPreElement(domNode) {
234
267
  return {
235
268
  node: $createCodeNode(),
236
269
  preformatted: true
237
270
  };
238
271
  }
272
+
239
273
  function convertDivElement(domNode) {
240
274
  // domNode is a <div> since we matched it by nodeName
241
275
  const div = domNode;
242
276
  const isCode = isCodeElement(div);
277
+
243
278
  if (!isCode && !isCodeChildElement(div)) {
244
279
  return {
245
280
  node: null
246
281
  };
247
282
  }
283
+
248
284
  return {
249
285
  after: childLexicalNodes => {
250
286
  const domParent = domNode.parentNode;
287
+
251
288
  if (domParent != null && domNode !== domParent.lastChild) {
252
289
  childLexicalNodes.push(lexical.$createLineBreakNode());
253
290
  }
291
+
254
292
  return childLexicalNodes;
255
293
  },
256
294
  node: isCode ? $createCodeNode() : null,
257
295
  preformatted: isCode
258
296
  };
259
297
  }
298
+
260
299
  function convertTableElement() {
261
300
  return {
262
301
  node: $createCodeNode(),
263
302
  preformatted: true
264
303
  };
265
304
  }
305
+
266
306
  function convertCodeNoop() {
267
307
  return {
268
308
  node: null
269
309
  };
270
310
  }
311
+
271
312
  function convertTableCellElement(domNode) {
272
313
  // domNode is a <td> since we matched it by nodeName
273
314
  const cell = domNode;
@@ -277,27 +318,35 @@ function convertTableCellElement(domNode) {
277
318
  // Append newline between code lines
278
319
  childLexicalNodes.push(lexical.$createLineBreakNode());
279
320
  }
321
+
280
322
  return childLexicalNodes;
281
323
  },
282
324
  node: null
283
325
  };
284
326
  }
327
+
285
328
  function isCodeElement(div) {
286
329
  return div.style.fontFamily.match('monospace') !== null;
287
330
  }
331
+
288
332
  function isCodeChildElement(node) {
289
333
  let parent = node.parentElement;
334
+
290
335
  while (parent !== null) {
291
336
  if (isCodeElement(parent)) {
292
337
  return true;
293
338
  }
339
+
294
340
  parent = parent.parentElement;
295
341
  }
342
+
296
343
  return false;
297
344
  }
345
+
298
346
  function isGitHubCodeCell(cell) {
299
347
  return cell.classList.contains('js-file-line');
300
348
  }
349
+
301
350
  function isGitHubCodeTable(table) {
302
351
  return table.classList.contains('js-file-line-container');
303
352
  }
@@ -343,52 +392,60 @@ function normalizeCodeLang(lang) {
343
392
  }
344
393
  function getLanguageFriendlyName(lang) {
345
394
  const _lang = normalizeCodeLang(lang);
395
+
346
396
  return CODE_LANGUAGE_FRIENDLY_NAME_MAP[_lang] || _lang;
347
397
  }
348
398
  const getDefaultCodeLanguage = () => DEFAULT_CODE_LANGUAGE;
349
- const getCodeLanguages = () => Object.keys(Prism.languages).filter(
350
- // Prism has several language helpers mixed into languages object
399
+ const getCodeLanguages = () => Object.keys(Prism.languages).filter( // Prism has several language helpers mixed into languages object
351
400
  // so filtering them out here to get langs list
352
401
  language => typeof Prism.languages[language] !== 'function').sort();
353
-
354
402
  /** @noInheritDoc */
403
+
355
404
  class CodeHighlightNode extends lexical.TextNode {
356
405
  /** @internal */
357
-
358
406
  constructor(text, highlightType, key) {
359
407
  super(text, key);
360
408
  this.__highlightType = highlightType;
361
409
  }
410
+
362
411
  static getType() {
363
412
  return 'code-highlight';
364
413
  }
414
+
365
415
  static clone(node) {
366
416
  return new CodeHighlightNode(node.__text, node.__highlightType || undefined, node.__key);
367
417
  }
418
+
368
419
  getHighlightType() {
369
420
  const self = this.getLatest();
370
421
  return self.__highlightType;
371
422
  }
423
+
372
424
  createDOM(config) {
373
425
  const element = super.createDOM(config);
374
426
  const className = getHighlightThemeClass(config.theme, this.__highlightType);
375
427
  utils.addClassNamesToElement(element, className);
376
428
  return element;
377
429
  }
430
+
378
431
  updateDOM(prevNode, dom, config) {
379
432
  const update = super.updateDOM(prevNode, dom, config);
380
433
  const prevClassName = getHighlightThemeClass(config.theme, prevNode.__highlightType);
381
434
  const nextClassName = getHighlightThemeClass(config.theme, this.__highlightType);
435
+
382
436
  if (prevClassName !== nextClassName) {
383
437
  if (prevClassName) {
384
438
  utils.removeClassNamesFromElement(dom, prevClassName);
385
439
  }
440
+
386
441
  if (nextClassName) {
387
442
  utils.addClassNamesToElement(dom, nextClassName);
388
443
  }
389
444
  }
445
+
390
446
  return update;
391
447
  }
448
+
392
449
  static importJSON(serializedNode) {
393
450
  const node = $createCodeHighlightNode(serializedNode.text, serializedNode.highlightType);
394
451
  node.setFormat(serializedNode.format);
@@ -397,29 +454,34 @@ class CodeHighlightNode extends lexical.TextNode {
397
454
  node.setStyle(serializedNode.style);
398
455
  return node;
399
456
  }
457
+
400
458
  exportJSON() {
401
- return {
402
- ...super.exportJSON(),
459
+ return { ...super.exportJSON(),
403
460
  highlightType: this.getHighlightType(),
404
461
  type: 'code-highlight',
405
462
  version: 1
406
463
  };
407
- }
464
+ } // Prevent formatting (bold, underline, etc)
465
+
408
466
 
409
- // Prevent formatting (bold, underline, etc)
410
467
  setFormat(format) {
411
468
  return this;
412
469
  }
470
+
413
471
  isParentRequired() {
414
472
  return true;
415
473
  }
474
+
416
475
  createParentElementNode() {
417
476
  return $createCodeNode();
418
477
  }
478
+
419
479
  }
480
+
420
481
  function getHighlightThemeClass(theme, highlightType) {
421
482
  return highlightType && theme && theme.codeHighlight && theme.codeHighlight[highlightType];
422
483
  }
484
+
423
485
  function $createCodeHighlightNode(text, highlightType) {
424
486
  return lexical.$applyNodeReplacement(new CodeHighlightNode(text, highlightType));
425
487
  }
@@ -430,30 +492,38 @@ function getFirstCodeHighlightNodeOfLine(anchor) {
430
492
  let currentNode = null;
431
493
  const previousSiblings = anchor.getPreviousSiblings();
432
494
  previousSiblings.push(anchor);
495
+
433
496
  while (previousSiblings.length > 0) {
434
497
  const node = previousSiblings.pop();
498
+
435
499
  if ($isCodeHighlightNode(node)) {
436
500
  currentNode = node;
437
501
  }
502
+
438
503
  if (lexical.$isLineBreakNode(node)) {
439
504
  break;
440
505
  }
441
506
  }
507
+
442
508
  return currentNode;
443
509
  }
444
510
  function getLastCodeHighlightNodeOfLine(anchor) {
445
511
  let currentNode = null;
446
512
  const nextSiblings = anchor.getNextSiblings();
447
513
  nextSiblings.unshift(anchor);
514
+
448
515
  while (nextSiblings.length > 0) {
449
516
  const node = nextSiblings.shift();
517
+
450
518
  if ($isCodeHighlightNode(node)) {
451
519
  currentNode = node;
452
520
  }
521
+
453
522
  if (lexical.$isLineBreakNode(node)) {
454
523
  break;
455
524
  }
456
525
  }
526
+
457
527
  return currentNode;
458
528
  }
459
529
 
@@ -466,19 +536,25 @@ function getLastCodeHighlightNodeOfLine(anchor) {
466
536
  */
467
537
  const PrismTokenizer = {
468
538
  defaultLanguage: DEFAULT_CODE_LANGUAGE,
539
+
469
540
  tokenize(code, language) {
470
541
  return Prism.tokenize(code, Prism.languages[language || ''] || Prism.languages[this.defaultLanguage]);
471
542
  }
543
+
472
544
  };
545
+
473
546
  function isSpaceOrTabChar(char) {
474
547
  return char === ' ' || char === '\t';
475
548
  }
549
+
476
550
  function findFirstNotSpaceOrTabCharAtText(text, isForward) {
477
551
  const length = text.length;
478
552
  let offset = -1;
553
+
479
554
  if (isForward) {
480
555
  for (let i = 0; i < length; i++) {
481
556
  const char = text[i];
557
+
482
558
  if (!isSpaceOrTabChar(char)) {
483
559
  offset = i;
484
560
  break;
@@ -487,51 +563,64 @@ function findFirstNotSpaceOrTabCharAtText(text, isForward) {
487
563
  } else {
488
564
  for (let i = length - 1; i > -1; i--) {
489
565
  const char = text[i];
566
+
490
567
  if (!isSpaceOrTabChar(char)) {
491
568
  offset = i;
492
569
  break;
493
570
  }
494
571
  }
495
572
  }
573
+
496
574
  return offset;
497
575
  }
576
+
498
577
  function getStartOfCodeInLine(anchor) {
499
578
  let currentNode = null;
500
579
  let currentNodeOffset = -1;
501
580
  const previousSiblings = anchor.getPreviousSiblings();
502
581
  previousSiblings.push(anchor);
582
+
503
583
  while (previousSiblings.length > 0) {
504
584
  const node = previousSiblings.pop();
585
+
505
586
  if ($isCodeHighlightNode(node)) {
506
587
  const text = node.getTextContent();
507
588
  const offset = findFirstNotSpaceOrTabCharAtText(text, true);
589
+
508
590
  if (offset !== -1) {
509
591
  currentNode = node;
510
592
  currentNodeOffset = offset;
511
593
  }
512
594
  }
595
+
513
596
  if (lexical.$isLineBreakNode(node)) {
514
597
  break;
515
598
  }
516
599
  }
600
+
517
601
  if (currentNode === null) {
518
602
  const nextSiblings = anchor.getNextSiblings();
603
+
519
604
  while (nextSiblings.length > 0) {
520
605
  const node = nextSiblings.shift();
606
+
521
607
  if ($isCodeHighlightNode(node)) {
522
608
  const text = node.getTextContent();
523
609
  const offset = findFirstNotSpaceOrTabCharAtText(text, true);
610
+
524
611
  if (offset !== -1) {
525
612
  currentNode = node;
526
613
  currentNodeOffset = offset;
527
614
  break;
528
615
  }
529
616
  }
617
+
530
618
  if (lexical.$isLineBreakNode(node)) {
531
619
  break;
532
620
  }
533
621
  }
534
622
  }
623
+
535
624
  return {
536
625
  node: currentNode,
537
626
  offset: currentNodeOffset
@@ -542,47 +631,59 @@ function getEndOfCodeInLine(anchor) {
542
631
  let currentNodeOffset = -1;
543
632
  const nextSiblings = anchor.getNextSiblings();
544
633
  nextSiblings.unshift(anchor);
634
+
545
635
  while (nextSiblings.length > 0) {
546
636
  const node = nextSiblings.shift();
637
+
547
638
  if ($isCodeHighlightNode(node)) {
548
639
  const text = node.getTextContent();
549
640
  const offset = findFirstNotSpaceOrTabCharAtText(text, false);
641
+
550
642
  if (offset !== -1) {
551
643
  currentNode = node;
552
644
  currentNodeOffset = offset + 1;
553
645
  }
554
646
  }
647
+
555
648
  if (lexical.$isLineBreakNode(node)) {
556
649
  break;
557
650
  }
558
651
  }
652
+
559
653
  if (currentNode === null) {
560
654
  const previousSiblings = anchor.getPreviousSiblings();
655
+
561
656
  while (previousSiblings.length > 0) {
562
657
  const node = previousSiblings.pop();
658
+
563
659
  if ($isCodeHighlightNode(node)) {
564
660
  const text = node.getTextContent();
565
661
  const offset = findFirstNotSpaceOrTabCharAtText(text, false);
662
+
566
663
  if (offset !== -1) {
567
664
  currentNode = node;
568
665
  currentNodeOffset = offset + 1;
569
666
  break;
570
667
  }
571
668
  }
669
+
572
670
  if (lexical.$isLineBreakNode(node)) {
573
671
  break;
574
672
  }
575
673
  }
576
674
  }
675
+
577
676
  return {
578
677
  node: currentNode,
579
678
  offset: currentNodeOffset
580
679
  };
581
680
  }
681
+
582
682
  function textNodeTransform(node, editor, tokenizer) {
583
683
  // Since CodeNode has flat children structure we only need to check
584
684
  // if node's parent is a code node and run highlighting if so
585
685
  const parentNode = node.getParent();
686
+
586
687
  if ($isCodeNode(parentNode)) {
587
688
  codeNodeTransform(parentNode, editor, tokenizer);
588
689
  } else if ($isCodeHighlightNode(node)) {
@@ -591,31 +692,35 @@ function textNodeTransform(node, editor, tokenizer) {
591
692
  node.replace(lexical.$createTextNode(node.__text));
592
693
  }
593
694
  }
695
+
594
696
  function updateCodeGutter(node, editor) {
595
697
  const codeElement = editor.getElementByKey(node.getKey());
698
+
596
699
  if (codeElement === null) {
597
700
  return;
598
701
  }
702
+
599
703
  const children = node.getChildren();
600
- const childrenLength = children.length;
601
- // @ts-ignore: internal field
704
+ const childrenLength = children.length; // @ts-ignore: internal field
705
+
602
706
  if (childrenLength === codeElement.__cachedChildrenLength) {
603
707
  // Avoid updating the attribute if the children length hasn't changed.
604
708
  return;
605
- }
606
- // @ts-ignore:: internal field
709
+ } // @ts-ignore:: internal field
710
+
711
+
607
712
  codeElement.__cachedChildrenLength = childrenLength;
608
713
  let gutter = '1';
609
714
  let count = 1;
715
+
610
716
  for (let i = 0; i < childrenLength; i++) {
611
717
  if (lexical.$isLineBreakNode(children[i])) {
612
718
  gutter += '\n' + ++count;
613
719
  }
614
720
  }
615
- codeElement.setAttribute('data-gutter', gutter);
616
- }
617
721
 
618
- // Using `skipTransforms` to prevent extra transforms since reformatting the code
722
+ codeElement.setAttribute('data-gutter', gutter);
723
+ } // Using `skipTransforms` to prevent extra transforms since reformatting the code
619
724
  // will not affect code block content itself.
620
725
  //
621
726
  // Using extra cache (`nodesCurrentlyHighlighting`) since both CodeNode and CodeHighlightNode
@@ -623,28 +728,33 @@ function updateCodeGutter(node, editor) {
623
728
  // in both cases we'll rerun whole reformatting over CodeNode, which is redundant.
624
729
  // Especially when pasting code into CodeBlock.
625
730
 
731
+
626
732
  const nodesCurrentlyHighlighting = new Set();
733
+
627
734
  function codeNodeTransform(node, editor, tokenizer) {
628
735
  const nodeKey = node.getKey();
736
+
629
737
  if (nodesCurrentlyHighlighting.has(nodeKey)) {
630
738
  return;
631
739
  }
632
- nodesCurrentlyHighlighting.add(nodeKey);
633
740
 
634
- // When new code block inserted it might not have language selected
741
+ nodesCurrentlyHighlighting.add(nodeKey); // When new code block inserted it might not have language selected
742
+
635
743
  if (node.getLanguage() === undefined) {
636
744
  node.setLanguage(tokenizer.defaultLanguage);
637
- }
638
-
639
- // Using nested update call to pass `skipTransforms` since we don't want
745
+ } // Using nested update call to pass `skipTransforms` since we don't want
640
746
  // each individual codehighlight node to be transformed again as it's already
641
747
  // in its final state
748
+
749
+
642
750
  editor.update(() => {
643
751
  updateAndRetainSelection(nodeKey, () => {
644
752
  const currentNode = lexical.$getNodeByKey(nodeKey);
753
+
645
754
  if (!$isCodeNode(currentNode) || !currentNode.isAttached()) {
646
755
  return false;
647
756
  }
757
+
648
758
  const code = currentNode.getTextContent();
649
759
  const tokens = tokenizer.tokenize(code, currentNode.getLanguage() || tokenizer.defaultLanguage);
650
760
  const highlightNodes = getHighlightNodes(tokens);
@@ -654,10 +764,12 @@ function codeNodeTransform(node, editor, tokenizer) {
654
764
  to,
655
765
  nodesForReplacement
656
766
  } = diffRange;
767
+
657
768
  if (from !== to || nodesForReplacement.length) {
658
769
  node.splice(from, to - from, nodesForReplacement);
659
770
  return true;
660
771
  }
772
+
661
773
  return false;
662
774
  });
663
775
  }, {
@@ -667,16 +779,20 @@ function codeNodeTransform(node, editor, tokenizer) {
667
779
  skipTransforms: true
668
780
  });
669
781
  }
782
+
670
783
  function getHighlightNodes(tokens) {
671
784
  const nodes = [];
672
785
  tokens.forEach(token => {
673
786
  if (typeof token === 'string') {
674
787
  const partials = token.split('\n');
788
+
675
789
  for (let i = 0; i < partials.length; i++) {
676
790
  const text = partials[i];
791
+
677
792
  if (text.length) {
678
793
  nodes.push($createCodeHighlightNode(text));
679
794
  }
795
+
680
796
  if (i < partials.length - 1) {
681
797
  nodes.push(lexical.$createLineBreakNode());
682
798
  }
@@ -685,6 +801,7 @@ function getHighlightNodes(tokens) {
685
801
  const {
686
802
  content
687
803
  } = token;
804
+
688
805
  if (typeof content === 'string') {
689
806
  nodes.push($createCodeHighlightNode(content, token.type));
690
807
  } else if (Array.isArray(content) && content.length === 1 && typeof content[0] === 'string') {
@@ -695,83 +812,98 @@ function getHighlightNodes(tokens) {
695
812
  }
696
813
  });
697
814
  return nodes;
698
- }
699
-
700
- // Wrapping update function into selection retainer, that tries to keep cursor at the same
815
+ } // Wrapping update function into selection retainer, that tries to keep cursor at the same
701
816
  // position as before.
817
+
818
+
702
819
  function updateAndRetainSelection(nodeKey, updateFn) {
703
820
  const node = lexical.$getNodeByKey(nodeKey);
821
+
704
822
  if (!$isCodeNode(node) || !node.isAttached()) {
705
823
  return;
706
824
  }
707
- const selection = lexical.$getSelection();
708
- // If it's not range selection (or null selection) there's no need to change it,
825
+
826
+ const selection = lexical.$getSelection(); // If it's not range selection (or null selection) there's no need to change it,
709
827
  // but we can still run highlighting logic
828
+
710
829
  if (!lexical.$isRangeSelection(selection)) {
711
830
  updateFn();
712
831
  return;
713
832
  }
833
+
714
834
  const anchor = selection.anchor;
715
835
  const anchorOffset = anchor.offset;
716
836
  const isNewLineAnchor = anchor.type === 'element' && lexical.$isLineBreakNode(node.getChildAtIndex(anchor.offset - 1));
717
- let textOffset = 0;
837
+ let textOffset = 0; // Calculating previous text offset (all text node prior to anchor + anchor own text offset)
718
838
 
719
- // Calculating previous text offset (all text node prior to anchor + anchor own text offset)
720
839
  if (!isNewLineAnchor) {
721
840
  const anchorNode = anchor.getNode();
722
841
  textOffset = anchorOffset + anchorNode.getPreviousSiblings().reduce((offset, _node) => {
723
842
  return offset + _node.getTextContentSize();
724
843
  }, 0);
725
844
  }
845
+
726
846
  const hasChanges = updateFn();
847
+
727
848
  if (!hasChanges) {
728
849
  return;
729
- }
730
-
731
- // Non-text anchors only happen for line breaks, otherwise
850
+ } // Non-text anchors only happen for line breaks, otherwise
732
851
  // selection will be within text node (code highlight node)
852
+
853
+
733
854
  if (isNewLineAnchor) {
734
855
  anchor.getNode().select(anchorOffset, anchorOffset);
735
856
  return;
736
- }
737
-
738
- // If it was non-element anchor then we walk through child nodes
857
+ } // If it was non-element anchor then we walk through child nodes
739
858
  // and looking for a position of original text offset
859
+
860
+
740
861
  node.getChildren().some(_node => {
741
862
  const isText = lexical.$isTextNode(_node);
863
+
742
864
  if (isText || lexical.$isLineBreakNode(_node)) {
743
865
  const textContentSize = _node.getTextContentSize();
866
+
744
867
  if (isText && textContentSize >= textOffset) {
745
868
  _node.select(textOffset, textOffset);
869
+
746
870
  return true;
747
871
  }
872
+
748
873
  textOffset -= textContentSize;
749
874
  }
875
+
750
876
  return false;
751
877
  });
752
- }
753
-
754
- // Finds minimal diff range between two nodes lists. It returns from/to range boundaries of prevNodes
878
+ } // Finds minimal diff range between two nodes lists. It returns from/to range boundaries of prevNodes
755
879
  // that needs to be replaced with `nodes` (subset of nextNodes) to make prevNodes equal to nextNodes.
880
+
881
+
756
882
  function getDiffRange(prevNodes, nextNodes) {
757
883
  let leadingMatch = 0;
884
+
758
885
  while (leadingMatch < prevNodes.length) {
759
886
  if (!isEqual(prevNodes[leadingMatch], nextNodes[leadingMatch])) {
760
887
  break;
761
888
  }
889
+
762
890
  leadingMatch++;
763
891
  }
892
+
764
893
  const prevNodesLength = prevNodes.length;
765
894
  const nextNodesLength = nextNodes.length;
766
895
  const maxTrailingMatch = Math.min(prevNodesLength, nextNodesLength) - leadingMatch;
767
896
  let trailingMatch = 0;
897
+
768
898
  while (trailingMatch < maxTrailingMatch) {
769
899
  trailingMatch++;
900
+
770
901
  if (!isEqual(prevNodes[prevNodesLength - trailingMatch], nextNodes[nextNodesLength - trailingMatch])) {
771
902
  trailingMatch--;
772
903
  break;
773
904
  }
774
905
  }
906
+
775
907
  const from = leadingMatch;
776
908
  const to = prevNodesLength - trailingMatch;
777
909
  const nodesForReplacement = nextNodes.slice(leadingMatch, nextNodesLength - trailingMatch);
@@ -781,45 +913,59 @@ function getDiffRange(prevNodes, nextNodes) {
781
913
  to
782
914
  };
783
915
  }
916
+
784
917
  function isEqual(nodeA, nodeB) {
785
918
  // Only checking for code higlight nodes and linebreaks. If it's regular text node
786
919
  // returning false so that it's transformed into code highlight node
787
920
  if ($isCodeHighlightNode(nodeA) && $isCodeHighlightNode(nodeB)) {
788
921
  return nodeA.__text === nodeB.__text && nodeA.__highlightType === nodeB.__highlightType;
789
922
  }
923
+
790
924
  if (lexical.$isLineBreakNode(nodeA) && lexical.$isLineBreakNode(nodeB)) {
791
925
  return true;
792
926
  }
927
+
793
928
  return false;
794
929
  }
930
+
795
931
  function handleMultilineIndent(type) {
796
932
  const selection = lexical.$getSelection();
933
+
797
934
  if (!lexical.$isRangeSelection(selection) || selection.isCollapsed()) {
798
935
  return false;
799
- }
936
+ } // Only run multiline indent logic on selections exclusively composed of code highlights and linebreaks
937
+
800
938
 
801
- // Only run multiline indent logic on selections exclusively composed of code highlights and linebreaks
802
939
  const nodes = selection.getNodes();
940
+
803
941
  for (let i = 0; i < nodes.length; i++) {
804
942
  const node = nodes[i];
943
+
805
944
  if (!$isCodeHighlightNode(node) && !lexical.$isLineBreakNode(node)) {
806
945
  return false;
807
946
  }
808
947
  }
948
+
809
949
  const startOfLine = getFirstCodeHighlightNodeOfLine(nodes[0]);
950
+
810
951
  if (startOfLine != null) {
811
952
  doIndent(startOfLine, type);
812
953
  }
954
+
813
955
  for (let i = 1; i < nodes.length; i++) {
814
956
  const node = nodes[i];
957
+
815
958
  if (lexical.$isLineBreakNode(nodes[i - 1]) && $isCodeHighlightNode(node)) {
816
959
  doIndent(node, type);
817
960
  }
818
961
  }
962
+
819
963
  return true;
820
964
  }
965
+
821
966
  function doIndent(node, type) {
822
967
  const text = node.getTextContent();
968
+
823
969
  if (type === lexical.INDENT_CONTENT_COMMAND) {
824
970
  // If the codeblock node doesn't start with whitespace, we don't want to
825
971
  // naively prepend a '\t'; Prism will then mangle all of our nodes when
@@ -844,15 +990,17 @@ function doIndent(node, type) {
844
990
  }
845
991
  }
846
992
  }
993
+
847
994
  function handleShiftLines(type, event) {
848
995
  // We only care about the alt+arrow keys
849
996
  const selection = lexical.$getSelection();
997
+
850
998
  if (!lexical.$isRangeSelection(selection)) {
851
999
  return false;
852
- }
853
-
854
- // I'm not quite sure why, but it seems like calling anchor.getNode() collapses the selection here
1000
+ } // I'm not quite sure why, but it seems like calling anchor.getNode() collapses the selection here
855
1001
  // So first, get the anchor and the focus, then get their nodes
1002
+
1003
+
856
1004
  const {
857
1005
  anchor,
858
1006
  focus
@@ -861,19 +1009,21 @@ function handleShiftLines(type, event) {
861
1009
  const focusOffset = focus.offset;
862
1010
  const anchorNode = anchor.getNode();
863
1011
  const focusNode = focus.getNode();
864
- const arrowIsUp = type === lexical.KEY_ARROW_UP_COMMAND;
1012
+ const arrowIsUp = type === lexical.KEY_ARROW_UP_COMMAND; // Ensure the selection is within the codeblock
865
1013
 
866
- // Ensure the selection is within the codeblock
867
1014
  if (!$isCodeHighlightNode(anchorNode) || !$isCodeHighlightNode(focusNode)) {
868
1015
  return false;
869
1016
  }
1017
+
870
1018
  if (!event.altKey) {
871
1019
  // Handle moving selection out of the code block, given there are no
872
1020
  // sibling thats can natively take the selection.
873
1021
  if (selection.isCollapsed()) {
874
1022
  const codeNode = anchorNode.getParentOrThrow();
1023
+
875
1024
  if (arrowIsUp && anchorOffset === 0 && anchorNode.getPreviousSibling() === null) {
876
1025
  const codeNodeSibling = codeNode.getPreviousSibling();
1026
+
877
1027
  if (codeNodeSibling === null) {
878
1028
  codeNode.selectPrevious();
879
1029
  event.preventDefault();
@@ -881,6 +1031,7 @@ function handleShiftLines(type, event) {
881
1031
  }
882
1032
  } else if (!arrowIsUp && anchorOffset === anchorNode.getTextContentSize() && anchorNode.getNextSibling() === null) {
883
1033
  const codeNodeSibling = codeNode.getNextSibling();
1034
+
884
1035
  if (codeNodeSibling === null) {
885
1036
  codeNode.selectNext();
886
1037
  event.preventDefault();
@@ -888,39 +1039,50 @@ function handleShiftLines(type, event) {
888
1039
  }
889
1040
  }
890
1041
  }
1042
+
891
1043
  return false;
892
1044
  }
1045
+
893
1046
  const start = getFirstCodeHighlightNodeOfLine(anchorNode);
894
1047
  const end = getLastCodeHighlightNodeOfLine(focusNode);
1048
+
895
1049
  if (start == null || end == null) {
896
1050
  return false;
897
1051
  }
1052
+
898
1053
  const range = start.getNodesBetween(end);
1054
+
899
1055
  for (let i = 0; i < range.length; i++) {
900
1056
  const node = range[i];
1057
+
901
1058
  if (!$isCodeHighlightNode(node) && !lexical.$isLineBreakNode(node)) {
902
1059
  return false;
903
1060
  }
904
- }
905
-
906
- // After this point, we know the selection is within the codeblock. We may not be able to
1061
+ } // After this point, we know the selection is within the codeblock. We may not be able to
907
1062
  // actually move the lines around, but we want to return true either way to prevent
908
1063
  // the event's default behavior
1064
+
1065
+
909
1066
  event.preventDefault();
910
1067
  event.stopPropagation(); // required to stop cursor movement under Firefox
911
1068
 
912
1069
  const linebreak = arrowIsUp ? start.getPreviousSibling() : end.getNextSibling();
1070
+
913
1071
  if (!lexical.$isLineBreakNode(linebreak)) {
914
1072
  return true;
915
1073
  }
1074
+
916
1075
  const sibling = arrowIsUp ? linebreak.getPreviousSibling() : linebreak.getNextSibling();
1076
+
917
1077
  if (sibling == null) {
918
1078
  return true;
919
1079
  }
1080
+
920
1081
  const maybeInsertionPoint = arrowIsUp ? getFirstCodeHighlightNodeOfLine(sibling) : getLastCodeHighlightNodeOfLine(sibling);
921
1082
  let insertionPoint = maybeInsertionPoint != null ? maybeInsertionPoint : sibling;
922
1083
  linebreak.remove();
923
1084
  range.forEach(node => node.remove());
1085
+
924
1086
  if (type === lexical.KEY_ARROW_UP_COMMAND) {
925
1087
  range.forEach(node => insertionPoint.insertBefore(node));
926
1088
  insertionPoint.insertBefore(linebreak);
@@ -932,14 +1094,18 @@ function handleShiftLines(type, event) {
932
1094
  insertionPoint = node;
933
1095
  });
934
1096
  }
1097
+
935
1098
  selection.setTextNodeRange(anchorNode, anchorOffset, focusNode, focusOffset);
936
1099
  return true;
937
1100
  }
1101
+
938
1102
  function handleMoveTo(type, event) {
939
1103
  const selection = lexical.$getSelection();
1104
+
940
1105
  if (!lexical.$isRangeSelection(selection)) {
941
1106
  return false;
942
1107
  }
1108
+
943
1109
  const {
944
1110
  anchor,
945
1111
  focus
@@ -947,11 +1113,14 @@ function handleMoveTo(type, event) {
947
1113
  const anchorNode = anchor.getNode();
948
1114
  const focusNode = focus.getNode();
949
1115
  const isMoveToStart = type === lexical.MOVE_TO_START;
1116
+
950
1117
  if (!$isCodeHighlightNode(anchorNode) || !$isCodeHighlightNode(focusNode)) {
951
1118
  return false;
952
1119
  }
1120
+
953
1121
  let node;
954
1122
  let offset;
1123
+
955
1124
  if (isMoveToStart) {
956
1125
  ({
957
1126
  node,
@@ -963,25 +1132,31 @@ function handleMoveTo(type, event) {
963
1132
  offset
964
1133
  } = getEndOfCodeInLine(focusNode));
965
1134
  }
1135
+
966
1136
  if (node !== null && offset !== -1) {
967
1137
  selection.setTextNodeRange(node, offset, node, offset);
968
1138
  }
1139
+
969
1140
  event.preventDefault();
970
1141
  event.stopPropagation();
971
1142
  return true;
972
1143
  }
1144
+
973
1145
  function registerCodeHighlighting(editor, tokenizer) {
974
1146
  if (!editor.hasNodes([CodeNode, CodeHighlightNode])) {
975
1147
  throw new Error('CodeHighlightPlugin: CodeNode or CodeHighlightNode not registered on editor');
976
1148
  }
1149
+
977
1150
  if (tokenizer == null) {
978
1151
  tokenizer = PrismTokenizer;
979
1152
  }
1153
+
980
1154
  return utils.mergeRegister(editor.registerMutationListener(CodeNode, mutations => {
981
1155
  editor.update(() => {
982
1156
  for (const [key, type] of mutations) {
983
1157
  if (type !== 'destroyed') {
984
1158
  const node = lexical.$getNodeByKey(key);
1159
+
985
1160
  if (node !== null) {
986
1161
  updateCodeGutter(node, editor);
987
1162
  }
package/package.json CHANGED
@@ -8,13 +8,13 @@
8
8
  "code"
9
9
  ],
10
10
  "license": "MIT",
11
- "version": "0.8.1",
11
+ "version": "0.9.0",
12
12
  "main": "LexicalCode.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",
17
+ "@lexical/utils": "0.9.0",
18
18
  "prismjs": "^1.27.0"
19
19
  },
20
20
  "repository": {