@lexical/code-core 0.44.1-nightly.20260519.0 → 0.45.1-dev.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} +367 -6
- package/{LexicalCodeCore.dev.mjs → dist/LexicalCodeCore.dev.mjs} +366 -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 +302 -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/package.json
CHANGED
|
@@ -8,35 +8,51 @@
|
|
|
8
8
|
"code"
|
|
9
9
|
],
|
|
10
10
|
"license": "MIT",
|
|
11
|
-
"version": "0.
|
|
12
|
-
"main": "LexicalCodeCore.js",
|
|
13
|
-
"types": "index.d.ts",
|
|
11
|
+
"version": "0.45.1-dev.0",
|
|
12
|
+
"main": "./dist/LexicalCodeCore.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@lexical/extension": "0.
|
|
16
|
-
"lexical": "0.
|
|
15
|
+
"@lexical/extension": "0.45.1-dev.0",
|
|
16
|
+
"lexical": "0.45.1-dev.0",
|
|
17
|
+
"@lexical/internal": "0.45.1-dev.0",
|
|
18
|
+
"@lexical/html": "0.45.1-dev.0"
|
|
17
19
|
},
|
|
18
20
|
"repository": {
|
|
19
21
|
"type": "git",
|
|
20
22
|
"url": "git+https://github.com/facebook/lexical.git",
|
|
21
23
|
"directory": "packages/lexical-code-core"
|
|
22
24
|
},
|
|
23
|
-
"module": "LexicalCodeCore.mjs",
|
|
25
|
+
"module": "./dist/LexicalCodeCore.mjs",
|
|
24
26
|
"sideEffects": false,
|
|
25
27
|
"exports": {
|
|
26
28
|
".": {
|
|
29
|
+
"source": "./src/index.ts",
|
|
27
30
|
"import": {
|
|
28
|
-
"types": "./index.d.ts",
|
|
29
|
-
"development": "./LexicalCodeCore.dev.mjs",
|
|
30
|
-
"production": "./LexicalCodeCore.prod.mjs",
|
|
31
|
-
"node": "./LexicalCodeCore.node.mjs",
|
|
32
|
-
"default": "./LexicalCodeCore.mjs"
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"development": "./dist/LexicalCodeCore.dev.mjs",
|
|
33
|
+
"production": "./dist/LexicalCodeCore.prod.mjs",
|
|
34
|
+
"node": "./dist/LexicalCodeCore.node.mjs",
|
|
35
|
+
"default": "./dist/LexicalCodeCore.mjs"
|
|
33
36
|
},
|
|
34
37
|
"require": {
|
|
35
|
-
"types": "./index.d.ts",
|
|
36
|
-
"development": "./LexicalCodeCore.dev.js",
|
|
37
|
-
"production": "./LexicalCodeCore.prod.js",
|
|
38
|
-
"default": "./LexicalCodeCore.js"
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"development": "./dist/LexicalCodeCore.dev.js",
|
|
40
|
+
"production": "./dist/LexicalCodeCore.prod.js",
|
|
41
|
+
"default": "./dist/LexicalCodeCore.js"
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
|
-
}
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist",
|
|
47
|
+
"src",
|
|
48
|
+
"!src/__tests__",
|
|
49
|
+
"!src/__bench__",
|
|
50
|
+
"!src/__mocks__",
|
|
51
|
+
"!src/**/*.test.ts",
|
|
52
|
+
"!src/**/*.test.tsx",
|
|
53
|
+
"!src/**/*.bench.ts",
|
|
54
|
+
"!src/**/*.bench.tsx",
|
|
55
|
+
"README.md",
|
|
56
|
+
"LICENSE"
|
|
57
|
+
]
|
|
42
58
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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 {
|
|
10
|
+
$getSelection,
|
|
11
|
+
$isRangeSelection,
|
|
12
|
+
COMMAND_PRIORITY_LOW,
|
|
13
|
+
defineExtension,
|
|
14
|
+
KEY_ENTER_COMMAND,
|
|
15
|
+
} from 'lexical';
|
|
16
|
+
|
|
17
|
+
import {CodeHighlightNode} from './CodeHighlightNode';
|
|
18
|
+
import {$exitCodeNodeOnEnter, CodeNode} from './CodeNode';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Add code blocks to the editor (syntax highlighting provided separately)
|
|
22
|
+
*/
|
|
23
|
+
export const CodeExtension = defineExtension({
|
|
24
|
+
name: '@lexical/code',
|
|
25
|
+
nodes: () => [CodeNode, CodeHighlightNode],
|
|
26
|
+
register(editor) {
|
|
27
|
+
return editor.registerCommand<KeyboardEvent>(
|
|
28
|
+
KEY_ENTER_COMMAND,
|
|
29
|
+
event => {
|
|
30
|
+
const selection = $getSelection();
|
|
31
|
+
if ($isRangeSelection(selection) && $exitCodeNodeOnEnter(selection)) {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
},
|
|
37
|
+
COMMAND_PRIORITY_LOW,
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
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 {
|
|
10
|
+
EditorConfig,
|
|
11
|
+
EditorThemeClasses,
|
|
12
|
+
LexicalNode,
|
|
13
|
+
LexicalUpdateJSON,
|
|
14
|
+
NodeKey,
|
|
15
|
+
SerializedTextNode,
|
|
16
|
+
Spread,
|
|
17
|
+
} from 'lexical';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
$applyNodeReplacement,
|
|
21
|
+
addClassNamesToElement,
|
|
22
|
+
ElementNode,
|
|
23
|
+
removeClassNamesFromElement,
|
|
24
|
+
TextNode,
|
|
25
|
+
} from 'lexical';
|
|
26
|
+
|
|
27
|
+
import {$createCodeNode} from './CodeNode';
|
|
28
|
+
|
|
29
|
+
type SerializedCodeHighlightNode = Spread<
|
|
30
|
+
{
|
|
31
|
+
highlightType: string | null | undefined;
|
|
32
|
+
},
|
|
33
|
+
SerializedTextNode
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
/** @noInheritDoc */
|
|
37
|
+
export class CodeHighlightNode extends TextNode {
|
|
38
|
+
/** @internal */
|
|
39
|
+
__highlightType: string | null | undefined;
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
text: string = '',
|
|
43
|
+
highlightType?: string | null | undefined,
|
|
44
|
+
key?: NodeKey,
|
|
45
|
+
) {
|
|
46
|
+
super(text, key);
|
|
47
|
+
this.__highlightType = highlightType;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static getType(): string {
|
|
51
|
+
return 'code-highlight';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static clone(node: CodeHighlightNode): CodeHighlightNode {
|
|
55
|
+
return new CodeHighlightNode(
|
|
56
|
+
node.__text,
|
|
57
|
+
node.__highlightType || undefined,
|
|
58
|
+
node.__key,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
afterCloneFrom(prevNode: this): void {
|
|
63
|
+
super.afterCloneFrom(prevNode);
|
|
64
|
+
this.__highlightType = prevNode.__highlightType;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getHighlightType(): string | null | undefined {
|
|
68
|
+
const self = this.getLatest();
|
|
69
|
+
return self.__highlightType;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setHighlightType(highlightType?: string | null | undefined): this {
|
|
73
|
+
const self = this.getWritable();
|
|
74
|
+
self.__highlightType = highlightType || undefined;
|
|
75
|
+
return self;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
canHaveFormat(): boolean {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
createDOM(config: EditorConfig): HTMLElement {
|
|
83
|
+
const element = super.createDOM(config);
|
|
84
|
+
const className = getHighlightThemeClass(
|
|
85
|
+
config.theme,
|
|
86
|
+
this.__highlightType,
|
|
87
|
+
);
|
|
88
|
+
addClassNamesToElement(element, className);
|
|
89
|
+
return element;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean {
|
|
93
|
+
const update = super.updateDOM(prevNode, dom, config);
|
|
94
|
+
const prevClassName = getHighlightThemeClass(
|
|
95
|
+
config.theme,
|
|
96
|
+
prevNode.__highlightType,
|
|
97
|
+
);
|
|
98
|
+
const nextClassName = getHighlightThemeClass(
|
|
99
|
+
config.theme,
|
|
100
|
+
this.__highlightType,
|
|
101
|
+
);
|
|
102
|
+
if (prevClassName !== nextClassName) {
|
|
103
|
+
if (prevClassName) {
|
|
104
|
+
removeClassNamesFromElement(dom, prevClassName);
|
|
105
|
+
}
|
|
106
|
+
if (nextClassName) {
|
|
107
|
+
addClassNamesToElement(dom, nextClassName);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return update;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static importJSON(
|
|
114
|
+
serializedNode: SerializedCodeHighlightNode,
|
|
115
|
+
): CodeHighlightNode {
|
|
116
|
+
return $createCodeHighlightNode().updateFromJSON(serializedNode);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
updateFromJSON(
|
|
120
|
+
serializedNode: LexicalUpdateJSON<SerializedCodeHighlightNode>,
|
|
121
|
+
): this {
|
|
122
|
+
return super
|
|
123
|
+
.updateFromJSON(serializedNode)
|
|
124
|
+
.setHighlightType(serializedNode.highlightType);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
exportJSON(): SerializedCodeHighlightNode {
|
|
128
|
+
return {
|
|
129
|
+
...super.exportJSON(),
|
|
130
|
+
highlightType: this.getHighlightType(),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Prevent formatting (bold, underline, etc)
|
|
135
|
+
setFormat(format: number): this {
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
isParentRequired(): true {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
createParentElementNode(): ElementNode {
|
|
144
|
+
return $createCodeNode();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getHighlightThemeClass(
|
|
149
|
+
theme: EditorThemeClasses,
|
|
150
|
+
highlightType: string | null | undefined,
|
|
151
|
+
): string | null | undefined {
|
|
152
|
+
return (
|
|
153
|
+
highlightType &&
|
|
154
|
+
theme &&
|
|
155
|
+
theme.codeHighlight &&
|
|
156
|
+
theme.codeHighlight[highlightType]
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function $createCodeHighlightNode(
|
|
161
|
+
text: string = '',
|
|
162
|
+
highlightType?: string | null | undefined,
|
|
163
|
+
): CodeHighlightNode {
|
|
164
|
+
return $applyNodeReplacement(new CodeHighlightNode(text, highlightType));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function $isCodeHighlightNode(
|
|
168
|
+
node: LexicalNode | CodeHighlightNode | null | undefined,
|
|
169
|
+
): node is CodeHighlightNode {
|
|
170
|
+
return node instanceof CodeHighlightNode;
|
|
171
|
+
}
|
|
@@ -0,0 +1,403 @@
|
|
|
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 {DOMPreprocessFn} from '@lexical/html';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
CoreImportExtension,
|
|
13
|
+
defineImportRule,
|
|
14
|
+
defineOverlayRules,
|
|
15
|
+
DOMImportExtension,
|
|
16
|
+
ImportOverlays,
|
|
17
|
+
sel,
|
|
18
|
+
} from '@lexical/html';
|
|
19
|
+
import {
|
|
20
|
+
$generateNodesFromRawText,
|
|
21
|
+
configExtension,
|
|
22
|
+
defineExtension,
|
|
23
|
+
isDOMDocumentNode,
|
|
24
|
+
isDOMTextNode,
|
|
25
|
+
isHTMLElement,
|
|
26
|
+
} from 'lexical';
|
|
27
|
+
|
|
28
|
+
import {CodeExtension} from './CodeExtension';
|
|
29
|
+
import {$createCodeNode} from './CodeNode';
|
|
30
|
+
|
|
31
|
+
const LANGUAGE_DATA_ATTRIBUTE = 'data-language';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* True for elements whose `font-family` mentions `monospace` — the
|
|
35
|
+
* heuristic the legacy `<div>` rule uses to spot copy-pasted code blocks
|
|
36
|
+
* (e.g. Google Docs serializes a code block as a styled `<div>`).
|
|
37
|
+
*/
|
|
38
|
+
function isMonospaceElement(el: HTMLElement): boolean {
|
|
39
|
+
return el.style.fontFamily.match('monospace') !== null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isMonospaceDescendant(node: HTMLElement): boolean {
|
|
43
|
+
let parent: HTMLElement | null = node.parentElement;
|
|
44
|
+
while (parent !== null) {
|
|
45
|
+
if (isMonospaceElement(parent)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
parent = parent.parentElement;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Overlay rules active only while {@link GitHubCodeTableRule} is
|
|
55
|
+
* processing its children. Inside the code-table subtree, every `<tr>`
|
|
56
|
+
* and `<td>` unwraps unconditionally — they never become table-row /
|
|
57
|
+
* table-cell nodes (even when `@lexical/table` registers its rules for
|
|
58
|
+
* those tags). Outside the subtree, this overlay isn't installed, so
|
|
59
|
+
* the cost of these rules is never paid against unrelated `<tr>` /
|
|
60
|
+
* `<td>` pastes.
|
|
61
|
+
*/
|
|
62
|
+
const GitHubCodeTableOverlayRules = defineOverlayRules([
|
|
63
|
+
defineImportRule({
|
|
64
|
+
$import: (ctx, el) => ctx.$importChildren(el),
|
|
65
|
+
match: sel.tag('tr', 'td'),
|
|
66
|
+
name: '@lexical/code/github-code-table/unwrap',
|
|
67
|
+
}),
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
const PreRule = defineImportRule({
|
|
71
|
+
$import: (ctx, el) => [
|
|
72
|
+
$createCodeNode(el.getAttribute(LANGUAGE_DATA_ATTRIBUTE)).splice(
|
|
73
|
+
0,
|
|
74
|
+
0,
|
|
75
|
+
ctx.$importChildren(el),
|
|
76
|
+
),
|
|
77
|
+
],
|
|
78
|
+
match: sel.tag('pre'),
|
|
79
|
+
name: '@lexical/code/pre',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Multi-line `<code>` (containing newlines or `<br>`) is treated as a
|
|
84
|
+
* block code element — mirrors the legacy behavior. Single-line `<code>`
|
|
85
|
+
* defers to the inline-format rule from `CoreImportExtension` so it
|
|
86
|
+
* becomes a TextNode with IS_CODE.
|
|
87
|
+
*/
|
|
88
|
+
const MultilineCodeRule = defineImportRule({
|
|
89
|
+
$import: (ctx, el, $next) => {
|
|
90
|
+
const text = el.textContent || '';
|
|
91
|
+
const isMultiLine = /\r?\n/.test(text) || el.querySelector('br') !== null;
|
|
92
|
+
if (!isMultiLine) {
|
|
93
|
+
return $next();
|
|
94
|
+
}
|
|
95
|
+
return [
|
|
96
|
+
$createCodeNode(el.getAttribute(LANGUAGE_DATA_ATTRIBUTE)).splice(
|
|
97
|
+
0,
|
|
98
|
+
0,
|
|
99
|
+
ctx.$importChildren(el),
|
|
100
|
+
),
|
|
101
|
+
];
|
|
102
|
+
},
|
|
103
|
+
match: sel.tag('code'),
|
|
104
|
+
name: '@lexical/code/code-multiline',
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* True for elements carrying BOTH `font-family: …monospace…` and
|
|
109
|
+
* `white-space: pre*` inline — the shape VS Code uses for every line
|
|
110
|
+
* of a copied code block (on every per-line `<div>` on Safari, on
|
|
111
|
+
* the single outer wrapper on Chrome).
|
|
112
|
+
*/
|
|
113
|
+
function isMonospacePreElement(el: Element): boolean {
|
|
114
|
+
if (!isHTMLElement(el)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const ff = el.style.fontFamily;
|
|
118
|
+
const ws = el.style.whiteSpace;
|
|
119
|
+
return (
|
|
120
|
+
typeof ff === 'string' &&
|
|
121
|
+
/monospace/i.test(ff) &&
|
|
122
|
+
typeof ws === 'string' &&
|
|
123
|
+
ws.startsWith('pre')
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Split a monospace-pre wrapper element into logical code lines:
|
|
129
|
+
* `<div>` children contribute their text content as one line,
|
|
130
|
+
* `<br>` children contribute an empty line, inline children (spans
|
|
131
|
+
* and bare text) accumulate into the current line until the next
|
|
132
|
+
* block child.
|
|
133
|
+
*
|
|
134
|
+
* Returns `null` if `el` has no block children (i.e. it's a leaf
|
|
135
|
+
* line, not a wrapper) so the caller can leave it to the
|
|
136
|
+
* sibling-run pass.
|
|
137
|
+
*/
|
|
138
|
+
function splitMonospaceWrapperLines(el: HTMLElement): string[] | null {
|
|
139
|
+
let hasBlockChild = false;
|
|
140
|
+
const lines: string[] = [];
|
|
141
|
+
let acc = '';
|
|
142
|
+
let hasAcc = false;
|
|
143
|
+
const flush = () => {
|
|
144
|
+
if (hasAcc) {
|
|
145
|
+
lines.push(acc);
|
|
146
|
+
acc = '';
|
|
147
|
+
hasAcc = false;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
for (const child of Array.from(el.childNodes)) {
|
|
151
|
+
if (isHTMLElement(child)) {
|
|
152
|
+
if (child.tagName === 'DIV') {
|
|
153
|
+
flush();
|
|
154
|
+
lines.push(child.textContent || '');
|
|
155
|
+
hasBlockChild = true;
|
|
156
|
+
} else if (child.tagName === 'BR') {
|
|
157
|
+
flush();
|
|
158
|
+
lines.push('');
|
|
159
|
+
hasBlockChild = true;
|
|
160
|
+
} else {
|
|
161
|
+
acc += child.textContent || '';
|
|
162
|
+
hasAcc = true;
|
|
163
|
+
}
|
|
164
|
+
} else if (isDOMTextNode(child)) {
|
|
165
|
+
const t = child.textContent || '';
|
|
166
|
+
if (t.length > 0) {
|
|
167
|
+
acc += t;
|
|
168
|
+
hasAcc = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
flush();
|
|
173
|
+
return hasBlockChild ? lines : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Returns `true` if `root` contains the structural signature of a
|
|
178
|
+
* VS Code code-block paste:
|
|
179
|
+
*
|
|
180
|
+
* - a monospace+pre `<div>` wrapper with at least one block (`<div>` /
|
|
181
|
+
* `<br>`) child — the Chrome shape, or
|
|
182
|
+
* - two or more consecutive monospace+pre siblings — the Safari shape.
|
|
183
|
+
*
|
|
184
|
+
* Walked once in preprocess; the matching overlay is only installed
|
|
185
|
+
* when this returns `true` so an unrelated paste doesn't pay for the
|
|
186
|
+
* detection or rule cost.
|
|
187
|
+
*/
|
|
188
|
+
function looksLikeVscodePaste(root: ParentNode): boolean {
|
|
189
|
+
for (const child of Array.from(root.children)) {
|
|
190
|
+
if (isHTMLElement(child) && isMonospacePreElement(child)) {
|
|
191
|
+
const lines = splitMonospaceWrapperLines(child);
|
|
192
|
+
if (lines !== null) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
const next = child.nextElementSibling;
|
|
196
|
+
if (next && isMonospacePreElement(next)) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (looksLikeVscodePaste(child)) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Match a monospace+pre `<div>` whose direct children include block
|
|
210
|
+
* (`<div>` / `<br>`) elements — the Chrome shape, one outer wrapper
|
|
211
|
+
* around per-line `<div>`s and `<br>`s. Emits a single CodeNode whose
|
|
212
|
+
* text is the wrapper's lines joined by `\n`.
|
|
213
|
+
*/
|
|
214
|
+
const VscodeWrapperRule = defineImportRule({
|
|
215
|
+
$import: (_ctx, el, $next) => {
|
|
216
|
+
if (!isMonospacePreElement(el) || isMonospaceDescendant(el)) {
|
|
217
|
+
return $next();
|
|
218
|
+
}
|
|
219
|
+
const lines = splitMonospaceWrapperLines(el);
|
|
220
|
+
if (lines === null || lines.length === 0) {
|
|
221
|
+
return $next();
|
|
222
|
+
}
|
|
223
|
+
return [
|
|
224
|
+
$createCodeNode().splice(
|
|
225
|
+
0,
|
|
226
|
+
0,
|
|
227
|
+
$generateNodesFromRawText(lines.join('\n')),
|
|
228
|
+
),
|
|
229
|
+
];
|
|
230
|
+
},
|
|
231
|
+
match: sel.tag('div'),
|
|
232
|
+
name: '@lexical/code/vscode-wrapper',
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Match the first of a run of consecutive monospace+pre `<div>` /
|
|
237
|
+
* `<br>` siblings (the Safari shape) and emit one CodeNode for the
|
|
238
|
+
* whole run. When the framework's per-child dispatch lands on a
|
|
239
|
+
* subsequent sibling in the same run, the prev-sibling check below
|
|
240
|
+
* returns `[]` so the run is only emitted once.
|
|
241
|
+
*/
|
|
242
|
+
const VscodeLineRunRule = defineImportRule({
|
|
243
|
+
$import: (_ctx, el, $next) => {
|
|
244
|
+
if (!isMonospacePreElement(el) || isMonospaceDescendant(el)) {
|
|
245
|
+
return $next();
|
|
246
|
+
}
|
|
247
|
+
const prev = el.previousElementSibling;
|
|
248
|
+
if (prev && isMonospacePreElement(prev)) {
|
|
249
|
+
// An earlier sibling's walk already absorbed `el` into its run.
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
const lines: string[] = [];
|
|
253
|
+
let cur: Element | null = el;
|
|
254
|
+
while (cur && isMonospacePreElement(cur)) {
|
|
255
|
+
lines.push(cur.tagName === 'BR' ? '' : cur.textContent || '');
|
|
256
|
+
cur = cur.nextElementSibling;
|
|
257
|
+
}
|
|
258
|
+
if (lines.length < 2) {
|
|
259
|
+
return $next();
|
|
260
|
+
}
|
|
261
|
+
return [
|
|
262
|
+
$createCodeNode().splice(
|
|
263
|
+
0,
|
|
264
|
+
0,
|
|
265
|
+
$generateNodesFromRawText(lines.join('\n')),
|
|
266
|
+
),
|
|
267
|
+
];
|
|
268
|
+
},
|
|
269
|
+
match: sel.tag('div', 'br'),
|
|
270
|
+
name: '@lexical/code/vscode-line-run',
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const VscodeCodePasteOverlay = defineOverlayRules([
|
|
274
|
+
VscodeWrapperRule,
|
|
275
|
+
VscodeLineRunRule,
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* VS Code → browser code-block pastes ship the block as either:
|
|
280
|
+
*
|
|
281
|
+
* - **Chrome**: one outer
|
|
282
|
+
* `<div style="font-family: …monospace…; white-space: pre">…</div>`
|
|
283
|
+
* wrapping per-line `<div>`s and `<br>`s.
|
|
284
|
+
* - **Safari**: a flat run of sibling
|
|
285
|
+
* `<div style="…monospace…; white-space: pre">…</div>` and
|
|
286
|
+
* `<br style="…monospace…; …">` elements with no wrapping
|
|
287
|
+
* monospace ancestor (the styles are duplicated onto every
|
|
288
|
+
* element).
|
|
289
|
+
*
|
|
290
|
+
* The legacy `<div>` rule (and {@link DivRule}) produces one CodeNode
|
|
291
|
+
* per `<div>` on Safari and concatenates inner divs without
|
|
292
|
+
* separating `\n`s on Chrome. This preprocess scans once for the
|
|
293
|
+
* structural signature and, only when it matches, pushes
|
|
294
|
+
* {@link VscodeCodePasteOverlay} onto {@link ImportOverlays} so the
|
|
295
|
+
* VS Code-specific rules participate in the walk. Pastes from other
|
|
296
|
+
* sources pay only the detection cost.
|
|
297
|
+
*
|
|
298
|
+
* @experimental
|
|
299
|
+
*/
|
|
300
|
+
export const $installVscodeCodePasteOverlay: DOMPreprocessFn = (
|
|
301
|
+
dom,
|
|
302
|
+
ctx,
|
|
303
|
+
$next,
|
|
304
|
+
) => {
|
|
305
|
+
const root: ParentNode = isDOMDocumentNode(dom) ? dom.body : dom;
|
|
306
|
+
if (looksLikeVscodePaste(root)) {
|
|
307
|
+
ctx.session.update(ImportOverlays, prev => [
|
|
308
|
+
...prev,
|
|
309
|
+
VscodeCodePasteOverlay,
|
|
310
|
+
]);
|
|
311
|
+
}
|
|
312
|
+
$next();
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* A `<div style="font-family: …monospace…">` (Google-Docs-style code
|
|
317
|
+
* block) creates a CodeNode. Descendant elements inside a monospace
|
|
318
|
+
* wrapper just unwrap so their text content flows into the surrounding
|
|
319
|
+
* CodeNode.
|
|
320
|
+
*/
|
|
321
|
+
const DivRule = defineImportRule({
|
|
322
|
+
$import: (ctx, el, $next) => {
|
|
323
|
+
if (isMonospaceElement(el)) {
|
|
324
|
+
return [$createCodeNode().splice(0, 0, ctx.$importChildren(el))];
|
|
325
|
+
}
|
|
326
|
+
if (isMonospaceDescendant(el)) {
|
|
327
|
+
// Unwrap so children flow into the enclosing CodeNode.
|
|
328
|
+
return ctx.$importChildren(el);
|
|
329
|
+
}
|
|
330
|
+
return $next();
|
|
331
|
+
},
|
|
332
|
+
match: sel.tag('div'),
|
|
333
|
+
name: '@lexical/code/div',
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* GitHub raw-file-view `<table class="js-file-line-container">` becomes
|
|
338
|
+
* a CodeNode. Walking the table's children pushes an overlay (see
|
|
339
|
+
* {@link GitHubCodeTableOverlayRules}) so `<tr>` / `<td>` inside this
|
|
340
|
+
* subtree unwrap unconditionally — without paying the predicate cost
|
|
341
|
+
* on every other `<tr>` / `<td>` paste elsewhere.
|
|
342
|
+
*/
|
|
343
|
+
const GitHubCodeTableRule = defineImportRule({
|
|
344
|
+
$import: (ctx, el) => [
|
|
345
|
+
$createCodeNode().splice(
|
|
346
|
+
0,
|
|
347
|
+
0,
|
|
348
|
+
ctx.$importChildren(el, {rules: GitHubCodeTableOverlayRules}),
|
|
349
|
+
),
|
|
350
|
+
],
|
|
351
|
+
match: sel.tag('table').classAll('js-file-line-container'),
|
|
352
|
+
name: '@lexical/code/github-code-table',
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Stray `<td class="js-file-line">` (cell with the explicit GitHub code-
|
|
357
|
+
* line class but no surrounding code-table wrapper) — unwrap so the
|
|
358
|
+
* descendant text flows up into whatever context the cell is in. The
|
|
359
|
+
* class is part of the selector itself, so no runtime guard.
|
|
360
|
+
*/
|
|
361
|
+
const GitHubCodeCellByClassRule = defineImportRule({
|
|
362
|
+
$import: (ctx, el) => ctx.$importChildren(el),
|
|
363
|
+
match: sel.tag('td').classAll('js-file-line'),
|
|
364
|
+
name: '@lexical/code/github-code-cell-by-class',
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Import rules for {@link CodeNode}.
|
|
369
|
+
*
|
|
370
|
+
* Specific class-restricted rules (GitHub raw-file-view detectors) are
|
|
371
|
+
* registered before the generic `<table>` / `<tr>` / `<td>` rules so
|
|
372
|
+
* they win dispatch.
|
|
373
|
+
*
|
|
374
|
+
* @experimental
|
|
375
|
+
*/
|
|
376
|
+
export const CodeImportRules = [
|
|
377
|
+
// Higher-priority (more-specific) rules first:
|
|
378
|
+
GitHubCodeTableRule,
|
|
379
|
+
GitHubCodeCellByClassRule,
|
|
380
|
+
MultilineCodeRule,
|
|
381
|
+
PreRule,
|
|
382
|
+
DivRule,
|
|
383
|
+
];
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Bundles {@link CodeImportRules} (plus {@link CoreImportExtension}) into
|
|
387
|
+
* a single dependency. The legacy {@link CodeNode.importDOM} continues to
|
|
388
|
+
* work in parallel; depend on this extension to opt into the new
|
|
389
|
+
* pipeline.
|
|
390
|
+
*
|
|
391
|
+
* @experimental
|
|
392
|
+
*/
|
|
393
|
+
export const CodeImportExtension = defineExtension({
|
|
394
|
+
dependencies: [
|
|
395
|
+
CoreImportExtension,
|
|
396
|
+
CodeExtension,
|
|
397
|
+
configExtension(DOMImportExtension, {
|
|
398
|
+
preprocess: [$installVscodeCodePasteOverlay],
|
|
399
|
+
rules: CodeImportRules,
|
|
400
|
+
}),
|
|
401
|
+
],
|
|
402
|
+
name: '@lexical/code/Import',
|
|
403
|
+
});
|