@lexical/code 0.3.6 → 0.3.9

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