@lexical/code-core 0.44.1-nightly.20260518.0 → 0.45.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/dist/CodeImportExtension.d.ts +50 -0
- package/{CodeNode.d.ts → dist/CodeNode.d.ts} +1 -0
- package/{FlatStructureUtils.d.ts → dist/FlatStructureUtils.d.ts} +10 -1
- package/{LexicalCodeCore.dev.js → dist/LexicalCodeCore.dev.js} +377 -6
- package/{LexicalCodeCore.dev.mjs → dist/LexicalCodeCore.dev.mjs} +376 -8
- package/{LexicalCodeCore.js.flow → dist/LexicalCodeCore.js.flow} +2 -0
- package/{LexicalCodeCore.mjs → dist/LexicalCodeCore.mjs} +3 -0
- package/{LexicalCodeCore.node.mjs → dist/LexicalCodeCore.node.mjs} +3 -0
- package/dist/LexicalCodeCore.prod.js +9 -0
- package/dist/LexicalCodeCore.prod.mjs +9 -0
- package/{index.d.ts → dist/index.d.ts} +2 -1
- package/package.json +32 -16
- package/src/CodeExtension.ts +40 -0
- package/src/CodeHighlightNode.ts +171 -0
- package/src/CodeImportExtension.ts +403 -0
- package/src/CodeIndentation.ts +654 -0
- package/src/CodeNode.ts +503 -0
- package/src/FlatStructureUtils.ts +311 -0
- package/src/index.ts +37 -0
- package/LexicalCodeCore.prod.js +0 -9
- package/LexicalCodeCore.prod.mjs +0 -9
- /package/{CodeExtension.d.ts → dist/CodeExtension.d.ts} +0 -0
- /package/{CodeHighlightNode.d.ts → dist/CodeHighlightNode.d.ts} +0 -0
- /package/{CodeIndentation.d.ts → dist/CodeIndentation.d.ts} +0 -0
- /package/{LexicalCodeCore.js → dist/LexicalCodeCore.js} +0 -0
package/src/CodeNode.ts
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {CodeExtension} from './CodeExtension';
|
|
10
|
+
import type {
|
|
11
|
+
DOMConversionMap,
|
|
12
|
+
DOMConversionOutput,
|
|
13
|
+
DOMExportOutput,
|
|
14
|
+
EditorConfig,
|
|
15
|
+
LexicalEditor,
|
|
16
|
+
LexicalNode,
|
|
17
|
+
LexicalUpdateJSON,
|
|
18
|
+
NodeKey,
|
|
19
|
+
ParagraphNode,
|
|
20
|
+
RangeSelection,
|
|
21
|
+
SerializedElementNode,
|
|
22
|
+
Spread,
|
|
23
|
+
TabNode,
|
|
24
|
+
} from 'lexical';
|
|
25
|
+
|
|
26
|
+
import {getPeerDependencyFromEditor} from '@lexical/extension';
|
|
27
|
+
import warnOnlyOnce from '@lexical/internal/warnOnlyOnce';
|
|
28
|
+
import {
|
|
29
|
+
$create,
|
|
30
|
+
$createLineBreakNode,
|
|
31
|
+
$createParagraphNode,
|
|
32
|
+
$createTabNode,
|
|
33
|
+
$getEditor,
|
|
34
|
+
$isLineBreakNode,
|
|
35
|
+
$isTabNode,
|
|
36
|
+
$isTextNode,
|
|
37
|
+
addClassNamesToElement,
|
|
38
|
+
ElementNode,
|
|
39
|
+
isHTMLElement,
|
|
40
|
+
setDOMStyleFromCSS,
|
|
41
|
+
} from 'lexical';
|
|
42
|
+
|
|
43
|
+
import {
|
|
44
|
+
$createCodeHighlightNode,
|
|
45
|
+
$isCodeHighlightNode,
|
|
46
|
+
type CodeHighlightNode,
|
|
47
|
+
} from './CodeHighlightNode';
|
|
48
|
+
import {$getFirstCodeNodeOfLine} from './FlatStructureUtils';
|
|
49
|
+
|
|
50
|
+
export type SerializedCodeNode = Spread<
|
|
51
|
+
{
|
|
52
|
+
language: string | null | undefined;
|
|
53
|
+
theme?: string | undefined;
|
|
54
|
+
},
|
|
55
|
+
SerializedElementNode
|
|
56
|
+
>;
|
|
57
|
+
|
|
58
|
+
export const DEFAULT_CODE_LANGUAGE = 'javascript';
|
|
59
|
+
/** @internal Configurable through the extensions. */
|
|
60
|
+
export const getDefaultCodeLanguage = (): string => DEFAULT_CODE_LANGUAGE;
|
|
61
|
+
|
|
62
|
+
function hasChildDOMNodeTag(node: Node, tagName: string) {
|
|
63
|
+
for (const child of node.childNodes) {
|
|
64
|
+
if (isHTMLElement(child) && child.tagName === tagName) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
if (hasChildDOMNodeTag(child, tagName)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const LANGUAGE_DATA_ATTRIBUTE = 'data-language';
|
|
75
|
+
const HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE = 'data-highlight-language';
|
|
76
|
+
const THEME_DATA_ATTRIBUTE = 'data-theme';
|
|
77
|
+
|
|
78
|
+
const noExtensionDeprecation = warnOnlyOnce(
|
|
79
|
+
'Using CodeNode without CodeExtension is deprecated',
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
/** @noInheritDoc */
|
|
83
|
+
export class CodeNode extends ElementNode {
|
|
84
|
+
/** @internal */
|
|
85
|
+
__language: string | null | undefined;
|
|
86
|
+
/** @internal */
|
|
87
|
+
__theme: string | undefined;
|
|
88
|
+
/** @internal */
|
|
89
|
+
__isSyntaxHighlightSupported: boolean;
|
|
90
|
+
|
|
91
|
+
static getType(): string {
|
|
92
|
+
return 'code';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static clone(node: CodeNode): CodeNode {
|
|
96
|
+
return new CodeNode(node.__language, node.__key);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
constructor(language?: string | null | undefined, key?: NodeKey) {
|
|
100
|
+
super(key);
|
|
101
|
+
this.__language = language || undefined;
|
|
102
|
+
this.__isSyntaxHighlightSupported = false;
|
|
103
|
+
this.__theme = undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
afterCloneFrom(prevNode: this): void {
|
|
107
|
+
super.afterCloneFrom(prevNode);
|
|
108
|
+
this.__language = prevNode.__language;
|
|
109
|
+
this.__theme = prevNode.__theme;
|
|
110
|
+
this.__isSyntaxHighlightSupported = prevNode.__isSyntaxHighlightSupported;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// View
|
|
114
|
+
createDOM(config: EditorConfig): HTMLElement {
|
|
115
|
+
const element = document.createElement('code');
|
|
116
|
+
addClassNamesToElement(element, config.theme.code);
|
|
117
|
+
element.setAttribute('spellcheck', 'false');
|
|
118
|
+
const language = this.getLanguage();
|
|
119
|
+
if (language) {
|
|
120
|
+
element.setAttribute(LANGUAGE_DATA_ATTRIBUTE, language);
|
|
121
|
+
if (this.getIsSyntaxHighlightSupported()) {
|
|
122
|
+
element.setAttribute(HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE, language);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const theme = this.getTheme();
|
|
127
|
+
if (theme) {
|
|
128
|
+
element.setAttribute(THEME_DATA_ATTRIBUTE, theme);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const style = this.getStyle();
|
|
132
|
+
if (style) {
|
|
133
|
+
setDOMStyleFromCSS(element.style, style);
|
|
134
|
+
}
|
|
135
|
+
return element;
|
|
136
|
+
}
|
|
137
|
+
updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean {
|
|
138
|
+
const language = this.__language;
|
|
139
|
+
const prevLanguage = prevNode.__language;
|
|
140
|
+
|
|
141
|
+
if (language) {
|
|
142
|
+
if (language !== prevLanguage) {
|
|
143
|
+
dom.setAttribute(LANGUAGE_DATA_ATTRIBUTE, language);
|
|
144
|
+
}
|
|
145
|
+
} else if (prevLanguage) {
|
|
146
|
+
dom.removeAttribute(LANGUAGE_DATA_ATTRIBUTE);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const isSyntaxHighlightSupported = this.__isSyntaxHighlightSupported;
|
|
150
|
+
const prevIsSyntaxHighlightSupported =
|
|
151
|
+
prevNode.__isSyntaxHighlightSupported;
|
|
152
|
+
|
|
153
|
+
if (prevIsSyntaxHighlightSupported && prevLanguage) {
|
|
154
|
+
if (isSyntaxHighlightSupported && language) {
|
|
155
|
+
if (language !== prevLanguage) {
|
|
156
|
+
dom.setAttribute(HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE, language);
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
dom.removeAttribute(HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE);
|
|
160
|
+
}
|
|
161
|
+
} else if (isSyntaxHighlightSupported && language) {
|
|
162
|
+
dom.setAttribute(HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE, language);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const theme = this.__theme;
|
|
166
|
+
const prevTheme = prevNode.__theme;
|
|
167
|
+
|
|
168
|
+
if (theme) {
|
|
169
|
+
if (theme !== prevTheme) {
|
|
170
|
+
dom.setAttribute(THEME_DATA_ATTRIBUTE, theme);
|
|
171
|
+
}
|
|
172
|
+
} else if (prevTheme) {
|
|
173
|
+
dom.removeAttribute(THEME_DATA_ATTRIBUTE);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const style = this.__style;
|
|
177
|
+
const prevStyle = prevNode.__style;
|
|
178
|
+
if (style !== prevStyle) {
|
|
179
|
+
setDOMStyleFromCSS(dom.style, style, prevStyle);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
exportDOM(editor: LexicalEditor): DOMExportOutput {
|
|
186
|
+
const element = document.createElement('pre');
|
|
187
|
+
addClassNamesToElement(element, editor._config.theme.code);
|
|
188
|
+
element.setAttribute('spellcheck', 'false');
|
|
189
|
+
const language = this.getLanguage();
|
|
190
|
+
if (language) {
|
|
191
|
+
element.setAttribute(LANGUAGE_DATA_ATTRIBUTE, language);
|
|
192
|
+
|
|
193
|
+
if (this.getIsSyntaxHighlightSupported()) {
|
|
194
|
+
element.setAttribute(HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE, language);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const theme = this.getTheme();
|
|
199
|
+
if (theme) {
|
|
200
|
+
element.setAttribute(THEME_DATA_ATTRIBUTE, theme);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const style = this.getStyle();
|
|
204
|
+
if (style) {
|
|
205
|
+
setDOMStyleFromCSS(element.style, style);
|
|
206
|
+
}
|
|
207
|
+
return {element};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
static importDOM(): DOMConversionMap | null {
|
|
211
|
+
return {
|
|
212
|
+
// Typically <pre> is used for code blocks, and <code> for inline code styles
|
|
213
|
+
// but if it's a multi line <code> we'll create a block. Pass through to
|
|
214
|
+
// inline format handled by TextNode otherwise.
|
|
215
|
+
code: (node: Node) => {
|
|
216
|
+
const isMultiLine =
|
|
217
|
+
node.textContent != null &&
|
|
218
|
+
(/\r?\n/.test(node.textContent) || hasChildDOMNodeTag(node, 'BR'));
|
|
219
|
+
|
|
220
|
+
return isMultiLine
|
|
221
|
+
? {
|
|
222
|
+
conversion: $convertPreElement,
|
|
223
|
+
priority: 1,
|
|
224
|
+
}
|
|
225
|
+
: null;
|
|
226
|
+
},
|
|
227
|
+
div: () => ({
|
|
228
|
+
conversion: $convertDivElement,
|
|
229
|
+
priority: 1,
|
|
230
|
+
}),
|
|
231
|
+
pre: () => ({
|
|
232
|
+
conversion: $convertPreElement,
|
|
233
|
+
priority: 0,
|
|
234
|
+
}),
|
|
235
|
+
table: (node: Node) => {
|
|
236
|
+
const table = node;
|
|
237
|
+
// domNode is a <table> since we matched it by nodeName
|
|
238
|
+
if (isGitHubCodeTable(table as HTMLTableElement)) {
|
|
239
|
+
return {
|
|
240
|
+
conversion: $convertTableElement,
|
|
241
|
+
priority: 3,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
},
|
|
246
|
+
td: (node: Node) => {
|
|
247
|
+
// element is a <td> since we matched it by nodeName
|
|
248
|
+
const td = node as HTMLTableCellElement;
|
|
249
|
+
const table: HTMLTableElement | null = td.closest('table');
|
|
250
|
+
|
|
251
|
+
if (isGitHubCodeCell(td) || (table && isGitHubCodeTable(table))) {
|
|
252
|
+
// Return a no-op if it's a table cell in a code table, but not a code line.
|
|
253
|
+
// Otherwise it'll fall back to the T
|
|
254
|
+
return {
|
|
255
|
+
conversion: convertCodeNoop,
|
|
256
|
+
priority: 3,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return null;
|
|
261
|
+
},
|
|
262
|
+
tr: (node: Node) => {
|
|
263
|
+
// element is a <tr> since we matched it by nodeName
|
|
264
|
+
const tr = node as HTMLTableCellElement;
|
|
265
|
+
const table: HTMLTableElement | null = tr.closest('table');
|
|
266
|
+
if (table && isGitHubCodeTable(table)) {
|
|
267
|
+
return {
|
|
268
|
+
conversion: convertCodeNoop,
|
|
269
|
+
priority: 3,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
static importJSON(serializedNode: SerializedCodeNode): CodeNode {
|
|
278
|
+
return $createCodeNode().updateFromJSON(serializedNode);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
updateFromJSON(serializedNode: LexicalUpdateJSON<SerializedCodeNode>): this {
|
|
282
|
+
return super
|
|
283
|
+
.updateFromJSON(serializedNode)
|
|
284
|
+
.setLanguage(serializedNode.language)
|
|
285
|
+
.setTheme(serializedNode.theme);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
exportJSON(): SerializedCodeNode {
|
|
289
|
+
return {
|
|
290
|
+
...super.exportJSON(),
|
|
291
|
+
language: this.getLanguage(),
|
|
292
|
+
theme: this.getTheme(),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Mutation
|
|
297
|
+
insertNewAfter(
|
|
298
|
+
selection: RangeSelection,
|
|
299
|
+
restoreSelection = true,
|
|
300
|
+
): null | ParagraphNode | CodeHighlightNode | TabNode {
|
|
301
|
+
if (
|
|
302
|
+
!getPeerDependencyFromEditor<typeof CodeExtension>(
|
|
303
|
+
$getEditor(),
|
|
304
|
+
'@lexical/code',
|
|
305
|
+
)
|
|
306
|
+
) {
|
|
307
|
+
noExtensionDeprecation();
|
|
308
|
+
const el = $exitCodeNodeOnEnter(selection);
|
|
309
|
+
if (el) {
|
|
310
|
+
return el;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// If the selection is within the codeblock, find all leading tabs and
|
|
314
|
+
// spaces of the current line. Create a new line that has all those
|
|
315
|
+
// tabs and spaces, such that leading indentation is preserved.
|
|
316
|
+
const {anchor, focus} = selection;
|
|
317
|
+
const firstPoint = anchor.isBefore(focus) ? anchor : focus;
|
|
318
|
+
const firstSelectionNode = firstPoint.getNode();
|
|
319
|
+
if ($isTextNode(firstSelectionNode)) {
|
|
320
|
+
let node: null | LexicalNode =
|
|
321
|
+
$getFirstCodeNodeOfLine(firstSelectionNode);
|
|
322
|
+
const insertNodes = [];
|
|
323
|
+
|
|
324
|
+
while (true) {
|
|
325
|
+
if ($isTabNode(node)) {
|
|
326
|
+
insertNodes.push($createTabNode());
|
|
327
|
+
node = node.getNextSibling();
|
|
328
|
+
} else if ($isCodeHighlightNode(node)) {
|
|
329
|
+
let spaces = 0;
|
|
330
|
+
const text = node.getTextContent();
|
|
331
|
+
const textSize = node.getTextContentSize();
|
|
332
|
+
while (spaces < textSize && text[spaces] === ' ') {
|
|
333
|
+
spaces++;
|
|
334
|
+
}
|
|
335
|
+
if (spaces !== 0) {
|
|
336
|
+
insertNodes.push($createCodeHighlightNode(' '.repeat(spaces)));
|
|
337
|
+
}
|
|
338
|
+
if (spaces !== textSize) {
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
node = node.getNextSibling();
|
|
342
|
+
} else {
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const split = firstSelectionNode.splitText(anchor.offset)[0];
|
|
347
|
+
const x = anchor.offset === 0 ? 0 : 1;
|
|
348
|
+
const index = split.getIndexWithinParent() + x;
|
|
349
|
+
const codeNode = firstSelectionNode.getParentOrThrow();
|
|
350
|
+
const nodesToInsert = [$createLineBreakNode(), ...insertNodes];
|
|
351
|
+
codeNode.splice(index, 0, nodesToInsert);
|
|
352
|
+
const last = insertNodes[insertNodes.length - 1];
|
|
353
|
+
if (last) {
|
|
354
|
+
last.select();
|
|
355
|
+
} else if (anchor.offset === 0) {
|
|
356
|
+
split.selectPrevious();
|
|
357
|
+
} else {
|
|
358
|
+
split.getNextSibling()!.selectNext(0, 0);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if ($isCodeNode(firstSelectionNode)) {
|
|
362
|
+
const {offset} = selection.anchor;
|
|
363
|
+
firstSelectionNode.splice(offset, 0, [$createLineBreakNode()]);
|
|
364
|
+
firstSelectionNode.select(offset + 1, offset + 1);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
canIndent(): false {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
collapseAtStart(): boolean {
|
|
375
|
+
const paragraph = $createParagraphNode();
|
|
376
|
+
const children = this.getChildren();
|
|
377
|
+
children.forEach(child => paragraph.append(child));
|
|
378
|
+
this.replace(paragraph);
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
setLanguage(language: string | null | undefined): this {
|
|
383
|
+
const writable = this.getWritable();
|
|
384
|
+
writable.__language = language || undefined;
|
|
385
|
+
return writable;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
getLanguage(): string | null | undefined {
|
|
389
|
+
return this.getLatest().__language;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
setIsSyntaxHighlightSupported(isSupported: boolean): this {
|
|
393
|
+
const writable = this.getWritable();
|
|
394
|
+
writable.__isSyntaxHighlightSupported = isSupported;
|
|
395
|
+
return writable;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
getIsSyntaxHighlightSupported(): boolean {
|
|
399
|
+
return this.getLatest().__isSyntaxHighlightSupported;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
setTheme(theme: string | null | undefined): this {
|
|
403
|
+
const writable = this.getWritable();
|
|
404
|
+
writable.__theme = theme || undefined;
|
|
405
|
+
return writable;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
getTheme(): string | undefined {
|
|
409
|
+
return this.getLatest().__theme;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function $createCodeNode(
|
|
414
|
+
language?: string | null | undefined,
|
|
415
|
+
theme?: string | null | undefined,
|
|
416
|
+
): CodeNode {
|
|
417
|
+
return $create(CodeNode).setLanguage(language).setTheme(theme);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function $isCodeNode(
|
|
421
|
+
node: LexicalNode | null | undefined,
|
|
422
|
+
): node is CodeNode {
|
|
423
|
+
return node instanceof CodeNode;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function $convertPreElement(domNode: HTMLElement): DOMConversionOutput {
|
|
427
|
+
const language = domNode.getAttribute(LANGUAGE_DATA_ATTRIBUTE);
|
|
428
|
+
return {node: $createCodeNode(language)};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function $convertDivElement(domNode: Node): DOMConversionOutput {
|
|
432
|
+
// domNode is a <div> since we matched it by nodeName
|
|
433
|
+
const div = domNode as HTMLDivElement;
|
|
434
|
+
const isCode = isCodeElement(div);
|
|
435
|
+
if (!isCode && !isCodeChildElement(div)) {
|
|
436
|
+
return {
|
|
437
|
+
node: null,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
node: isCode ? $createCodeNode() : null,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function $convertTableElement(): DOMConversionOutput {
|
|
446
|
+
return {node: $createCodeNode()};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function convertCodeNoop(): DOMConversionOutput {
|
|
450
|
+
return {node: null};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function isCodeElement(div: HTMLElement): boolean {
|
|
454
|
+
return div.style.fontFamily.match('monospace') !== null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function isCodeChildElement(node: HTMLElement): boolean {
|
|
458
|
+
let parent = node.parentElement;
|
|
459
|
+
while (parent !== null) {
|
|
460
|
+
if (isCodeElement(parent)) {
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
parent = parent.parentElement;
|
|
464
|
+
}
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function isGitHubCodeCell(
|
|
469
|
+
cell: HTMLTableCellElement,
|
|
470
|
+
): cell is HTMLTableCellElement {
|
|
471
|
+
return cell.classList.contains('js-file-line');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function isGitHubCodeTable(table: HTMLTableElement): table is HTMLTableElement {
|
|
475
|
+
return table.classList.contains('js-file-line-container');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function $exitCodeNodeOnEnter(
|
|
479
|
+
selection: RangeSelection,
|
|
480
|
+
): null | ParagraphNode {
|
|
481
|
+
const {anchor} = selection;
|
|
482
|
+
if (selection.isCollapsed() && anchor.type === 'element') {
|
|
483
|
+
const codeNode = anchor.getNode();
|
|
484
|
+
if ($isCodeNode(codeNode)) {
|
|
485
|
+
const childrenSize = codeNode.getChildrenSize();
|
|
486
|
+
if (childrenSize >= 2 && anchor.offset === childrenSize) {
|
|
487
|
+
const lastChild = codeNode.getLastChild();
|
|
488
|
+
if (
|
|
489
|
+
$isLineBreakNode(lastChild) &&
|
|
490
|
+
$isLineBreakNode(lastChild.getPreviousSibling())
|
|
491
|
+
) {
|
|
492
|
+
const newElement = $createParagraphNode();
|
|
493
|
+
codeNode
|
|
494
|
+
.splice(childrenSize - 2, 2, [])
|
|
495
|
+
.insertAfter(newElement, false);
|
|
496
|
+
newElement.select();
|
|
497
|
+
return newElement;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return null;
|
|
503
|
+
}
|