@lexical/html 0.44.1-nightly.20260518.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{DOMRenderExtension.d.ts → dist/DOMRenderExtension.d.ts} +12 -1
- package/dist/DOMRenderRuntime.d.ts +51 -0
- package/dist/LexicalHtml.dev.js +3192 -0
- package/dist/LexicalHtml.dev.mjs +3146 -0
- package/{LexicalHtml.js.flow → dist/LexicalHtml.js.flow} +16 -16
- package/dist/LexicalHtml.mjs +56 -0
- package/dist/LexicalHtml.node.mjs +54 -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 +27 -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 +91 -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 +53 -0
- package/src/import/ImportContext.ts +339 -0
- package/src/import/compileImportRules.ts +178 -0
- package/src/import/coreImportRules.ts +485 -0
- package/src/import/defineImportRule.ts +40 -0
- package/src/import/defineOverlayRules.ts +105 -0
- package/src/import/index.ts +96 -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 +236 -0
- package/src/import/sel.ts +314 -0
- package/src/import/types.ts +471 -0
- package/src/index.ts +555 -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,105 @@
|
|
|
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 {AnyDOMImportRule} from './types';
|
|
9
|
+
|
|
10
|
+
import {type CompiledDispatch, compileImportRules} from './compileImportRules';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Opaque handle for a pre-compiled set of overlay rules. Produce one with
|
|
14
|
+
* {@link defineOverlayRules} and pass it to
|
|
15
|
+
* {@link DOMImportContext.$importChildren} via
|
|
16
|
+
* {@link ImportChildrenOpts.rules}.
|
|
17
|
+
*
|
|
18
|
+
* To merge two or more overlays into a single one, pass them (alongside
|
|
19
|
+
* raw {@link DOMImportRule}s if desired) to a fresh
|
|
20
|
+
* {@link defineOverlayRules} — earlier arguments are higher priority.
|
|
21
|
+
*
|
|
22
|
+
* The internal shape is intentionally not part of the public API: it's a
|
|
23
|
+
* compiled dispatch table tagged with `__type` so callers cannot pass a
|
|
24
|
+
* raw rule array where a compiled overlay is expected.
|
|
25
|
+
*
|
|
26
|
+
* @experimental
|
|
27
|
+
*/
|
|
28
|
+
export interface CompiledOverlayRules {
|
|
29
|
+
readonly __type: 'CompiledOverlayRules';
|
|
30
|
+
/** @internal */
|
|
31
|
+
readonly dispatch: CompiledDispatch;
|
|
32
|
+
/**
|
|
33
|
+
* @internal — flattened source rules retained so an overlay can be
|
|
34
|
+
* recompiled when it is passed to another {@link defineOverlayRules}
|
|
35
|
+
* call or as part of {@link DOMImportConfig.rules}.
|
|
36
|
+
*/
|
|
37
|
+
readonly rules: readonly AnyDOMImportRule[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* An entry accepted everywhere rules are configured (overlay
|
|
42
|
+
* definitions, {@link DOMImportConfig.rules}). Either a single
|
|
43
|
+
* {@link DOMImportRule} or a {@link CompiledOverlayRules} produced by
|
|
44
|
+
* a previous {@link defineOverlayRules} call — passing the latter
|
|
45
|
+
* inlines the overlay's rules at this position in priority order.
|
|
46
|
+
*
|
|
47
|
+
* @experimental
|
|
48
|
+
*/
|
|
49
|
+
export type DOMImportRuleEntry = AnyDOMImportRule | CompiledOverlayRules;
|
|
50
|
+
|
|
51
|
+
/** @internal */
|
|
52
|
+
export function flattenRuleEntries(
|
|
53
|
+
entries: readonly DOMImportRuleEntry[],
|
|
54
|
+
): AnyDOMImportRule[] {
|
|
55
|
+
const out: AnyDOMImportRule[] = [];
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (isCompiledOverlayRules(entry)) {
|
|
58
|
+
for (const r of entry.rules) {
|
|
59
|
+
out.push(r);
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
out.push(entry);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isCompiledOverlayRules(
|
|
69
|
+
entry: DOMImportRuleEntry,
|
|
70
|
+
): entry is CompiledOverlayRules {
|
|
71
|
+
return (
|
|
72
|
+
typeof entry === 'object' &&
|
|
73
|
+
entry !== null &&
|
|
74
|
+
'__type' in entry &&
|
|
75
|
+
entry.__type === 'CompiledOverlayRules'
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Pre-compile a set of {@link DOMImportRuleEntry}s into a
|
|
81
|
+
* {@link CompiledOverlayRules} handle that can be installed via
|
|
82
|
+
* `ctx.$importChildren(el, {rules: …})`.
|
|
83
|
+
*
|
|
84
|
+
* Entries can be raw {@link DOMImportRule}s or other
|
|
85
|
+
* {@link CompiledOverlayRules} (the latter are inlined at their
|
|
86
|
+
* position in priority order, so the same call composes any number of
|
|
87
|
+
* overlays). Earlier entries are higher priority.
|
|
88
|
+
*
|
|
89
|
+
* Overlay rules installed as a raw array would be re-compiled on every
|
|
90
|
+
* `$importChildren` call. For overlays that are reused (e.g. a GitHub
|
|
91
|
+
* code-table rule that wraps every matching table), call this once at
|
|
92
|
+
* module scope so the dispatch table is built up front.
|
|
93
|
+
*
|
|
94
|
+
* @experimental
|
|
95
|
+
*/
|
|
96
|
+
export function defineOverlayRules(
|
|
97
|
+
entries: readonly DOMImportRuleEntry[],
|
|
98
|
+
): CompiledOverlayRules {
|
|
99
|
+
const rules = flattenRuleEntries(entries);
|
|
100
|
+
return {
|
|
101
|
+
__type: 'CompiledOverlayRules',
|
|
102
|
+
dispatch: compileImportRules(rules),
|
|
103
|
+
rules,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
BlockSchema,
|
|
70
|
+
InlineSchema,
|
|
71
|
+
NestedBlockSchema,
|
|
72
|
+
RootSchema,
|
|
73
|
+
} from './schemas';
|
|
74
|
+
export {isElementOfTag} from './sel';
|
|
75
|
+
export type {
|
|
76
|
+
AnyDOMImportRule,
|
|
77
|
+
AttrMatchOptions,
|
|
78
|
+
CapturesOfSelector,
|
|
79
|
+
ChildSchema,
|
|
80
|
+
CompiledSelector,
|
|
81
|
+
DOMImportContext,
|
|
82
|
+
DOMImportExtensionOutput,
|
|
83
|
+
DOMImportFn,
|
|
84
|
+
DOMImportRule,
|
|
85
|
+
DOMPreprocessContext,
|
|
86
|
+
DOMPreprocessFn,
|
|
87
|
+
ElementSelectorBuilder,
|
|
88
|
+
GenerateNodesFromDOMOptions,
|
|
89
|
+
ImportChildrenOpts,
|
|
90
|
+
ImportContextPairOrUpdater,
|
|
91
|
+
ImportNodeOpts,
|
|
92
|
+
ImportSession,
|
|
93
|
+
ImportStateConfig,
|
|
94
|
+
NodeOfSelector,
|
|
95
|
+
StyleMatchOptions,
|
|
96
|
+
} 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
|
+
}
|