@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
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
import type { DOMPreprocessFn } from '@lexical/html';
|
|
9
|
+
/**
|
|
10
|
+
* VS Code → browser code-block pastes ship the block as either:
|
|
11
|
+
*
|
|
12
|
+
* - **Chrome**: one outer
|
|
13
|
+
* `<div style="font-family: …monospace…; white-space: pre">…</div>`
|
|
14
|
+
* wrapping per-line `<div>`s and `<br>`s.
|
|
15
|
+
* - **Safari**: a flat run of sibling
|
|
16
|
+
* `<div style="…monospace…; white-space: pre">…</div>` and
|
|
17
|
+
* `<br style="…monospace…; …">` elements with no wrapping
|
|
18
|
+
* monospace ancestor (the styles are duplicated onto every
|
|
19
|
+
* element).
|
|
20
|
+
*
|
|
21
|
+
* The legacy `<div>` rule (and {@link DivRule}) produces one CodeNode
|
|
22
|
+
* per `<div>` on Safari and concatenates inner divs without
|
|
23
|
+
* separating `\n`s on Chrome. This preprocess scans once for the
|
|
24
|
+
* structural signature and, only when it matches, pushes
|
|
25
|
+
* {@link VscodeCodePasteOverlay} onto {@link ImportOverlays} so the
|
|
26
|
+
* VS Code-specific rules participate in the walk. Pastes from other
|
|
27
|
+
* sources pay only the detection cost.
|
|
28
|
+
*
|
|
29
|
+
* @experimental
|
|
30
|
+
*/
|
|
31
|
+
export declare const $installVscodeCodePasteOverlay: DOMPreprocessFn;
|
|
32
|
+
/**
|
|
33
|
+
* Import rules for {@link CodeNode}.
|
|
34
|
+
*
|
|
35
|
+
* Specific class-restricted rules (GitHub raw-file-view detectors) are
|
|
36
|
+
* registered before the generic `<table>` / `<tr>` / `<td>` rules so
|
|
37
|
+
* they win dispatch.
|
|
38
|
+
*
|
|
39
|
+
* @experimental
|
|
40
|
+
*/
|
|
41
|
+
export declare const CodeImportRules: import("@lexical/html").DOMImportRule<import("@lexical/html").ElementSelectorBuilder<HTMLElement, Record<string, never>>>[];
|
|
42
|
+
/**
|
|
43
|
+
* Bundles {@link CodeImportRules} (plus {@link CoreImportExtension}) into
|
|
44
|
+
* a single dependency. The legacy {@link CodeNode.importDOM} continues to
|
|
45
|
+
* work in parallel; depend on this extension to opt into the new
|
|
46
|
+
* pipeline.
|
|
47
|
+
*
|
|
48
|
+
* @experimental
|
|
49
|
+
*/
|
|
50
|
+
export declare const CodeImportExtension: import("lexical").LexicalExtension<import("lexical").ExtensionConfigBase, "@lexical/code/Import", unknown, unknown>;
|
|
@@ -13,6 +13,7 @@ export type SerializedCodeNode = Spread<{
|
|
|
13
13
|
theme?: string | undefined;
|
|
14
14
|
}, SerializedElementNode>;
|
|
15
15
|
export declare const DEFAULT_CODE_LANGUAGE = "javascript";
|
|
16
|
+
/** @internal Configurable through the extensions. */
|
|
16
17
|
export declare const getDefaultCodeLanguage: () => string;
|
|
17
18
|
/** @noInheritDoc */
|
|
18
19
|
export declare class CodeNode extends ElementNode {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
import type { CodeHighlightNode } from './CodeHighlightNode';
|
|
9
|
-
import type { LineBreakNode, RangeSelection, TabNode } from 'lexical';
|
|
9
|
+
import type { LexicalNode, LineBreakNode, RangeSelection, TabNode } from 'lexical';
|
|
10
10
|
export declare function $getFirstCodeNodeOfLine(anchor: CodeHighlightNode | TabNode | LineBreakNode): CodeHighlightNode | TabNode | LineBreakNode;
|
|
11
11
|
export declare function $getLastCodeNodeOfLine(anchor: CodeHighlightNode | TabNode | LineBreakNode): CodeHighlightNode | TabNode | LineBreakNode;
|
|
12
12
|
/**
|
|
@@ -23,6 +23,15 @@ export declare function $getStartOfCodeInLine(anchor: CodeHighlightNode | TabNod
|
|
|
23
23
|
offset: number;
|
|
24
24
|
};
|
|
25
25
|
export declare function $getEndOfCodeInLine(anchor: CodeHighlightNode | TabNode): CodeHighlightNode | TabNode;
|
|
26
|
+
/**
|
|
27
|
+
* Plain split of code text into CodeHighlightNodes (with no highlight
|
|
28
|
+
* type) + LineBreakNodes + TabNodes. Used when the tokenizer opts out
|
|
29
|
+
* of a default language so a previously highlighted block still
|
|
30
|
+
* renders its `\n` / `\t` as real line breaks / tabs, while staying
|
|
31
|
+
* compatible with the indent / shift-lines handlers that only accept
|
|
32
|
+
* CodeHighlightNode + TabNode + LineBreakNode inside a CodeNode.
|
|
33
|
+
*/
|
|
34
|
+
export declare function $plainifyCodeContent(text: string): LexicalNode[];
|
|
26
35
|
/**
|
|
27
36
|
* Strip up to `tabSize` leading spaces from a {@link CodeHighlightNode} that
|
|
28
37
|
* starts a code line, to support outdenting space-indented code lines (e.g.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
var lexical = require('lexical');
|
|
12
12
|
var extension = require('@lexical/extension');
|
|
13
|
+
var html = require('@lexical/html');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
@@ -19,6 +20,7 @@ var extension = require('@lexical/extension');
|
|
|
19
20
|
*
|
|
20
21
|
*/
|
|
21
22
|
|
|
23
|
+
|
|
22
24
|
/*@__INLINE__*/
|
|
23
25
|
function warnOnlyOnce(message) {
|
|
24
26
|
{
|
|
@@ -192,6 +194,24 @@ function $getEndOfCodeInLine(anchor) {
|
|
|
192
194
|
return lastNode;
|
|
193
195
|
}
|
|
194
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Plain split of code text into CodeHighlightNodes (with no highlight
|
|
199
|
+
* type) + LineBreakNodes + TabNodes. Used when the tokenizer opts out
|
|
200
|
+
* of a default language so a previously highlighted block still
|
|
201
|
+
* renders its `\n` / `\t` as real line breaks / tabs, while staying
|
|
202
|
+
* compatible with the indent / shift-lines handlers that only accept
|
|
203
|
+
* CodeHighlightNode + TabNode + LineBreakNode inside a CodeNode.
|
|
204
|
+
*/
|
|
205
|
+
function $plainifyCodeContent(text) {
|
|
206
|
+
const out = [];
|
|
207
|
+
lexical.tokenizeRawText(text, {
|
|
208
|
+
linebreak: () => out.push(lexical.$createLineBreakNode()),
|
|
209
|
+
tab: () => out.push(lexical.$createTabNode()),
|
|
210
|
+
text: part => out.push($createCodeHighlightNode(part))
|
|
211
|
+
});
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
|
|
195
215
|
/**
|
|
196
216
|
* Strip up to `tabSize` leading spaces from a {@link CodeHighlightNode} that
|
|
197
217
|
* starts a code line, to support outdenting space-indented code lines (e.g.
|
|
@@ -238,6 +258,7 @@ function $outdentLeadingSpaces(node, tabSize, selection) {
|
|
|
238
258
|
*/
|
|
239
259
|
|
|
240
260
|
const DEFAULT_CODE_LANGUAGE = 'javascript';
|
|
261
|
+
/** @internal Configurable through the extensions. */
|
|
241
262
|
const getDefaultCodeLanguage = () => DEFAULT_CODE_LANGUAGE;
|
|
242
263
|
function hasChildDOMNodeTag(node, tagName) {
|
|
243
264
|
for (const child of node.childNodes) {
|
|
@@ -250,7 +271,7 @@ function hasChildDOMNodeTag(node, tagName) {
|
|
|
250
271
|
}
|
|
251
272
|
return false;
|
|
252
273
|
}
|
|
253
|
-
const LANGUAGE_DATA_ATTRIBUTE = 'data-language';
|
|
274
|
+
const LANGUAGE_DATA_ATTRIBUTE$1 = 'data-language';
|
|
254
275
|
const HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE = 'data-highlight-language';
|
|
255
276
|
const THEME_DATA_ATTRIBUTE = 'data-theme';
|
|
256
277
|
const noExtensionDeprecation = warnOnlyOnce('Using CodeNode without CodeExtension is deprecated');
|
|
@@ -289,7 +310,7 @@ class CodeNode extends lexical.ElementNode {
|
|
|
289
310
|
element.setAttribute('spellcheck', 'false');
|
|
290
311
|
const language = this.getLanguage();
|
|
291
312
|
if (language) {
|
|
292
|
-
element.setAttribute(LANGUAGE_DATA_ATTRIBUTE, language);
|
|
313
|
+
element.setAttribute(LANGUAGE_DATA_ATTRIBUTE$1, language);
|
|
293
314
|
if (this.getIsSyntaxHighlightSupported()) {
|
|
294
315
|
element.setAttribute(HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE, language);
|
|
295
316
|
}
|
|
@@ -309,10 +330,10 @@ class CodeNode extends lexical.ElementNode {
|
|
|
309
330
|
const prevLanguage = prevNode.__language;
|
|
310
331
|
if (language) {
|
|
311
332
|
if (language !== prevLanguage) {
|
|
312
|
-
dom.setAttribute(LANGUAGE_DATA_ATTRIBUTE, language);
|
|
333
|
+
dom.setAttribute(LANGUAGE_DATA_ATTRIBUTE$1, language);
|
|
313
334
|
}
|
|
314
335
|
} else if (prevLanguage) {
|
|
315
|
-
dom.removeAttribute(LANGUAGE_DATA_ATTRIBUTE);
|
|
336
|
+
dom.removeAttribute(LANGUAGE_DATA_ATTRIBUTE$1);
|
|
316
337
|
}
|
|
317
338
|
const isSyntaxHighlightSupported = this.__isSyntaxHighlightSupported;
|
|
318
339
|
const prevIsSyntaxHighlightSupported = prevNode.__isSyntaxHighlightSupported;
|
|
@@ -349,7 +370,7 @@ class CodeNode extends lexical.ElementNode {
|
|
|
349
370
|
element.setAttribute('spellcheck', 'false');
|
|
350
371
|
const language = this.getLanguage();
|
|
351
372
|
if (language) {
|
|
352
|
-
element.setAttribute(LANGUAGE_DATA_ATTRIBUTE, language);
|
|
373
|
+
element.setAttribute(LANGUAGE_DATA_ATTRIBUTE$1, language);
|
|
353
374
|
if (this.getIsSyntaxHighlightSupported()) {
|
|
354
375
|
element.setAttribute(HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE, language);
|
|
355
376
|
}
|
|
@@ -548,7 +569,7 @@ function $isCodeNode(node) {
|
|
|
548
569
|
return node instanceof CodeNode;
|
|
549
570
|
}
|
|
550
571
|
function $convertPreElement(domNode) {
|
|
551
|
-
const language = domNode.getAttribute(LANGUAGE_DATA_ATTRIBUTE);
|
|
572
|
+
const language = domNode.getAttribute(LANGUAGE_DATA_ATTRIBUTE$1);
|
|
552
573
|
return {
|
|
553
574
|
node: $createCodeNode(language)
|
|
554
575
|
};
|
|
@@ -736,6 +757,331 @@ const CodeExtension = lexical.defineExtension({
|
|
|
736
757
|
}
|
|
737
758
|
});
|
|
738
759
|
|
|
760
|
+
/**
|
|
761
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
762
|
+
*
|
|
763
|
+
* This source code is licensed under the MIT license found in the
|
|
764
|
+
* LICENSE file in the root directory of this source tree.
|
|
765
|
+
*
|
|
766
|
+
*/
|
|
767
|
+
|
|
768
|
+
const LANGUAGE_DATA_ATTRIBUTE = 'data-language';
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* True for elements whose `font-family` mentions `monospace` — the
|
|
772
|
+
* heuristic the legacy `<div>` rule uses to spot copy-pasted code blocks
|
|
773
|
+
* (e.g. Google Docs serializes a code block as a styled `<div>`).
|
|
774
|
+
*/
|
|
775
|
+
function isMonospaceElement(el) {
|
|
776
|
+
return el.style.fontFamily.match('monospace') !== null;
|
|
777
|
+
}
|
|
778
|
+
function isMonospaceDescendant(node) {
|
|
779
|
+
let parent = node.parentElement;
|
|
780
|
+
while (parent !== null) {
|
|
781
|
+
if (isMonospaceElement(parent)) {
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
parent = parent.parentElement;
|
|
785
|
+
}
|
|
786
|
+
return false;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Overlay rules active only while {@link GitHubCodeTableRule} is
|
|
791
|
+
* processing its children. Inside the code-table subtree, every `<tr>`
|
|
792
|
+
* and `<td>` unwraps unconditionally — they never become table-row /
|
|
793
|
+
* table-cell nodes (even when `@lexical/table` registers its rules for
|
|
794
|
+
* those tags). Outside the subtree, this overlay isn't installed, so
|
|
795
|
+
* the cost of these rules is never paid against unrelated `<tr>` /
|
|
796
|
+
* `<td>` pastes.
|
|
797
|
+
*/
|
|
798
|
+
const GitHubCodeTableOverlayRules = html.defineOverlayRules([html.defineImportRule({
|
|
799
|
+
$import: (ctx, el) => ctx.$importChildren(el),
|
|
800
|
+
match: html.sel.tag('tr', 'td'),
|
|
801
|
+
name: '@lexical/code/github-code-table/unwrap'
|
|
802
|
+
})]);
|
|
803
|
+
const PreRule = html.defineImportRule({
|
|
804
|
+
$import: (ctx, el) => [$createCodeNode(el.getAttribute(LANGUAGE_DATA_ATTRIBUTE)).splice(0, 0, ctx.$importChildren(el))],
|
|
805
|
+
match: html.sel.tag('pre'),
|
|
806
|
+
name: '@lexical/code/pre'
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Multi-line `<code>` (containing newlines or `<br>`) is treated as a
|
|
811
|
+
* block code element — mirrors the legacy behavior. Single-line `<code>`
|
|
812
|
+
* defers to the inline-format rule from `CoreImportExtension` so it
|
|
813
|
+
* becomes a TextNode with IS_CODE.
|
|
814
|
+
*/
|
|
815
|
+
const MultilineCodeRule = html.defineImportRule({
|
|
816
|
+
$import: (ctx, el, $next) => {
|
|
817
|
+
const text = el.textContent || '';
|
|
818
|
+
const isMultiLine = /\r?\n/.test(text) || el.querySelector('br') !== null;
|
|
819
|
+
if (!isMultiLine) {
|
|
820
|
+
return $next();
|
|
821
|
+
}
|
|
822
|
+
return [$createCodeNode(el.getAttribute(LANGUAGE_DATA_ATTRIBUTE)).splice(0, 0, ctx.$importChildren(el))];
|
|
823
|
+
},
|
|
824
|
+
match: html.sel.tag('code'),
|
|
825
|
+
name: '@lexical/code/code-multiline'
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* True for elements carrying BOTH `font-family: …monospace…` and
|
|
830
|
+
* `white-space: pre*` inline — the shape VS Code uses for every line
|
|
831
|
+
* of a copied code block (on every per-line `<div>` on Safari, on
|
|
832
|
+
* the single outer wrapper on Chrome).
|
|
833
|
+
*/
|
|
834
|
+
function isMonospacePreElement(el) {
|
|
835
|
+
if (!lexical.isHTMLElement(el)) {
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
const ff = el.style.fontFamily;
|
|
839
|
+
const ws = el.style.whiteSpace;
|
|
840
|
+
return typeof ff === 'string' && /monospace/i.test(ff) && typeof ws === 'string' && ws.startsWith('pre');
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Split a monospace-pre wrapper element into logical code lines:
|
|
845
|
+
* `<div>` children contribute their text content as one line,
|
|
846
|
+
* `<br>` children contribute an empty line, inline children (spans
|
|
847
|
+
* and bare text) accumulate into the current line until the next
|
|
848
|
+
* block child.
|
|
849
|
+
*
|
|
850
|
+
* Returns `null` if `el` has no block children (i.e. it's a leaf
|
|
851
|
+
* line, not a wrapper) so the caller can leave it to the
|
|
852
|
+
* sibling-run pass.
|
|
853
|
+
*/
|
|
854
|
+
function splitMonospaceWrapperLines(el) {
|
|
855
|
+
let hasBlockChild = false;
|
|
856
|
+
const lines = [];
|
|
857
|
+
let acc = '';
|
|
858
|
+
let hasAcc = false;
|
|
859
|
+
const flush = () => {
|
|
860
|
+
if (hasAcc) {
|
|
861
|
+
lines.push(acc);
|
|
862
|
+
acc = '';
|
|
863
|
+
hasAcc = false;
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
for (const child of Array.from(el.childNodes)) {
|
|
867
|
+
if (lexical.isHTMLElement(child)) {
|
|
868
|
+
if (child.tagName === 'DIV') {
|
|
869
|
+
flush();
|
|
870
|
+
lines.push(child.textContent || '');
|
|
871
|
+
hasBlockChild = true;
|
|
872
|
+
} else if (child.tagName === 'BR') {
|
|
873
|
+
flush();
|
|
874
|
+
lines.push('');
|
|
875
|
+
hasBlockChild = true;
|
|
876
|
+
} else {
|
|
877
|
+
acc += child.textContent || '';
|
|
878
|
+
hasAcc = true;
|
|
879
|
+
}
|
|
880
|
+
} else if (lexical.isDOMTextNode(child)) {
|
|
881
|
+
const t = child.textContent || '';
|
|
882
|
+
if (t.length > 0) {
|
|
883
|
+
acc += t;
|
|
884
|
+
hasAcc = true;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
flush();
|
|
889
|
+
return hasBlockChild ? lines : null;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Returns `true` if `root` contains the structural signature of a
|
|
894
|
+
* VS Code code-block paste:
|
|
895
|
+
*
|
|
896
|
+
* - a monospace+pre `<div>` wrapper with at least one block (`<div>` /
|
|
897
|
+
* `<br>`) child — the Chrome shape, or
|
|
898
|
+
* - two or more consecutive monospace+pre siblings — the Safari shape.
|
|
899
|
+
*
|
|
900
|
+
* Walked once in preprocess; the matching overlay is only installed
|
|
901
|
+
* when this returns `true` so an unrelated paste doesn't pay for the
|
|
902
|
+
* detection or rule cost.
|
|
903
|
+
*/
|
|
904
|
+
function looksLikeVscodePaste(root) {
|
|
905
|
+
for (const child of Array.from(root.children)) {
|
|
906
|
+
if (lexical.isHTMLElement(child) && isMonospacePreElement(child)) {
|
|
907
|
+
const lines = splitMonospaceWrapperLines(child);
|
|
908
|
+
if (lines !== null) {
|
|
909
|
+
return true;
|
|
910
|
+
}
|
|
911
|
+
const next = child.nextElementSibling;
|
|
912
|
+
if (next && isMonospacePreElement(next)) {
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
if (looksLikeVscodePaste(child)) {
|
|
918
|
+
return true;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Match a monospace+pre `<div>` whose direct children include block
|
|
926
|
+
* (`<div>` / `<br>`) elements — the Chrome shape, one outer wrapper
|
|
927
|
+
* around per-line `<div>`s and `<br>`s. Emits a single CodeNode whose
|
|
928
|
+
* text is the wrapper's lines joined by `\n`.
|
|
929
|
+
*/
|
|
930
|
+
const VscodeWrapperRule = html.defineImportRule({
|
|
931
|
+
$import: (_ctx, el, $next) => {
|
|
932
|
+
if (!isMonospacePreElement(el) || isMonospaceDescendant(el)) {
|
|
933
|
+
return $next();
|
|
934
|
+
}
|
|
935
|
+
const lines = splitMonospaceWrapperLines(el);
|
|
936
|
+
if (lines === null || lines.length === 0) {
|
|
937
|
+
return $next();
|
|
938
|
+
}
|
|
939
|
+
return [$createCodeNode().splice(0, 0, lexical.$generateNodesFromRawText(lines.join('\n')))];
|
|
940
|
+
},
|
|
941
|
+
match: html.sel.tag('div'),
|
|
942
|
+
name: '@lexical/code/vscode-wrapper'
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Match the first of a run of consecutive monospace+pre `<div>` /
|
|
947
|
+
* `<br>` siblings (the Safari shape) and emit one CodeNode for the
|
|
948
|
+
* whole run. When the framework's per-child dispatch lands on a
|
|
949
|
+
* subsequent sibling in the same run, the prev-sibling check below
|
|
950
|
+
* returns `[]` so the run is only emitted once.
|
|
951
|
+
*/
|
|
952
|
+
const VscodeLineRunRule = html.defineImportRule({
|
|
953
|
+
$import: (_ctx, el, $next) => {
|
|
954
|
+
if (!isMonospacePreElement(el) || isMonospaceDescendant(el)) {
|
|
955
|
+
return $next();
|
|
956
|
+
}
|
|
957
|
+
const prev = el.previousElementSibling;
|
|
958
|
+
if (prev && isMonospacePreElement(prev)) {
|
|
959
|
+
// An earlier sibling's walk already absorbed `el` into its run.
|
|
960
|
+
return [];
|
|
961
|
+
}
|
|
962
|
+
const lines = [];
|
|
963
|
+
let cur = el;
|
|
964
|
+
while (cur && isMonospacePreElement(cur)) {
|
|
965
|
+
lines.push(cur.tagName === 'BR' ? '' : cur.textContent || '');
|
|
966
|
+
cur = cur.nextElementSibling;
|
|
967
|
+
}
|
|
968
|
+
if (lines.length < 2) {
|
|
969
|
+
return $next();
|
|
970
|
+
}
|
|
971
|
+
return [$createCodeNode().splice(0, 0, lexical.$generateNodesFromRawText(lines.join('\n')))];
|
|
972
|
+
},
|
|
973
|
+
match: html.sel.tag('div', 'br'),
|
|
974
|
+
name: '@lexical/code/vscode-line-run'
|
|
975
|
+
});
|
|
976
|
+
const VscodeCodePasteOverlay = html.defineOverlayRules([VscodeWrapperRule, VscodeLineRunRule]);
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* VS Code → browser code-block pastes ship the block as either:
|
|
980
|
+
*
|
|
981
|
+
* - **Chrome**: one outer
|
|
982
|
+
* `<div style="font-family: …monospace…; white-space: pre">…</div>`
|
|
983
|
+
* wrapping per-line `<div>`s and `<br>`s.
|
|
984
|
+
* - **Safari**: a flat run of sibling
|
|
985
|
+
* `<div style="…monospace…; white-space: pre">…</div>` and
|
|
986
|
+
* `<br style="…monospace…; …">` elements with no wrapping
|
|
987
|
+
* monospace ancestor (the styles are duplicated onto every
|
|
988
|
+
* element).
|
|
989
|
+
*
|
|
990
|
+
* The legacy `<div>` rule (and {@link DivRule}) produces one CodeNode
|
|
991
|
+
* per `<div>` on Safari and concatenates inner divs without
|
|
992
|
+
* separating `\n`s on Chrome. This preprocess scans once for the
|
|
993
|
+
* structural signature and, only when it matches, pushes
|
|
994
|
+
* {@link VscodeCodePasteOverlay} onto {@link ImportOverlays} so the
|
|
995
|
+
* VS Code-specific rules participate in the walk. Pastes from other
|
|
996
|
+
* sources pay only the detection cost.
|
|
997
|
+
*
|
|
998
|
+
* @experimental
|
|
999
|
+
*/
|
|
1000
|
+
const $installVscodeCodePasteOverlay = (dom, ctx, $next) => {
|
|
1001
|
+
const root = lexical.isDOMDocumentNode(dom) ? dom.body : dom;
|
|
1002
|
+
if (looksLikeVscodePaste(root)) {
|
|
1003
|
+
ctx.session.update(html.ImportOverlays, prev => [...prev, VscodeCodePasteOverlay]);
|
|
1004
|
+
}
|
|
1005
|
+
$next();
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* A `<div style="font-family: …monospace…">` (Google-Docs-style code
|
|
1010
|
+
* block) creates a CodeNode. Descendant elements inside a monospace
|
|
1011
|
+
* wrapper just unwrap so their text content flows into the surrounding
|
|
1012
|
+
* CodeNode.
|
|
1013
|
+
*/
|
|
1014
|
+
const DivRule = html.defineImportRule({
|
|
1015
|
+
$import: (ctx, el, $next) => {
|
|
1016
|
+
if (isMonospaceElement(el)) {
|
|
1017
|
+
return [$createCodeNode().splice(0, 0, ctx.$importChildren(el))];
|
|
1018
|
+
}
|
|
1019
|
+
if (isMonospaceDescendant(el)) {
|
|
1020
|
+
// Unwrap so children flow into the enclosing CodeNode.
|
|
1021
|
+
return ctx.$importChildren(el);
|
|
1022
|
+
}
|
|
1023
|
+
return $next();
|
|
1024
|
+
},
|
|
1025
|
+
match: html.sel.tag('div'),
|
|
1026
|
+
name: '@lexical/code/div'
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* GitHub raw-file-view `<table class="js-file-line-container">` becomes
|
|
1031
|
+
* a CodeNode. Walking the table's children pushes an overlay (see
|
|
1032
|
+
* {@link GitHubCodeTableOverlayRules}) so `<tr>` / `<td>` inside this
|
|
1033
|
+
* subtree unwrap unconditionally — without paying the predicate cost
|
|
1034
|
+
* on every other `<tr>` / `<td>` paste elsewhere.
|
|
1035
|
+
*/
|
|
1036
|
+
const GitHubCodeTableRule = html.defineImportRule({
|
|
1037
|
+
$import: (ctx, el) => [$createCodeNode().splice(0, 0, ctx.$importChildren(el, {
|
|
1038
|
+
rules: GitHubCodeTableOverlayRules
|
|
1039
|
+
}))],
|
|
1040
|
+
match: html.sel.tag('table').classAll('js-file-line-container'),
|
|
1041
|
+
name: '@lexical/code/github-code-table'
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Stray `<td class="js-file-line">` (cell with the explicit GitHub code-
|
|
1046
|
+
* line class but no surrounding code-table wrapper) — unwrap so the
|
|
1047
|
+
* descendant text flows up into whatever context the cell is in. The
|
|
1048
|
+
* class is part of the selector itself, so no runtime guard.
|
|
1049
|
+
*/
|
|
1050
|
+
const GitHubCodeCellByClassRule = html.defineImportRule({
|
|
1051
|
+
$import: (ctx, el) => ctx.$importChildren(el),
|
|
1052
|
+
match: html.sel.tag('td').classAll('js-file-line'),
|
|
1053
|
+
name: '@lexical/code/github-code-cell-by-class'
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Import rules for {@link CodeNode}.
|
|
1058
|
+
*
|
|
1059
|
+
* Specific class-restricted rules (GitHub raw-file-view detectors) are
|
|
1060
|
+
* registered before the generic `<table>` / `<tr>` / `<td>` rules so
|
|
1061
|
+
* they win dispatch.
|
|
1062
|
+
*
|
|
1063
|
+
* @experimental
|
|
1064
|
+
*/
|
|
1065
|
+
const CodeImportRules = [
|
|
1066
|
+
// Higher-priority (more-specific) rules first:
|
|
1067
|
+
GitHubCodeTableRule, GitHubCodeCellByClassRule, MultilineCodeRule, PreRule, DivRule];
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Bundles {@link CodeImportRules} (plus {@link CoreImportExtension}) into
|
|
1071
|
+
* a single dependency. The legacy {@link CodeNode.importDOM} continues to
|
|
1072
|
+
* work in parallel; depend on this extension to opt into the new
|
|
1073
|
+
* pipeline.
|
|
1074
|
+
*
|
|
1075
|
+
* @experimental
|
|
1076
|
+
*/
|
|
1077
|
+
const CodeImportExtension = lexical.defineExtension({
|
|
1078
|
+
dependencies: [html.CoreImportExtension, CodeExtension, lexical.configExtension(html.DOMImportExtension, {
|
|
1079
|
+
preprocess: [$installVscodeCodePasteOverlay],
|
|
1080
|
+
rules: CodeImportRules
|
|
1081
|
+
})],
|
|
1082
|
+
name: '@lexical/code/Import'
|
|
1083
|
+
});
|
|
1084
|
+
|
|
739
1085
|
function $isSelectionInCode(selection) {
|
|
740
1086
|
if (!lexical.$isRangeSelection(selection)) {
|
|
741
1087
|
return false;
|
|
@@ -1029,6 +1375,15 @@ function $handleMoveTo(type, event) {
|
|
|
1029
1375
|
const focusLineNode = focusNode;
|
|
1030
1376
|
const direction = $getCodeLineDirection(focusLineNode);
|
|
1031
1377
|
const moveToStart = direction === 'rtl' ? !isMoveToStart : isMoveToStart;
|
|
1378
|
+
|
|
1379
|
+
// Shift variant: let the non-shift branches resolve the target via
|
|
1380
|
+
// framework helpers (`selectNext` / `selectStart` / `setTextNodeRange` /
|
|
1381
|
+
// `node.select`), then restore the original anchor so we end up with an
|
|
1382
|
+
// extended selection rather than a collapsed caret. This keeps point
|
|
1383
|
+
// shapes (text vs. element) consistent between shift and non-shift.
|
|
1384
|
+
const originalAnchorKey = anchor.key;
|
|
1385
|
+
const originalAnchorOffset = anchor.offset;
|
|
1386
|
+
const originalAnchorType = anchor.type;
|
|
1032
1387
|
if (moveToStart) {
|
|
1033
1388
|
const start = $getStartOfCodeInLine(focusLineNode, focus.offset);
|
|
1034
1389
|
if (start !== null) {
|
|
@@ -1048,6 +1403,9 @@ function $handleMoveTo(type, event) {
|
|
|
1048
1403
|
const node = $getEndOfCodeInLine(focusLineNode);
|
|
1049
1404
|
node.select();
|
|
1050
1405
|
}
|
|
1406
|
+
if (event.shiftKey) {
|
|
1407
|
+
selection.anchor.set(originalAnchorKey, originalAnchorOffset, originalAnchorType);
|
|
1408
|
+
}
|
|
1051
1409
|
event.preventDefault();
|
|
1052
1410
|
event.stopPropagation();
|
|
1053
1411
|
return true;
|
|
@@ -1162,8 +1520,11 @@ exports.$getStartOfCodeInLine = $getStartOfCodeInLine;
|
|
|
1162
1520
|
exports.$isCodeHighlightNode = $isCodeHighlightNode;
|
|
1163
1521
|
exports.$isCodeNode = $isCodeNode;
|
|
1164
1522
|
exports.$outdentLeadingSpaces = $outdentLeadingSpaces;
|
|
1523
|
+
exports.$plainifyCodeContent = $plainifyCodeContent;
|
|
1165
1524
|
exports.CodeExtension = CodeExtension;
|
|
1166
1525
|
exports.CodeHighlightNode = CodeHighlightNode;
|
|
1526
|
+
exports.CodeImportExtension = CodeImportExtension;
|
|
1527
|
+
exports.CodeImportRules = CodeImportRules;
|
|
1167
1528
|
exports.CodeIndentExtension = CodeIndentExtension;
|
|
1168
1529
|
exports.CodeNode = CodeNode;
|
|
1169
1530
|
exports.DEFAULT_CODE_LANGUAGE = DEFAULT_CODE_LANGUAGE;
|