@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,280 @@
|
|
|
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 {ChildSchema} from './types';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
$createParagraphNode,
|
|
12
|
+
$isBlockElementNode,
|
|
13
|
+
$isDecoratorNode,
|
|
14
|
+
$isElementNode,
|
|
15
|
+
$isLineBreakNode,
|
|
16
|
+
type ElementNode,
|
|
17
|
+
isHTMLElement,
|
|
18
|
+
type LexicalNode,
|
|
19
|
+
} from 'lexical';
|
|
20
|
+
|
|
21
|
+
import {isAlignmentValue} from './coreImportRules';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* True if the node fills a block slot at the root or inside another
|
|
25
|
+
* block — covers both ElementNode-style blocks (paragraph, heading,
|
|
26
|
+
* quote) and block-level DecoratorNodes (HorizontalRuleNode,
|
|
27
|
+
* ImageNode-as-block, etc.). Used by {@link BlockSchema},
|
|
28
|
+
* {@link RootSchema}, and {@link NestedBlockSchema}.
|
|
29
|
+
*
|
|
30
|
+
* @experimental
|
|
31
|
+
*/
|
|
32
|
+
export function $isBlockLevel(node: LexicalNode): boolean {
|
|
33
|
+
return (
|
|
34
|
+
$isBlockElementNode(node) || ($isDecoratorNode(node) && !node.isInline())
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Distribute an inline wrapper (`LinkNode`, `MarkNode`, …) across a
|
|
40
|
+
* heterogeneous run of children produced by `$importChildren`, lifting
|
|
41
|
+
* any block children to the top level while keeping the wrapper around
|
|
42
|
+
* the leaf inline content.
|
|
43
|
+
*
|
|
44
|
+
* Use from a rule whose DOM source is an inline element that the
|
|
45
|
+
* browser permitted to enclose block elements — the canonical case is
|
|
46
|
+
* `<a href="…"><h1>title</h1><div>body</div></a>`, which a link rule
|
|
47
|
+
* wants to surface as two block siblings (heading + paragraph), each
|
|
48
|
+
* with its own link wrapping the original inline content. Schemas
|
|
49
|
+
* can't express this because they reason about a parent's children
|
|
50
|
+
* only — they cannot lift the parent out of itself.
|
|
51
|
+
*
|
|
52
|
+
* For each top-level child:
|
|
53
|
+
* - **Inline children** are collected into runs; each run is wrapped
|
|
54
|
+
* in a single fresh wrapper (from `$makeWrapper()`).
|
|
55
|
+
* - **Block children** are descended into: their own children are
|
|
56
|
+
* recursively distributed with `$makeWrapper`, then re-attached so
|
|
57
|
+
* the block keeps its position at the top level.
|
|
58
|
+
*
|
|
59
|
+
* The returned list will contain a mix of blocks and wrapped inline
|
|
60
|
+
* runs. The enclosing schema (typically {@link BlockSchema}) will
|
|
61
|
+
* then package those inline wrappers into paragraphs as usual.
|
|
62
|
+
*
|
|
63
|
+
* @experimental
|
|
64
|
+
*/
|
|
65
|
+
export function $distributeInlineWrapper(
|
|
66
|
+
children: readonly LexicalNode[],
|
|
67
|
+
$makeWrapper: () => ElementNode,
|
|
68
|
+
): LexicalNode[] {
|
|
69
|
+
const out: LexicalNode[] = [];
|
|
70
|
+
let inlineRun: LexicalNode[] = [];
|
|
71
|
+
|
|
72
|
+
const flushInline = () => {
|
|
73
|
+
if (inlineRun.length === 0) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
out.push($makeWrapper().splice(0, 0, inlineRun));
|
|
77
|
+
inlineRun = [];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
for (const child of children) {
|
|
81
|
+
if ($isBlockLevel(child)) {
|
|
82
|
+
flushInline();
|
|
83
|
+
// Recursively distribute the wrapper into the block's own
|
|
84
|
+
// children. A block DecoratorNode (no children) is left alone.
|
|
85
|
+
if ($isElementNode(child)) {
|
|
86
|
+
const wrapped = $distributeInlineWrapper(
|
|
87
|
+
child.getChildren(),
|
|
88
|
+
$makeWrapper,
|
|
89
|
+
);
|
|
90
|
+
child.splice(0, child.getChildrenSize(), wrapped);
|
|
91
|
+
}
|
|
92
|
+
out.push(child);
|
|
93
|
+
} else {
|
|
94
|
+
inlineRun.push(child);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
flushInline();
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Apply a {@link ChildSchema} to a flat list of children produced by
|
|
103
|
+
* `$importChildren`. Walks the list once, partitions into accepted vs.
|
|
104
|
+
* rejected runs, packages or drops rejected runs, then runs `$finalize`.
|
|
105
|
+
*
|
|
106
|
+
* @internal
|
|
107
|
+
*/
|
|
108
|
+
export function $applySchema(
|
|
109
|
+
schema: ChildSchema,
|
|
110
|
+
children: LexicalNode[],
|
|
111
|
+
parent: LexicalNode | null,
|
|
112
|
+
domParent: Node | null,
|
|
113
|
+
): LexicalNode[] {
|
|
114
|
+
const out: LexicalNode[] = [];
|
|
115
|
+
let run: LexicalNode[] | null = null;
|
|
116
|
+
|
|
117
|
+
const flushRun = () => {
|
|
118
|
+
if (run === null) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const rejected = run;
|
|
122
|
+
run = null;
|
|
123
|
+
if (schema.$packageRun) {
|
|
124
|
+
const packaged = schema.$packageRun(rejected, parent, domParent);
|
|
125
|
+
if (packaged.length > 0) {
|
|
126
|
+
for (const n of packaged) {
|
|
127
|
+
out.push(n);
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// No $packageRun (or it returned []) — apply onReject. 'drop' (default)
|
|
133
|
+
// discards the run. 'hoist' lets it through unchanged at this level.
|
|
134
|
+
if (schema.onReject === 'hoist') {
|
|
135
|
+
for (const n of rejected) {
|
|
136
|
+
out.push(n);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
for (const child of children) {
|
|
142
|
+
if (schema.$accepts(child, parent)) {
|
|
143
|
+
flushRun();
|
|
144
|
+
out.push(child);
|
|
145
|
+
} else {
|
|
146
|
+
if (run === null) {
|
|
147
|
+
run = [];
|
|
148
|
+
}
|
|
149
|
+
run.push(child);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
flushRun();
|
|
153
|
+
|
|
154
|
+
return schema.$finalize ? schema.$finalize(out, parent) : out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Apply a parent DOM element's `text-align` (when set to one of the
|
|
159
|
+
* supported {@link ElementFormatType} values) to each block-level child
|
|
160
|
+
* Lexical node that does not yet have its own format.
|
|
161
|
+
*
|
|
162
|
+
* Mirrors the part of the legacy `wrapContinuousInlines` that wrote
|
|
163
|
+
* `node.setFormat(textAlign)` onto pre-existing block children when the
|
|
164
|
+
* DOM parent carried `style.textAlign`. Pair with
|
|
165
|
+
* {@link $paragraphPackageRun} (which carries the same propagation onto
|
|
166
|
+
* paragraphs synthesized around inline runs) to fully replicate the
|
|
167
|
+
* legacy behavior on a run of mixed children.
|
|
168
|
+
*
|
|
169
|
+
* @experimental
|
|
170
|
+
*/
|
|
171
|
+
export function $propagateTextAlignToBlockChildren(
|
|
172
|
+
children: LexicalNode[],
|
|
173
|
+
domParent: Node | null,
|
|
174
|
+
): LexicalNode[] {
|
|
175
|
+
if (!isHTMLElement(domParent)) {
|
|
176
|
+
return children;
|
|
177
|
+
}
|
|
178
|
+
const textAlign = domParent.style.textAlign;
|
|
179
|
+
if (!isAlignmentValue(textAlign)) {
|
|
180
|
+
return children;
|
|
181
|
+
}
|
|
182
|
+
for (const child of children) {
|
|
183
|
+
if ($isBlockElementNode(child) && child.getFormatType() === '') {
|
|
184
|
+
child.setFormat(textAlign);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return children;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Wrap a run of inline lexical nodes in a fresh paragraph, propagating the
|
|
192
|
+
* `text-align` of `domParent` as the paragraph's format type (matching the
|
|
193
|
+
* legacy `wrapContinuousInlines` behavior).
|
|
194
|
+
*/
|
|
195
|
+
function $paragraphPackageRun(
|
|
196
|
+
run: LexicalNode[],
|
|
197
|
+
_parent: LexicalNode | null,
|
|
198
|
+
domParent: Node | null,
|
|
199
|
+
): LexicalNode[] {
|
|
200
|
+
// Mirror the legacy `$wrapInlineNodes` (driven by
|
|
201
|
+
// `selection.insertNodes`) shortcut where a lone `<br>` at this
|
|
202
|
+
// level (a `LineBreakNode` is the only thing in the rejected run)
|
|
203
|
+
// becomes an *empty* paragraph rather than a paragraph wrapping a
|
|
204
|
+
// visible line break — that's the form clipboard pastes ending in a
|
|
205
|
+
// trailing `<br>` (Google Docs, Gmail, …) rely on for the editor's
|
|
206
|
+
// "extra trailing empty line" expectation.
|
|
207
|
+
if (run.length === 1 && $isLineBreakNode(run[0])) {
|
|
208
|
+
run = [];
|
|
209
|
+
}
|
|
210
|
+
const paragraph = $createParagraphNode();
|
|
211
|
+
if (isHTMLElement(domParent)) {
|
|
212
|
+
const textAlign = domParent.style.textAlign;
|
|
213
|
+
if (isAlignmentValue(textAlign)) {
|
|
214
|
+
paragraph.setFormat(textAlign);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return [paragraph.splice(0, 0, run)];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Default schema for block-level positions (root of the document, the body
|
|
222
|
+
* of a block element node). Accepts block lexical nodes; packages runs of
|
|
223
|
+
* inline children into fresh paragraph nodes.
|
|
224
|
+
*
|
|
225
|
+
* @experimental
|
|
226
|
+
*/
|
|
227
|
+
export const BlockSchema: ChildSchema = {
|
|
228
|
+
$accepts: $isBlockLevel,
|
|
229
|
+
$packageRun: $paragraphPackageRun,
|
|
230
|
+
name: 'BlockSchema',
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Schema for inline-only positions (the body of an inline lexical node such
|
|
235
|
+
* as a link). Accepts non-block lexical nodes; runs of block children are
|
|
236
|
+
* dropped (`onReject: 'drop'` is the default).
|
|
237
|
+
*
|
|
238
|
+
* @experimental
|
|
239
|
+
*/
|
|
240
|
+
export const InlineSchema: ChildSchema = {
|
|
241
|
+
$accepts: child => !$isBlockLevel(child),
|
|
242
|
+
name: 'InlineSchema',
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Schema for nested block positions — the equivalent of the legacy
|
|
247
|
+
* `ArtificialNode__DO_NOT_USE` flow used when a block DOM element appears
|
|
248
|
+
* inside another block lexical ancestor. Accepts block nodes; runs of inline
|
|
249
|
+
* children are emitted with a line break between consecutive runs (instead
|
|
250
|
+
* of being wrapped in a paragraph, which would introduce an extra level of
|
|
251
|
+
* nesting).
|
|
252
|
+
*
|
|
253
|
+
* @experimental
|
|
254
|
+
*/
|
|
255
|
+
export const NestedBlockSchema: ChildSchema = {
|
|
256
|
+
$accepts: $isBlockLevel,
|
|
257
|
+
/**
|
|
258
|
+
* Pass an inline run through unchanged. Because the schema iterator only
|
|
259
|
+
* groups *maximal* rejected runs (each separated from the next by an
|
|
260
|
+
* accepted block child), the legacy "linebreak between adjacent inline
|
|
261
|
+
* groups" case never arises — adjacent inline siblings are already
|
|
262
|
+
* coalesced into one run.
|
|
263
|
+
*/
|
|
264
|
+
$packageRun: run => run,
|
|
265
|
+
name: 'NestedBlockSchema',
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Schema for the topmost level of `$generateNodesFromDOM`. Identical to
|
|
270
|
+
* {@link BlockSchema}; aliased for clarity at the entry point and so it can
|
|
271
|
+
* be overridden separately in the future (e.g. to synthesize a `ListNode`
|
|
272
|
+
* around runs of orphan `ListItemNode`s).
|
|
273
|
+
*
|
|
274
|
+
* @experimental
|
|
275
|
+
*/
|
|
276
|
+
export const RootSchema: ChildSchema = {
|
|
277
|
+
$accepts: $isBlockLevel,
|
|
278
|
+
$packageRun: $paragraphPackageRun,
|
|
279
|
+
name: 'RootSchema',
|
|
280
|
+
};
|
|
@@ -0,0 +1,314 @@
|
|
|
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
|
+
AttrMatchOptions,
|
|
10
|
+
CompiledSelector,
|
|
11
|
+
ElementSelectorBuilder,
|
|
12
|
+
StyleMatchOptions,
|
|
13
|
+
} from './types';
|
|
14
|
+
|
|
15
|
+
import invariant from '@lexical/internal/invariant';
|
|
16
|
+
import {isDOMTextNode, isHTMLElement} from 'lexical';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @internal
|
|
20
|
+
*
|
|
21
|
+
* A predicate that may write into the per-invocation `captures` map. Returns
|
|
22
|
+
* `true` if the rule matches; `false` otherwise.
|
|
23
|
+
*/
|
|
24
|
+
export type Predicate = (
|
|
25
|
+
node: Node,
|
|
26
|
+
captures: Record<string, RegExpMatchArray>,
|
|
27
|
+
) => boolean;
|
|
28
|
+
|
|
29
|
+
/** @internal */
|
|
30
|
+
export type SelectorKind = 'element' | 'text' | 'comment';
|
|
31
|
+
|
|
32
|
+
/** @internal The runtime shape of a {@link CompiledSelector}. */
|
|
33
|
+
export interface SelectorImpl {
|
|
34
|
+
readonly kind: SelectorKind;
|
|
35
|
+
/**
|
|
36
|
+
* Uppercased tag names this selector is restricted to. Empty for wildcard
|
|
37
|
+
* element selectors and for text / comment selectors (dispatched by
|
|
38
|
+
* `kind`).
|
|
39
|
+
*/
|
|
40
|
+
readonly tags: ReadonlySet<string>;
|
|
41
|
+
/** Composed predicate run against a candidate node. */
|
|
42
|
+
readonly predicate: Predicate;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const IMPL = Symbol.for('@lexical/html/SelectorImpl');
|
|
46
|
+
|
|
47
|
+
/** @internal */
|
|
48
|
+
export function getSelectorImpl(sel: CompiledSelector): SelectorImpl {
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
|
+
const impl = (sel as any)[IMPL] as SelectorImpl | undefined;
|
|
51
|
+
invariant(
|
|
52
|
+
impl !== undefined,
|
|
53
|
+
'match must be a CompiledSelector produced by sel.* or sel.css(); received a raw object.',
|
|
54
|
+
);
|
|
55
|
+
return impl;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function combinePredicates(preds: readonly Predicate[]): Predicate {
|
|
59
|
+
if (preds.length === 0) {
|
|
60
|
+
return isHTMLElement;
|
|
61
|
+
}
|
|
62
|
+
if (preds.length === 1) {
|
|
63
|
+
return preds[0];
|
|
64
|
+
}
|
|
65
|
+
return (node, captures) => {
|
|
66
|
+
for (const p of preds) {
|
|
67
|
+
if (!p(node, captures)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @internal
|
|
77
|
+
*
|
|
78
|
+
* Build a selector value from a tag set and a predicate list. Used by the
|
|
79
|
+
* combinator API and the CSS parser.
|
|
80
|
+
*/
|
|
81
|
+
export function buildSelector(
|
|
82
|
+
tags: ReadonlySet<string>,
|
|
83
|
+
predicates: readonly Predicate[],
|
|
84
|
+
): ElementSelectorBuilder<HTMLElement> {
|
|
85
|
+
const impl: SelectorImpl = {
|
|
86
|
+
kind: 'element',
|
|
87
|
+
predicate: combinePredicates(predicates),
|
|
88
|
+
tags,
|
|
89
|
+
};
|
|
90
|
+
const refine = (additional: Predicate) =>
|
|
91
|
+
buildSelector(tags, [...predicates, additional]);
|
|
92
|
+
const builder = {
|
|
93
|
+
[IMPL]: impl,
|
|
94
|
+
attr: (name: string, value: unknown, options?: AttrMatchOptions) =>
|
|
95
|
+
refine(buildAttrPredicate(name, value, options)),
|
|
96
|
+
classAll: (...classes: readonly string[]) =>
|
|
97
|
+
refine(buildClassAllPredicate(classes)),
|
|
98
|
+
classAny: (...classes: readonly string[]) =>
|
|
99
|
+
refine(buildClassAnyPredicate(classes)),
|
|
100
|
+
styleAny: (prop: string, value: unknown, options?: StyleMatchOptions) =>
|
|
101
|
+
refine(buildStylePredicate(prop, value, options)),
|
|
102
|
+
};
|
|
103
|
+
// The runtime is fully type-erased; cast to satisfy the surface.
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
|
+
return builder as any;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeClassList(classes: readonly string[]): readonly string[] {
|
|
109
|
+
const out: string[] = [];
|
|
110
|
+
for (const c of classes) {
|
|
111
|
+
if (c) {
|
|
112
|
+
out.push(c);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @internal */
|
|
119
|
+
export function buildClassAllPredicate(classes: readonly string[]): Predicate {
|
|
120
|
+
const ns = normalizeClassList(classes);
|
|
121
|
+
if (ns.length === 0) {
|
|
122
|
+
return () => true;
|
|
123
|
+
}
|
|
124
|
+
return node => {
|
|
125
|
+
if (!isHTMLElement(node)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const cl = node.classList;
|
|
129
|
+
for (const c of ns) {
|
|
130
|
+
if (!cl.contains(c)) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** @internal */
|
|
139
|
+
export function buildClassAnyPredicate(classes: readonly string[]): Predicate {
|
|
140
|
+
const ns = normalizeClassList(classes);
|
|
141
|
+
if (ns.length === 0) {
|
|
142
|
+
return () => false;
|
|
143
|
+
}
|
|
144
|
+
return node => {
|
|
145
|
+
if (!isHTMLElement(node)) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
const cl = node.classList;
|
|
149
|
+
for (const c of ns) {
|
|
150
|
+
if (cl.contains(c)) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** @internal */
|
|
159
|
+
export function buildAttrPredicate(
|
|
160
|
+
name: string,
|
|
161
|
+
value: unknown,
|
|
162
|
+
options?: AttrMatchOptions,
|
|
163
|
+
): Predicate {
|
|
164
|
+
if (value === true) {
|
|
165
|
+
return node => isHTMLElement(node) && node.hasAttribute(name);
|
|
166
|
+
}
|
|
167
|
+
if (typeof value === 'string') {
|
|
168
|
+
return node => isHTMLElement(node) && node.getAttribute(name) === value;
|
|
169
|
+
}
|
|
170
|
+
if (value instanceof RegExp) {
|
|
171
|
+
const capture = options && options.capture;
|
|
172
|
+
const re = value;
|
|
173
|
+
return (node, captures) => {
|
|
174
|
+
if (!isHTMLElement(node)) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
const v = node.getAttribute(name);
|
|
178
|
+
if (v == null) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
const m = v.match(re);
|
|
182
|
+
if (m === null) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
if (capture !== undefined) {
|
|
186
|
+
captures[capture] = m;
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
invariant(
|
|
192
|
+
false,
|
|
193
|
+
'sel.attr(%s, ...) requires true, a string, or a RegExp',
|
|
194
|
+
JSON.stringify(name),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildStylePredicate(
|
|
199
|
+
prop: string,
|
|
200
|
+
value: unknown,
|
|
201
|
+
options?: StyleMatchOptions,
|
|
202
|
+
): Predicate {
|
|
203
|
+
if (typeof value === 'string') {
|
|
204
|
+
return node =>
|
|
205
|
+
isHTMLElement(node) && node.style.getPropertyValue(prop) === value;
|
|
206
|
+
}
|
|
207
|
+
if (value instanceof RegExp) {
|
|
208
|
+
const capture = options && options.capture;
|
|
209
|
+
const re = value;
|
|
210
|
+
return (node, captures) => {
|
|
211
|
+
if (!isHTMLElement(node)) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const v = node.style.getPropertyValue(prop);
|
|
215
|
+
if (!v) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
const m = v.match(re);
|
|
219
|
+
if (m === null) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
if (capture !== undefined) {
|
|
223
|
+
captures[capture] = m;
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
invariant(
|
|
229
|
+
false,
|
|
230
|
+
'sel.styleAny(%s, ...) requires a string or a RegExp',
|
|
231
|
+
JSON.stringify(prop),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const TEXT_SELECTOR_IMPL: SelectorImpl = {
|
|
236
|
+
kind: 'text',
|
|
237
|
+
predicate: isDOMTextNode,
|
|
238
|
+
tags: new Set(),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// The `as` cast is needed because `CompiledSelector` is an opaque
|
|
242
|
+
// branded interface — neither the object literal nor a typed const can
|
|
243
|
+
// declare the internal `IMPL` symbol without exposing it.
|
|
244
|
+
const TEXT_SELECTOR = {[IMPL]: TEXT_SELECTOR_IMPL} as CompiledSelector<Text>;
|
|
245
|
+
|
|
246
|
+
const COMMENT_SELECTOR_IMPL: SelectorImpl = {
|
|
247
|
+
kind: 'comment',
|
|
248
|
+
predicate: node => node.nodeType === 8 /* COMMENT_NODE */,
|
|
249
|
+
tags: new Set(),
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const COMMENT_SELECTOR = {
|
|
253
|
+
[IMPL]: COMMENT_SELECTOR_IMPL,
|
|
254
|
+
} as CompiledSelector<Comment>;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Combinator API for building {@link CompiledSelector}s. The public
|
|
258
|
+
* `sel` is augmented from this in `./index.ts` (where the CSS parser is
|
|
259
|
+
* available without a circular import); consumers outside `@lexical/html`
|
|
260
|
+
* should always import the public `sel` from the package root.
|
|
261
|
+
*
|
|
262
|
+
* @internal
|
|
263
|
+
*/
|
|
264
|
+
export const selBase = {
|
|
265
|
+
/** Match any {@link HTMLElement}. */
|
|
266
|
+
any(): ElementSelectorBuilder<HTMLElement> {
|
|
267
|
+
return buildSelector(new Set(), []);
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
/** Match DOM {@link Comment} nodes. */
|
|
271
|
+
comment(): CompiledSelector<Comment> {
|
|
272
|
+
return COMMENT_SELECTOR;
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Match by tag name(s). With one literal tag the element type is narrowed
|
|
277
|
+
* (e.g. `'a' → HTMLAnchorElement`); with multiple, it is the union of
|
|
278
|
+
* their `HTMLElementTagNameMap` entries.
|
|
279
|
+
*/
|
|
280
|
+
tag<const Tags extends readonly string[]>(
|
|
281
|
+
...tags: Tags
|
|
282
|
+
): ElementSelectorBuilder<
|
|
283
|
+
Tags[number] extends keyof HTMLElementTagNameMap
|
|
284
|
+
? HTMLElementTagNameMap[Tags[number]]
|
|
285
|
+
: HTMLElement
|
|
286
|
+
> {
|
|
287
|
+
invariant(tags.length > 0, 'sel.tag() requires at least one tag name');
|
|
288
|
+
const upper = new Set<string>();
|
|
289
|
+
for (const t of tags) {
|
|
290
|
+
upper.add(t.toUpperCase());
|
|
291
|
+
}
|
|
292
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
293
|
+
return buildSelector(upper, []) as any;
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
/** Match DOM {@link Text} nodes. */
|
|
297
|
+
text(): CompiledSelector<Text> {
|
|
298
|
+
return TEXT_SELECTOR;
|
|
299
|
+
},
|
|
300
|
+
} as const;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Cross-frame-safe replacement for `node instanceof HTMLXxxElement`. Returns
|
|
304
|
+
* true when `node` is an HTMLElement whose `nodeName` equals `tag` (compared
|
|
305
|
+
* case-insensitively).
|
|
306
|
+
*
|
|
307
|
+
* @experimental
|
|
308
|
+
*/
|
|
309
|
+
export function isElementOfTag<T extends keyof HTMLElementTagNameMap>(
|
|
310
|
+
node: Node,
|
|
311
|
+
tag: T,
|
|
312
|
+
): node is HTMLElementTagNameMap[T] {
|
|
313
|
+
return isHTMLElement(node) && node.nodeName === tag.toUpperCase();
|
|
314
|
+
}
|