@lexical/code 0.3.9 → 0.4.0

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