@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.
- package/CodeHighlightNode.d.ts +2 -0
- package/CodeNode.d.ts +3 -1
- package/LexicalCode.dev.js +793 -786
- package/LexicalCode.prod.js +25 -29
- package/index.d.ts +2 -0
- package/package.json +3 -3
package/LexicalCode.dev.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
75
|
+
static getType() {
|
|
76
|
+
return 'code-highlight';
|
|
77
|
+
}
|
|
44
78
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 (
|
|
55
|
-
|
|
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
|
|
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 (
|
|
74
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
184
|
-
|
|
199
|
+
const LANGUAGE_DATA_ATTRIBUTE = 'data-highlight-language';
|
|
200
|
+
/** @noInheritDoc */
|
|
185
201
|
|
|
186
|
-
|
|
187
|
-
|
|
202
|
+
class CodeNode extends lexical.ElementNode {
|
|
203
|
+
/** @internal */
|
|
204
|
+
static getType() {
|
|
205
|
+
return 'code';
|
|
188
206
|
}
|
|
189
207
|
|
|
190
|
-
|
|
191
|
-
|
|
208
|
+
static clone(node) {
|
|
209
|
+
return new CodeNode(node.__language, node.__key);
|
|
210
|
+
}
|
|
192
211
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
} //
|
|
212
|
+
constructor(language, key) {
|
|
213
|
+
super(key);
|
|
214
|
+
this.__language = mapToPrismLanguage(language);
|
|
215
|
+
} // View
|
|
197
216
|
|
|
198
217
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
222
|
-
if (isHighlighting) {
|
|
223
|
-
return;
|
|
243
|
+
return false;
|
|
224
244
|
}
|
|
225
245
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
269
|
+
if (isGitHubCodeTable(table)) {
|
|
270
|
+
return {
|
|
271
|
+
conversion: convertTableElement,
|
|
272
|
+
priority: 4
|
|
273
|
+
};
|
|
274
|
+
}
|
|
267
275
|
|
|
268
|
-
|
|
269
|
-
|
|
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 (
|
|
272
|
-
|
|
283
|
+
if (isGitHubCodeCell(td)) {
|
|
284
|
+
return {
|
|
285
|
+
conversion: convertTableCellElement,
|
|
286
|
+
priority: 4
|
|
287
|
+
};
|
|
273
288
|
}
|
|
274
289
|
|
|
275
|
-
if (
|
|
276
|
-
|
|
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
|
-
|
|
299
|
-
|
|
306
|
+
if (table && isGitHubCodeTable(table)) {
|
|
307
|
+
return {
|
|
308
|
+
conversion: convertCodeNoop,
|
|
309
|
+
priority: 4
|
|
310
|
+
};
|
|
311
|
+
}
|
|
300
312
|
|
|
301
|
-
|
|
302
|
-
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
303
316
|
}
|
|
304
317
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
const textContentSize = _node.getTextContentSize();
|
|
350
|
+
const anchor = selection.anchor.getNode();
|
|
351
|
+
const firstNode = getFirstCodeHighlightNodeOfLine(anchor);
|
|
335
352
|
|
|
336
|
-
|
|
337
|
-
|
|
353
|
+
if (firstNode != null) {
|
|
354
|
+
let leadingWhitespace = 0;
|
|
355
|
+
const firstNodeText = firstNode.getTextContent();
|
|
338
356
|
|
|
339
|
-
|
|
357
|
+
while (leadingWhitespace < firstNodeText.length && /[\t ]/.test(firstNodeText[leadingWhitespace])) {
|
|
358
|
+
leadingWhitespace += 1;
|
|
340
359
|
}
|
|
341
360
|
|
|
342
|
-
|
|
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
|
|
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
|
-
|
|
352
|
-
|
|
374
|
+
canInsertTab() {
|
|
375
|
+
const selection = lexical.$getSelection();
|
|
353
376
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
break;
|
|
377
|
+
if (!lexical.$isRangeSelection(selection) || !selection.isCollapsed()) {
|
|
378
|
+
return false;
|
|
357
379
|
}
|
|
358
380
|
|
|
359
|
-
|
|
381
|
+
return true;
|
|
360
382
|
}
|
|
361
383
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
let trailingMatch = 0;
|
|
384
|
+
canIndent() {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
366
387
|
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
396
|
+
setLanguage(language) {
|
|
397
|
+
const writable = this.getWritable();
|
|
398
|
+
writable.__language = mapToPrismLanguage(language);
|
|
374
399
|
}
|
|
375
400
|
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
394
|
-
|
|
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
|
-
|
|
413
|
+
function convertPreElement(domNode) {
|
|
414
|
+
return {
|
|
415
|
+
node: $createCodeNode()
|
|
416
|
+
};
|
|
398
417
|
}
|
|
399
418
|
|
|
400
|
-
function
|
|
401
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
436
|
+
function convertTableElement() {
|
|
437
|
+
return {
|
|
438
|
+
node: $createCodeNode()
|
|
439
|
+
};
|
|
440
|
+
}
|
|
409
441
|
|
|
410
|
-
|
|
411
|
-
|
|
442
|
+
function convertCodeNoop() {
|
|
443
|
+
return {
|
|
444
|
+
node: null
|
|
445
|
+
};
|
|
446
|
+
}
|
|
412
447
|
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
458
|
+
return childLexicalNodes;
|
|
459
|
+
},
|
|
460
|
+
node: null
|
|
461
|
+
};
|
|
462
|
+
}
|
|
419
463
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
464
|
+
function isCodeElement(div) {
|
|
465
|
+
return div.style.fontFamily.match('monospace') !== null;
|
|
466
|
+
}
|
|
423
467
|
|
|
424
|
-
|
|
425
|
-
|
|
468
|
+
function isGitHubCodeCell(cell) {
|
|
469
|
+
return cell.classList.contains('js-file-line');
|
|
470
|
+
}
|
|
426
471
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
472
|
+
function isGitHubCodeTable(table) {
|
|
473
|
+
return table.classList.contains('js-file-line-container');
|
|
474
|
+
}
|
|
431
475
|
|
|
432
|
-
|
|
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
|
|
436
|
-
const
|
|
488
|
+
function findFirstNotSpaceOrTabCharAtText(text, isForward) {
|
|
489
|
+
const length = text.length;
|
|
490
|
+
let offset = -1;
|
|
437
491
|
|
|
438
|
-
if (
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
if (
|
|
455
|
-
|
|
456
|
-
|
|
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
|
|
464
|
-
|
|
465
|
-
|
|
515
|
+
function getStartOfCodeInLine(anchor) {
|
|
516
|
+
let currentNode = null;
|
|
517
|
+
let currentNodeOffset = -1;
|
|
518
|
+
const previousSiblings = anchor.getPreviousSiblings();
|
|
519
|
+
previousSiblings.push(anchor);
|
|
466
520
|
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
484
|
-
|
|
534
|
+
if (lexical.$isLineBreakNode(node)) {
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
485
537
|
}
|
|
486
538
|
|
|
487
|
-
if (
|
|
488
|
-
|
|
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
|
-
|
|
494
|
-
|
|
542
|
+
while (nextSiblings.length > 0) {
|
|
543
|
+
const node = nextSiblings.shift();
|
|
495
544
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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 (
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
549
|
+
if (offset !== -1) {
|
|
550
|
+
currentNode = node;
|
|
551
|
+
currentNodeOffset = offset;
|
|
552
|
+
break;
|
|
508
553
|
}
|
|
509
554
|
}
|
|
510
|
-
}
|
|
511
555
|
|
|
512
|
-
|
|
556
|
+
if (lexical.$isLineBreakNode(node)) {
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
513
560
|
}
|
|
514
561
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
573
|
+
while (nextSiblings.length > 0) {
|
|
574
|
+
const node = nextSiblings.shift();
|
|
523
575
|
|
|
524
|
-
|
|
525
|
-
|
|
576
|
+
if ($isCodeHighlightNode(node)) {
|
|
577
|
+
const text = node.getTextContent();
|
|
578
|
+
const offset = findFirstNotSpaceOrTabCharAtText(text, false);
|
|
526
579
|
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
536
|
-
|
|
591
|
+
if (currentNode === null) {
|
|
592
|
+
const previousSiblings = anchor.getPreviousSiblings();
|
|
537
593
|
|
|
538
|
-
|
|
594
|
+
while (previousSiblings.length > 0) {
|
|
595
|
+
const node = previousSiblings.pop();
|
|
539
596
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
597
|
+
if ($isCodeHighlightNode(node)) {
|
|
598
|
+
const text = node.getTextContent();
|
|
599
|
+
const offset = findFirstNotSpaceOrTabCharAtText(text, false);
|
|
543
600
|
|
|
544
|
-
|
|
601
|
+
if (offset !== -1) {
|
|
602
|
+
currentNode = node;
|
|
603
|
+
currentNodeOffset = offset + 1;
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
545
607
|
|
|
546
|
-
|
|
547
|
-
|
|
608
|
+
if (lexical.$isLineBreakNode(node)) {
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
548
612
|
}
|
|
549
613
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
614
|
+
return {
|
|
615
|
+
node: currentNode,
|
|
616
|
+
offset: currentNodeOffset
|
|
617
|
+
};
|
|
618
|
+
}
|
|
554
619
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
568
|
-
|
|
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
|
|
572
|
-
const
|
|
634
|
+
function updateCodeGutter(node, editor) {
|
|
635
|
+
const codeElement = editor.getElementByKey(node.getKey());
|
|
573
636
|
|
|
574
|
-
if (
|
|
575
|
-
return
|
|
637
|
+
if (codeElement === null) {
|
|
638
|
+
return;
|
|
576
639
|
}
|
|
577
640
|
|
|
578
|
-
const
|
|
579
|
-
|
|
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 (
|
|
587
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
606
|
-
|
|
654
|
+
for (let i = 0; i < childrenLength; i++) {
|
|
655
|
+
if (lexical.$isLineBreakNode(children[i])) {
|
|
656
|
+
gutter += '\n' + ++count;
|
|
657
|
+
}
|
|
607
658
|
}
|
|
608
659
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
669
|
+
|
|
670
|
+
let isHighlighting = false;
|
|
671
|
+
|
|
672
|
+
function codeNodeTransform(node, editor) {
|
|
673
|
+
if (isHighlighting) {
|
|
674
|
+
return;
|
|
617
675
|
}
|
|
618
676
|
|
|
619
|
-
|
|
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
|
-
|
|
626
|
-
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
const
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
698
|
+
if (from !== to || nodesForReplacement.length) {
|
|
699
|
+
node.splice(from, to - from, nodesForReplacement);
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
685
702
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
703
|
+
return false;
|
|
704
|
+
});
|
|
705
|
+
}, {
|
|
706
|
+
onUpdate: () => {
|
|
707
|
+
isHighlighting = false;
|
|
708
|
+
},
|
|
709
|
+
skipTransforms: true
|
|
710
|
+
});
|
|
711
|
+
}
|
|
689
712
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
696
|
-
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const nextClassName = getHighlightThemeClass(config.theme, this.__highlightType);
|
|
722
|
+
if (text.length) {
|
|
723
|
+
nodes.push($createCodeHighlightNode(text));
|
|
724
|
+
}
|
|
706
725
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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 (
|
|
713
|
-
|
|
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
|
-
|
|
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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
-
|
|
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
|
-
|
|
749
|
-
|
|
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
|
-
|
|
760
|
-
|
|
789
|
+
if (textContentSize >= textOffset) {
|
|
790
|
+
_node.select(textOffset, textOffset);
|
|
761
791
|
|
|
762
|
-
|
|
763
|
-
|
|
792
|
+
return true;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
textOffset -= textContentSize;
|
|
764
796
|
}
|
|
765
797
|
|
|
766
|
-
|
|
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
|
-
|
|
772
|
-
}
|
|
773
|
-
function getLastCodeHighlightNodeOfLine(anchor) {
|
|
774
|
-
let currentNode = null;
|
|
775
|
-
const nextSiblings = anchor.getNextSiblings();
|
|
776
|
-
nextSiblings.unshift(anchor);
|
|
812
|
+
leadingMatch++;
|
|
813
|
+
}
|
|
777
814
|
|
|
778
|
-
|
|
779
|
-
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
}
|
|
820
|
+
while (trailingMatch < maxTrailingMatch) {
|
|
821
|
+
trailingMatch++;
|
|
784
822
|
|
|
785
|
-
if (
|
|
823
|
+
if (!isEqual(prevNodes[prevNodesLength - trailingMatch], nextNodes[nextNodesLength - trailingMatch])) {
|
|
824
|
+
trailingMatch--;
|
|
786
825
|
break;
|
|
787
826
|
}
|
|
788
827
|
}
|
|
789
828
|
|
|
790
|
-
|
|
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
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
813
|
-
return
|
|
846
|
+
if (lexical.$isLineBreakNode(nodeA) && lexical.$isLineBreakNode(nodeB)) {
|
|
847
|
+
return true;
|
|
814
848
|
}
|
|
815
849
|
|
|
816
|
-
|
|
817
|
-
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
|
|
833
|
-
}
|
|
861
|
+
const nodes = selection.getNodes();
|
|
834
862
|
|
|
835
|
-
|
|
836
|
-
const
|
|
837
|
-
const prevLanguage = prevNode.__language;
|
|
863
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
864
|
+
const node = nodes[i];
|
|
838
865
|
|
|
839
|
-
if (
|
|
840
|
-
|
|
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
|
-
|
|
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
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
priority: 4
|
|
891
|
-
};
|
|
892
|
-
}
|
|
873
|
+
if (startOfLine != null) {
|
|
874
|
+
doIndent(startOfLine, type);
|
|
875
|
+
}
|
|
893
876
|
|
|
894
|
-
|
|
895
|
-
|
|
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
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
-
|
|
911
|
-
|
|
912
|
-
conversion: convertCodeNoop,
|
|
913
|
-
priority: 4
|
|
914
|
-
};
|
|
915
|
-
}
|
|
885
|
+
return true;
|
|
886
|
+
}
|
|
916
887
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
};
|
|
920
|
-
}
|
|
888
|
+
function doIndent(node, type) {
|
|
889
|
+
const text = node.getTextContent();
|
|
921
890
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
node.
|
|
926
|
-
|
|
927
|
-
|
|
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
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
-
|
|
955
|
-
|
|
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
|
-
|
|
958
|
-
|
|
959
|
-
const firstNodeText = firstNode.getTextContent();
|
|
946
|
+
if (arrowIsUp && anchorOffset === 0 && anchorNode.getPreviousSibling() === null) {
|
|
947
|
+
const codeNodeSibling = codeNode.getPreviousSibling();
|
|
960
948
|
|
|
961
|
-
|
|
962
|
-
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
|
965
|
+
return false;
|
|
976
966
|
}
|
|
977
967
|
|
|
978
|
-
|
|
979
|
-
|
|
968
|
+
const start = getFirstCodeHighlightNodeOfLine(anchorNode);
|
|
969
|
+
const end = getLastCodeHighlightNodeOfLine(focusNode);
|
|
980
970
|
|
|
981
|
-
|
|
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
|
-
|
|
989
|
-
return false;
|
|
990
|
-
}
|
|
997
|
+
const sibling = arrowIsUp ? linebreak.getPreviousSibling() : linebreak.getNextSibling();
|
|
991
998
|
|
|
992
|
-
|
|
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
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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
|
-
|
|
1006
|
-
|
|
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
|
-
|
|
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
|
|
1018
|
-
|
|
1019
|
-
node: $createCodeNode()
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1024
|
+
function handleMoveTo(type, event) {
|
|
1025
|
+
const selection = lexical.$getSelection();
|
|
1022
1026
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
return {
|
|
1027
|
-
after: childLexicalNodes => {
|
|
1028
|
-
const domParent = domNode.parentNode;
|
|
1027
|
+
if (!lexical.$isRangeSelection(selection)) {
|
|
1028
|
+
return false;
|
|
1029
|
+
}
|
|
1029
1030
|
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
};
|
|
1038
|
-
}
|
|
1039
|
+
if (!$isCodeHighlightNode(anchorNode) || !$isCodeHighlightNode(focusNode)) {
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1039
1042
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
node: $createCodeNode()
|
|
1043
|
-
};
|
|
1044
|
-
}
|
|
1043
|
+
let node;
|
|
1044
|
+
let offset;
|
|
1045
1045
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
};
|
|
1062
|
+
event.preventDefault();
|
|
1063
|
+
event.stopPropagation();
|
|
1064
|
+
return true;
|
|
1066
1065
|
}
|
|
1067
1066
|
|
|
1068
|
-
function
|
|
1069
|
-
|
|
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
|
-
|
|
1073
|
-
|
|
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
|
-
|
|
1077
|
-
|
|
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;
|