@lexical/html 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/{DOMRenderExtension.d.ts → dist/DOMRenderExtension.d.ts} +12 -1
- package/dist/DOMRenderRuntime.d.ts +51 -0
- package/dist/LexicalHtml.dev.js +3289 -0
- package/dist/LexicalHtml.dev.mjs +3242 -0
- package/{LexicalHtml.js.flow → dist/LexicalHtml.js.flow} +16 -16
- package/dist/LexicalHtml.mjs +57 -0
- package/dist/LexicalHtml.node.mjs +55 -0
- package/dist/LexicalHtml.prod.js +9 -0
- package/dist/LexicalHtml.prod.mjs +9 -0
- package/dist/RenderContext.d.ts +68 -0
- package/{compileDOMRenderConfigOverrides.d.ts → dist/compileDOMRenderConfigOverrides.d.ts} +1 -1
- package/{constants.d.ts → dist/constants.d.ts} +2 -0
- package/dist/domOverride.d.ts +23 -0
- package/dist/import/CoreImportExtension.d.ts +11 -0
- package/dist/import/DOMImportExtension.d.ts +82 -0
- package/dist/import/HorizontalRuleImportExtension.d.ts +28 -0
- package/dist/import/ImportContext.d.ts +208 -0
- package/dist/import/compileImportRules.d.ts +50 -0
- package/dist/import/coreImportRules.d.ts +25 -0
- package/dist/import/defineImportRule.d.ts +32 -0
- package/dist/import/defineOverlayRules.d.ts +66 -0
- package/dist/import/index.d.ts +38 -0
- package/dist/import/inlineStylesFromStyleSheets.d.ts +28 -0
- package/dist/import/parseCss.d.ts +18 -0
- package/dist/import/runImport.d.ts +19 -0
- package/dist/import/schemas.d.ts +106 -0
- package/dist/import/sel.d.ts +74 -0
- package/dist/import/types.d.ts +394 -0
- package/dist/index.d.ts +44 -0
- package/{types.d.ts → dist/types.d.ts} +96 -8
- package/package.json +33 -18
- package/src/ContextRecord.ts +243 -0
- package/src/DOMRenderExtension.ts +96 -0
- package/src/DOMRenderRuntime.ts +265 -0
- package/src/RenderContext.ts +168 -0
- package/src/compileDOMRenderConfigOverrides.ts +416 -0
- package/src/constants.ts +18 -0
- package/src/domOverride.ts +46 -0
- package/src/import/CoreImportExtension.ts +26 -0
- package/src/import/DOMImportExtension.ts +221 -0
- package/src/import/HorizontalRuleImportExtension.ts +52 -0
- package/src/import/ImportContext.ts +339 -0
- package/src/import/compileImportRules.ts +178 -0
- package/src/import/coreImportRules.ts +545 -0
- package/src/import/defineImportRule.ts +40 -0
- package/src/import/defineOverlayRules.ts +105 -0
- package/src/import/index.ts +97 -0
- package/src/import/inlineStylesFromStyleSheets.ts +104 -0
- package/src/import/parseCss.ts +219 -0
- package/src/import/runImport.ts +245 -0
- package/src/import/schemas.ts +280 -0
- package/src/import/sel.ts +314 -0
- package/src/import/types.ts +471 -0
- package/src/index.ts +561 -0
- package/src/types.ts +470 -0
- package/LexicalHtml.dev.js +0 -914
- package/LexicalHtml.dev.mjs +0 -900
- package/LexicalHtml.mjs +0 -24
- package/LexicalHtml.node.mjs +0 -22
- package/LexicalHtml.prod.js +0 -9
- package/LexicalHtml.prod.mjs +0 -9
- package/RenderContext.d.ts +0 -32
- package/domOverride.d.ts +0 -18
- package/index.d.ts +0 -32
- /package/{ContextRecord.d.ts → dist/ContextRecord.d.ts} +0 -0
- /package/{LexicalHtml.js → dist/LexicalHtml.js} +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
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 {parseSelector} from './parseCss';
|
|
9
|
+
import {selBase} from './sel';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Combinator-and-parser-based builder for {@link CompiledSelector}s. The
|
|
13
|
+
* runtime shape returned by these factory methods is opaque; consumers
|
|
14
|
+
* should never inspect or construct selector objects directly.
|
|
15
|
+
*
|
|
16
|
+
* @experimental
|
|
17
|
+
*/
|
|
18
|
+
export const sel = {
|
|
19
|
+
any: selBase.any,
|
|
20
|
+
comment: selBase.comment,
|
|
21
|
+
/**
|
|
22
|
+
* Parse a reduced CSS-selector subset and return a builder you can chain
|
|
23
|
+
* combinator methods off of.
|
|
24
|
+
*/
|
|
25
|
+
css: parseSelector,
|
|
26
|
+
tag: selBase.tag,
|
|
27
|
+
text: selBase.text,
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
export {CoreImportExtension} from './CoreImportExtension';
|
|
31
|
+
export {CoreImportRules} from './coreImportRules';
|
|
32
|
+
export {defineImportRule} from './defineImportRule';
|
|
33
|
+
export {
|
|
34
|
+
type CompiledOverlayRules,
|
|
35
|
+
defineOverlayRules,
|
|
36
|
+
type DOMImportRuleEntry,
|
|
37
|
+
} from './defineOverlayRules';
|
|
38
|
+
export {
|
|
39
|
+
$generateNodesFromDOMViaExtension,
|
|
40
|
+
type DOMImportConfig,
|
|
41
|
+
DOMImportExtension,
|
|
42
|
+
} from './DOMImportExtension';
|
|
43
|
+
export {
|
|
44
|
+
HorizontalRuleImportExtension,
|
|
45
|
+
HorizontalRuleImportRules,
|
|
46
|
+
} from './HorizontalRuleImportExtension';
|
|
47
|
+
export {
|
|
48
|
+
$getImportContextValue,
|
|
49
|
+
$withImportContext,
|
|
50
|
+
createImportState,
|
|
51
|
+
defaultIsInline,
|
|
52
|
+
defaultPreservesWhitespace,
|
|
53
|
+
ImportOverlays,
|
|
54
|
+
ImportSource,
|
|
55
|
+
ImportSourceDataTransfer,
|
|
56
|
+
type ImportSourceKind,
|
|
57
|
+
ImportTextFormat,
|
|
58
|
+
ImportTextStyle,
|
|
59
|
+
ImportWhitespaceConfig,
|
|
60
|
+
type IsInlineForWhitespace,
|
|
61
|
+
type IsPreserveWhitespaceDom,
|
|
62
|
+
type WhitespaceImportConfig,
|
|
63
|
+
} from './ImportContext';
|
|
64
|
+
export {$inlineStylesFromStyleSheets} from './inlineStylesFromStyleSheets';
|
|
65
|
+
export {parseSelector} from './parseCss';
|
|
66
|
+
export {
|
|
67
|
+
$distributeInlineWrapper,
|
|
68
|
+
$isBlockLevel,
|
|
69
|
+
$propagateTextAlignToBlockChildren,
|
|
70
|
+
BlockSchema,
|
|
71
|
+
InlineSchema,
|
|
72
|
+
NestedBlockSchema,
|
|
73
|
+
RootSchema,
|
|
74
|
+
} from './schemas';
|
|
75
|
+
export {isElementOfTag} from './sel';
|
|
76
|
+
export type {
|
|
77
|
+
AnyDOMImportRule,
|
|
78
|
+
AttrMatchOptions,
|
|
79
|
+
CapturesOfSelector,
|
|
80
|
+
ChildSchema,
|
|
81
|
+
CompiledSelector,
|
|
82
|
+
DOMImportContext,
|
|
83
|
+
DOMImportExtensionOutput,
|
|
84
|
+
DOMImportFn,
|
|
85
|
+
DOMImportRule,
|
|
86
|
+
DOMPreprocessContext,
|
|
87
|
+
DOMPreprocessFn,
|
|
88
|
+
ElementSelectorBuilder,
|
|
89
|
+
GenerateNodesFromDOMOptions,
|
|
90
|
+
ImportChildrenOpts,
|
|
91
|
+
ImportContextPairOrUpdater,
|
|
92
|
+
ImportNodeOpts,
|
|
93
|
+
ImportSession,
|
|
94
|
+
ImportStateConfig,
|
|
95
|
+
NodeOfSelector,
|
|
96
|
+
StyleMatchOptions,
|
|
97
|
+
} from './types';
|
|
@@ -0,0 +1,104 @@
|
|
|
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 './types';
|
|
9
|
+
|
|
10
|
+
import {objectKlassEquals} from '@lexical/utils';
|
|
11
|
+
import {isDOMDocumentNode, isHTMLElement} from 'lexical';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Inlines CSS rules from `<style>` tags onto matching elements as inline
|
|
15
|
+
* styles.
|
|
16
|
+
*
|
|
17
|
+
* Used by apps like Excel that generate HTML where styles live in
|
|
18
|
+
* class-based `<style>` rules (e.g. `.xl65 { background: #FFFF00; color:
|
|
19
|
+
* blue; }`) rather than inline styles. Since Lexical's import converters
|
|
20
|
+
* read inline styles, we resolve stylesheet rules into inline styles
|
|
21
|
+
* before conversion.
|
|
22
|
+
*
|
|
23
|
+
* Mutates the DOM in-place. Original inline styles always take
|
|
24
|
+
* precedence over stylesheet rules (matching CSS specificity behavior).
|
|
25
|
+
*
|
|
26
|
+
* No-op for {@link ParentNode}s that are not {@link Document}s — only a
|
|
27
|
+
* full document carries `styleSheets` we can iterate.
|
|
28
|
+
*
|
|
29
|
+
* @experimental
|
|
30
|
+
*/
|
|
31
|
+
export const $inlineStylesFromStyleSheets: DOMPreprocessFn = (
|
|
32
|
+
dom,
|
|
33
|
+
_ctx,
|
|
34
|
+
$next,
|
|
35
|
+
) => {
|
|
36
|
+
$inlineStylesFromStyleSheetsDOM(dom);
|
|
37
|
+
$next();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function $inlineStylesFromStyleSheetsDOM(
|
|
41
|
+
dom: Document | ParentNode,
|
|
42
|
+
): void {
|
|
43
|
+
if (!isDOMDocumentNode(dom)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const doc = dom;
|
|
47
|
+
if (doc.querySelector('style') === null) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const originalInlineStyles = new Map<HTMLElement, Set<string>>();
|
|
52
|
+
|
|
53
|
+
function getOriginalInlineProps(el: HTMLElement): Set<string> {
|
|
54
|
+
let props = originalInlineStyles.get(el);
|
|
55
|
+
if (props === undefined) {
|
|
56
|
+
props = new Set<string>();
|
|
57
|
+
for (let i = 0; i < el.style.length; i++) {
|
|
58
|
+
props.add(el.style[i]);
|
|
59
|
+
}
|
|
60
|
+
originalInlineStyles.set(el, props);
|
|
61
|
+
}
|
|
62
|
+
return props;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
for (const sheet of Array.from(doc.styleSheets)) {
|
|
67
|
+
let rules: CSSRuleList;
|
|
68
|
+
try {
|
|
69
|
+
rules = sheet.cssRules;
|
|
70
|
+
} catch {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
for (const rule of Array.from(rules)) {
|
|
74
|
+
if (!objectKlassEquals(rule, CSSStyleRule)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
let elements: NodeListOf<Element>;
|
|
78
|
+
try {
|
|
79
|
+
elements = doc.querySelectorAll(rule.selectorText);
|
|
80
|
+
} catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
for (const el of Array.from(elements)) {
|
|
84
|
+
if (!isHTMLElement(el)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const originalProps = getOriginalInlineProps(el);
|
|
88
|
+
for (let i = 0; i < rule.style.length; i++) {
|
|
89
|
+
const prop = rule.style[i];
|
|
90
|
+
if (!originalProps.has(prop)) {
|
|
91
|
+
el.style.setProperty(
|
|
92
|
+
prop,
|
|
93
|
+
rule.style.getPropertyValue(prop),
|
|
94
|
+
rule.style.getPropertyPriority(prop),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// styleSheets API not supported in this environment
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
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 {Predicate} from './sel';
|
|
9
|
+
import type {ElementSelectorBuilder} from './types';
|
|
10
|
+
|
|
11
|
+
import invariant from '@lexical/internal/invariant';
|
|
12
|
+
|
|
13
|
+
import {buildAttrPredicate, buildClassAllPredicate, buildSelector} from './sel';
|
|
14
|
+
|
|
15
|
+
const IDENT_CHAR = /[A-Za-z0-9_-]/;
|
|
16
|
+
|
|
17
|
+
class Cursor {
|
|
18
|
+
constructor(
|
|
19
|
+
public readonly source: string,
|
|
20
|
+
public pos: number,
|
|
21
|
+
) {}
|
|
22
|
+
peek(offset = 0): string {
|
|
23
|
+
return this.source[this.pos + offset] || '';
|
|
24
|
+
}
|
|
25
|
+
consume(): string {
|
|
26
|
+
return this.source[this.pos++] || '';
|
|
27
|
+
}
|
|
28
|
+
eof(): boolean {
|
|
29
|
+
return this.pos >= this.source.length;
|
|
30
|
+
}
|
|
31
|
+
skipWhitespace(): void {
|
|
32
|
+
while (!this.eof() && /\s/.test(this.peek())) {
|
|
33
|
+
this.pos++;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
readIdent(): string {
|
|
37
|
+
const start = this.pos;
|
|
38
|
+
while (!this.eof() && IDENT_CHAR.test(this.peek())) {
|
|
39
|
+
this.pos++;
|
|
40
|
+
}
|
|
41
|
+
return this.source.slice(start, this.pos);
|
|
42
|
+
}
|
|
43
|
+
readQuoted(): string {
|
|
44
|
+
const quote = this.consume();
|
|
45
|
+
this.assert(quote === '"' || quote === "'", 'expected quote');
|
|
46
|
+
const start = this.pos;
|
|
47
|
+
while (!this.eof() && this.peek() !== quote) {
|
|
48
|
+
if (this.peek() === '\\') {
|
|
49
|
+
this.pos += 2;
|
|
50
|
+
} else {
|
|
51
|
+
this.pos++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
this.assert(!this.eof(), 'unterminated string');
|
|
55
|
+
const value = this.source.slice(start, this.pos);
|
|
56
|
+
this.pos++; // consume closing quote
|
|
57
|
+
return value.replace(/\\(.)/g, '$1');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `invariant(cond, fmt, …)`-flavored assertion that also surfaces the
|
|
61
|
+
* cursor's position context. Use for parse-time errors so a malformed
|
|
62
|
+
* CSS selector gets a useful, position-annotated message.
|
|
63
|
+
*/
|
|
64
|
+
assert(cond: boolean, msg: string): asserts cond {
|
|
65
|
+
invariant(
|
|
66
|
+
cond,
|
|
67
|
+
'invalid CSS selector at col %s: %s in %s',
|
|
68
|
+
String(this.pos + 1),
|
|
69
|
+
msg,
|
|
70
|
+
this.source,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface ParsedSimpleSelector {
|
|
76
|
+
readonly tags: Set<string>;
|
|
77
|
+
readonly predicates: Predicate[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseSimpleSelector(c: Cursor): ParsedSimpleSelector {
|
|
81
|
+
const tags = new Set<string>();
|
|
82
|
+
const predicates: Predicate[] = [];
|
|
83
|
+
const classes: string[] = [];
|
|
84
|
+
|
|
85
|
+
c.skipWhitespace();
|
|
86
|
+
|
|
87
|
+
// Optional tag or '*'
|
|
88
|
+
if (c.peek() === '*') {
|
|
89
|
+
c.consume();
|
|
90
|
+
} else if (IDENT_CHAR.test(c.peek())) {
|
|
91
|
+
const tag = c.readIdent();
|
|
92
|
+
if (tag) {
|
|
93
|
+
tags.add(tag.toUpperCase());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Zero or more refinements: .class, #id, [attr]
|
|
98
|
+
while (!c.eof()) {
|
|
99
|
+
const ch = c.peek();
|
|
100
|
+
if (ch === '.') {
|
|
101
|
+
c.consume();
|
|
102
|
+
const cls = c.readIdent();
|
|
103
|
+
c.assert(cls !== '', 'expected class name after "."');
|
|
104
|
+
classes.push(cls);
|
|
105
|
+
} else if (ch === '#') {
|
|
106
|
+
c.consume();
|
|
107
|
+
const id = c.readIdent();
|
|
108
|
+
c.assert(id !== '', 'expected id after "#"');
|
|
109
|
+
predicates.push(buildAttrPredicate('id', id));
|
|
110
|
+
} else if (ch === '[') {
|
|
111
|
+
c.consume();
|
|
112
|
+
c.skipWhitespace();
|
|
113
|
+
const name = c.readIdent();
|
|
114
|
+
c.assert(name !== '', 'expected attribute name after "["');
|
|
115
|
+
c.skipWhitespace();
|
|
116
|
+
let value: true | string = true;
|
|
117
|
+
if (c.peek() === '=') {
|
|
118
|
+
c.consume();
|
|
119
|
+
c.skipWhitespace();
|
|
120
|
+
const next = c.peek();
|
|
121
|
+
if (next === '"' || next === "'") {
|
|
122
|
+
value = c.readQuoted();
|
|
123
|
+
} else {
|
|
124
|
+
value = c.readIdent();
|
|
125
|
+
c.assert(value !== '', 'expected attribute value');
|
|
126
|
+
}
|
|
127
|
+
c.skipWhitespace();
|
|
128
|
+
}
|
|
129
|
+
c.assert(c.peek() === ']', 'expected "]"');
|
|
130
|
+
c.consume();
|
|
131
|
+
predicates.push(buildAttrPredicate(name, value));
|
|
132
|
+
} else {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (classes.length > 0) {
|
|
138
|
+
predicates.push(buildClassAllPredicate(classes));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {predicates, tags};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Parse a reduced CSS-selector subset and return a {@link CompiledSelector}.
|
|
146
|
+
* Supported:
|
|
147
|
+
* - Tag (`p`), wildcard (`*`).
|
|
148
|
+
* - Tag list (`h1, h2, h3`).
|
|
149
|
+
* - Class (`.foo`, `.foo.bar`).
|
|
150
|
+
* - ID (`#foo`).
|
|
151
|
+
* - Attribute presence (`[name]`).
|
|
152
|
+
* - Attribute equality (`[name="value"]`, `[name=value]`).
|
|
153
|
+
*
|
|
154
|
+
* Anything outside the subset (regex attribute, inline-style match,
|
|
155
|
+
* combinators, pseudo-classes) is intentionally rejected — chain combinator
|
|
156
|
+
* methods off the returned builder instead.
|
|
157
|
+
*
|
|
158
|
+
* @experimental
|
|
159
|
+
*/
|
|
160
|
+
export function parseSelector(
|
|
161
|
+
source: string,
|
|
162
|
+
): ElementSelectorBuilder<HTMLElement> {
|
|
163
|
+
const c: Cursor = new Cursor(source, 0);
|
|
164
|
+
const groups: ParsedSimpleSelector[] = [];
|
|
165
|
+
|
|
166
|
+
while (true) {
|
|
167
|
+
const group = parseSimpleSelector(c);
|
|
168
|
+
if (group.tags.size === 0 && group.predicates.length === 0) {
|
|
169
|
+
// Empty group with neither tag nor refinement — only OK if it came
|
|
170
|
+
// from the lone `*` (which produces zero tags but no preds either).
|
|
171
|
+
// We accept this as "wildcard element".
|
|
172
|
+
}
|
|
173
|
+
groups.push(group);
|
|
174
|
+
c.skipWhitespace();
|
|
175
|
+
if (c.eof()) {
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
c.assert(
|
|
179
|
+
c.peek() === ',',
|
|
180
|
+
'expected "," (selector lists are the only supported combinator)',
|
|
181
|
+
);
|
|
182
|
+
c.consume();
|
|
183
|
+
c.skipWhitespace();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (groups.length === 1) {
|
|
187
|
+
return buildSelector(groups[0].tags, groups[0].predicates);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Comma-separated list. Merge tag sets and OR-combine the per-group
|
|
191
|
+
// refinement predicates so that each candidate node satisfies *some*
|
|
192
|
+
// group entirely.
|
|
193
|
+
const tags = new Set<string>();
|
|
194
|
+
for (const g of groups) {
|
|
195
|
+
for (const t of g.tags) {
|
|
196
|
+
tags.add(t);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const orPredicate: Predicate = (node, captures) => {
|
|
200
|
+
for (const g of groups) {
|
|
201
|
+
const upper = node.nodeName;
|
|
202
|
+
if (g.tags.size > 0 && !g.tags.has(upper)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
let ok = true;
|
|
206
|
+
for (const p of g.predicates) {
|
|
207
|
+
if (!p(node, captures)) {
|
|
208
|
+
ok = false;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (ok) {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
};
|
|
218
|
+
return buildSelector(tags, [orPredicate]);
|
|
219
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
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 {
|
|
9
|
+
ChildSchema,
|
|
10
|
+
DOMImportContext,
|
|
11
|
+
ImportChildrenOpts,
|
|
12
|
+
ImportNodeOpts,
|
|
13
|
+
ImportSession,
|
|
14
|
+
ImportStateConfig,
|
|
15
|
+
} from './types';
|
|
16
|
+
|
|
17
|
+
import {isDOMDocumentNode, type LexicalEditor, type LexicalNode} from 'lexical';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
type CompiledDispatch,
|
|
21
|
+
type CompiledRule,
|
|
22
|
+
getDispatchIndices,
|
|
23
|
+
} from './compileImportRules';
|
|
24
|
+
import {
|
|
25
|
+
$getImportContextValue,
|
|
26
|
+
$withImportContext,
|
|
27
|
+
ImportOverlays,
|
|
28
|
+
} from './ImportContext';
|
|
29
|
+
import {$applySchema, RootSchema} from './schemas';
|
|
30
|
+
|
|
31
|
+
const __DEV__ = process.env.NODE_ENV !== 'production';
|
|
32
|
+
|
|
33
|
+
const NO_CAPTURES: Record<string, RegExpMatchArray> = Object.freeze(
|
|
34
|
+
{} as Record<string, RegExpMatchArray>,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
interface Runtime {
|
|
38
|
+
readonly dispatch: CompiledDispatch;
|
|
39
|
+
readonly editor: LexicalEditor;
|
|
40
|
+
readonly session: ImportSession;
|
|
41
|
+
/**
|
|
42
|
+
* Stack of overlay dispatchers installed via `$importChildren({rules})`.
|
|
43
|
+
* The most recently pushed overlay is at the highest index and is
|
|
44
|
+
* tried first; rules within an overlay are dispatched in their own
|
|
45
|
+
* registration order. When all overlay rules for a node have been
|
|
46
|
+
* exhausted (or have called `$next()` to defer), the main dispatcher
|
|
47
|
+
* is consulted. This lets app rules scope cost-bearing predicates to
|
|
48
|
+
* the subtrees where they apply.
|
|
49
|
+
*/
|
|
50
|
+
readonly overlays: CompiledDispatch[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeContext(
|
|
54
|
+
runtime: Runtime,
|
|
55
|
+
captures: Readonly<Record<string, RegExpMatchArray>>,
|
|
56
|
+
): DOMImportContext<Record<string, RegExpMatchArray>> {
|
|
57
|
+
const ctx: DOMImportContext<Record<string, RegExpMatchArray>> = {
|
|
58
|
+
$importChildren: (parent, opts) =>
|
|
59
|
+
$importChildrenInternal(runtime, parent, opts),
|
|
60
|
+
$importOne: (node, opts) => $importOneInternal(runtime, node, opts),
|
|
61
|
+
captures,
|
|
62
|
+
get<V>(cfg: ImportStateConfig<V>): V {
|
|
63
|
+
return $getImportContextValue(cfg, runtime.editor);
|
|
64
|
+
},
|
|
65
|
+
session: runtime.session,
|
|
66
|
+
};
|
|
67
|
+
return ctx;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function $importChildrenInternal(
|
|
71
|
+
runtime: Runtime,
|
|
72
|
+
parent: ParentNode,
|
|
73
|
+
opts: ImportChildrenOpts | undefined,
|
|
74
|
+
): LexicalNode[] {
|
|
75
|
+
const overlay = opts && opts.rules ? opts.rules.dispatch : undefined;
|
|
76
|
+
if (overlay) {
|
|
77
|
+
runtime.overlays.push(overlay);
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const run = () => $importChildrenRun(runtime, parent, opts);
|
|
81
|
+
return opts && opts.context
|
|
82
|
+
? $withImportContext(opts.context, runtime.editor)(run)
|
|
83
|
+
: run();
|
|
84
|
+
} finally {
|
|
85
|
+
if (overlay) {
|
|
86
|
+
runtime.overlays.pop();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function $importChildrenRun(
|
|
92
|
+
runtime: Runtime,
|
|
93
|
+
parent: ParentNode,
|
|
94
|
+
opts: ImportChildrenOpts | undefined,
|
|
95
|
+
): LexicalNode[] {
|
|
96
|
+
const onChild = opts && opts.$onChild;
|
|
97
|
+
const collected: LexicalNode[] = [];
|
|
98
|
+
for (const child of Array.from(parent.childNodes)) {
|
|
99
|
+
const produced = $importOneInternal(runtime, child, undefined);
|
|
100
|
+
for (const lex of produced) {
|
|
101
|
+
const result = onChild ? onChild(lex) : lex;
|
|
102
|
+
if (result != null) {
|
|
103
|
+
collected.push(result);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const afterApplied = opts && opts.$after ? opts.$after(collected) : collected;
|
|
108
|
+
const schema: ChildSchema | undefined = opts && opts.schema;
|
|
109
|
+
if (!schema) {
|
|
110
|
+
return afterApplied;
|
|
111
|
+
}
|
|
112
|
+
return $applySchema(schema, afterApplied, null, parent);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function $importOneInternal(
|
|
116
|
+
runtime: Runtime,
|
|
117
|
+
node: Node,
|
|
118
|
+
opts: ImportNodeOpts | undefined,
|
|
119
|
+
): LexicalNode[] {
|
|
120
|
+
const run = () => $dispatch(runtime, node);
|
|
121
|
+
const out =
|
|
122
|
+
opts && opts.context
|
|
123
|
+
? $withImportContext(opts.context, runtime.editor)(run)
|
|
124
|
+
: run();
|
|
125
|
+
// Surface to callers as a mutable array per the DOMImportContext contract.
|
|
126
|
+
return out as LexicalNode[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Build the candidate (dispatch, indices) list for `node`. Overlays are
|
|
131
|
+
* tried first in top-of-stack order; the main dispatcher comes last. The
|
|
132
|
+
* `$next()` chain walks through all of them in sequence — an overlay rule
|
|
133
|
+
* can defer to a lower overlay rule, or all the way through to a main
|
|
134
|
+
* rule, just by calling `$next()`.
|
|
135
|
+
*/
|
|
136
|
+
function getCandidates(
|
|
137
|
+
runtime: Runtime,
|
|
138
|
+
node: Node,
|
|
139
|
+
): readonly {dispatch: CompiledDispatch; indices: readonly number[]}[] {
|
|
140
|
+
const candidates: {
|
|
141
|
+
dispatch: CompiledDispatch;
|
|
142
|
+
indices: readonly number[];
|
|
143
|
+
}[] = [];
|
|
144
|
+
for (let i = runtime.overlays.length - 1; i >= 0; i--) {
|
|
145
|
+
const d = runtime.overlays[i];
|
|
146
|
+
const idx = getDispatchIndices(d, node);
|
|
147
|
+
if (idx.length > 0) {
|
|
148
|
+
candidates.push({dispatch: d, indices: idx});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const mainIdx = getDispatchIndices(runtime.dispatch, node);
|
|
152
|
+
if (mainIdx.length > 0) {
|
|
153
|
+
candidates.push({dispatch: runtime.dispatch, indices: mainIdx});
|
|
154
|
+
}
|
|
155
|
+
return candidates;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function $dispatch(runtime: Runtime, node: Node): readonly LexicalNode[] {
|
|
159
|
+
const candidates = getCandidates(runtime, node);
|
|
160
|
+
if (candidates.length === 0) {
|
|
161
|
+
return $hoistChildrenOf(runtime, node);
|
|
162
|
+
}
|
|
163
|
+
let groupCursor = 0;
|
|
164
|
+
let ruleCursor = 0;
|
|
165
|
+
const $next = (): readonly LexicalNode[] => {
|
|
166
|
+
while (groupCursor < candidates.length) {
|
|
167
|
+
const {dispatch, indices} = candidates[groupCursor];
|
|
168
|
+
while (ruleCursor < indices.length) {
|
|
169
|
+
const idx = indices[ruleCursor++];
|
|
170
|
+
const rule: CompiledRule = dispatch.rules[idx];
|
|
171
|
+
const captures: Record<string, RegExpMatchArray> = {};
|
|
172
|
+
if (rule.predicate(node, captures)) {
|
|
173
|
+
const ctx = makeContext(
|
|
174
|
+
runtime,
|
|
175
|
+
Object.keys(captures).length === 0 ? NO_CAPTURES : captures,
|
|
176
|
+
);
|
|
177
|
+
try {
|
|
178
|
+
return rule.$import(ctx, node, $next);
|
|
179
|
+
} catch (e) {
|
|
180
|
+
if (__DEV__) {
|
|
181
|
+
console.error(
|
|
182
|
+
`[lexical] DOM import rule "${rule.name}" threw on node`,
|
|
183
|
+
node,
|
|
184
|
+
e,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
throw e;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
groupCursor++;
|
|
192
|
+
ruleCursor = 0;
|
|
193
|
+
}
|
|
194
|
+
return $hoistChildrenOf(runtime, node);
|
|
195
|
+
};
|
|
196
|
+
return $next();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Fallback when no rule matched and `$next()` was called past the end of the
|
|
201
|
+
* chain: hoist the element's children to take its place, recursively. Pure
|
|
202
|
+
* elements with no rule become invisible, matching the legacy
|
|
203
|
+
* `$createNodesFromDOM` hoisting behavior.
|
|
204
|
+
*/
|
|
205
|
+
function $hoistChildrenOf(runtime: Runtime, node: Node): LexicalNode[] {
|
|
206
|
+
if (node.childNodes.length === 0) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
const collected: LexicalNode[] = [];
|
|
210
|
+
for (const child of Array.from(node.childNodes)) {
|
|
211
|
+
const produced = $importOneInternal(runtime, child, undefined);
|
|
212
|
+
for (const lex of produced) {
|
|
213
|
+
collected.push(lex);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return collected;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Top-level walker for a compiled dispatcher. Iterates the DOM children of
|
|
221
|
+
* `dom` (using the document body if a {@link Document} is passed) and
|
|
222
|
+
* applies `RootSchema` to the produced lexical nodes so runs of inlines are
|
|
223
|
+
* wrapped in paragraphs — same shape as the legacy `$generateNodesFromDOM`.
|
|
224
|
+
*
|
|
225
|
+
* @internal
|
|
226
|
+
*/
|
|
227
|
+
export function $runImport(
|
|
228
|
+
dispatch: CompiledDispatch,
|
|
229
|
+
editor: LexicalEditor,
|
|
230
|
+
dom: Document | ParentNode,
|
|
231
|
+
session: ImportSession,
|
|
232
|
+
): LexicalNode[] {
|
|
233
|
+
// Prime the overlay stack with any overlays a preprocess wrote to
|
|
234
|
+
// ImportOverlays. These remain in effect for the entire walk; nested
|
|
235
|
+
// `$importChildren({rules})` calls push on top.
|
|
236
|
+
const installed = session.get(ImportOverlays);
|
|
237
|
+
const runtime: Runtime = {
|
|
238
|
+
dispatch,
|
|
239
|
+
editor,
|
|
240
|
+
overlays: installed.map(o => o.dispatch),
|
|
241
|
+
session,
|
|
242
|
+
};
|
|
243
|
+
const rootParent: ParentNode = isDOMDocumentNode(dom) ? dom.body : dom;
|
|
244
|
+
return $importChildrenRun(runtime, rootParent, {schema: RootSchema});
|
|
245
|
+
}
|