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