@lexical/code 0.3.10 → 0.3.11

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