@ismail-elkorchi/css-parser 0.1.0 → 0.2.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/README.md +67 -69
- package/dist/internal/cssom/declarations.d.ts +36 -0
- package/dist/internal/cssom/declarations.js +177 -0
- package/dist/internal/generated/css-data.d.ts +2 -0
- package/dist/internal/generated/css-data.js +16229 -0
- package/dist/internal/grammar/catalog-types.d.ts +26 -0
- package/dist/internal/grammar/value-definition.d.ts +63 -0
- package/dist/internal/grammar/value-definition.js +439 -0
- package/dist/internal/properties/matcher.d.ts +29 -0
- package/dist/internal/properties/matcher.js +791 -0
- package/dist/internal/properties/registry.d.ts +24 -0
- package/dist/internal/properties/registry.js +52 -0
- package/dist/internal/selectors/matcher.d.ts +96 -0
- package/dist/internal/selectors/matcher.js +616 -0
- package/dist/internal/selectors/parser.d.ts +2 -0
- package/dist/internal/selectors/parser.js +702 -0
- package/dist/internal/selectors/specificity.d.ts +3 -0
- package/dist/internal/selectors/specificity.js +77 -0
- package/dist/internal/selectors/types.d.ts +110 -0
- package/dist/internal/syntax/ast.d.ts +70 -0
- package/dist/internal/syntax/ast.js +1 -0
- package/dist/internal/syntax/characters.d.ts +8 -0
- package/dist/internal/syntax/characters.js +45 -0
- package/dist/internal/syntax/encoding.d.ts +15 -0
- package/dist/internal/syntax/encoding.js +161 -0
- package/dist/internal/syntax/input.d.ts +23 -0
- package/dist/internal/syntax/input.js +184 -0
- package/dist/internal/syntax/parser.d.ts +26 -0
- package/dist/internal/syntax/parser.js +581 -0
- package/dist/internal/syntax/resources.d.ts +28 -0
- package/dist/internal/syntax/resources.js +135 -0
- package/dist/internal/syntax/serialize.d.ts +10 -0
- package/dist/internal/syntax/serialize.js +630 -0
- package/dist/internal/syntax/token-stream.d.ts +16 -0
- package/dist/internal/syntax/token-stream.js +66 -0
- package/dist/internal/syntax/tokenizer.d.ts +21 -0
- package/dist/internal/syntax/tokenizer.js +571 -0
- package/dist/internal/syntax/tokens.d.ts +115 -0
- package/dist/internal/syntax/tokens.js +1 -0
- package/dist/internal/syntax/types.d.ts +46 -0
- package/dist/internal/syntax/types.js +1 -0
- package/dist/mod.d.ts +7 -1
- package/dist/mod.js +7 -1
- package/dist/public/edits.d.ts +12 -0
- package/dist/public/edits.js +195 -0
- package/dist/public/mod.d.ts +13 -35
- package/dist/public/mod.js +12 -1740
- package/dist/public/parse.d.ts +37 -0
- package/dist/public/parse.js +298 -0
- package/dist/public/traversal.d.ts +13 -0
- package/dist/public/traversal.js +96 -0
- package/dist/public/types.d.ts +78 -264
- package/package.json +32 -53
- package/THIRD_PARTY_NOTICES.md +0 -19
- package/dist/internal/csstree-runtime.d.ts +0 -20
- package/dist/internal/csstree-runtime.js +0 -21
- package/dist/internal/encoding/mod.d.ts +0 -1
- package/dist/internal/encoding/mod.js +0 -1
- package/dist/internal/encoding/sniff.d.ts +0 -14
- package/dist/internal/encoding/sniff.js +0 -95
- package/dist/internal/serializer/mod.d.ts +0 -1
- package/dist/internal/serializer/mod.js +0 -1
- package/dist/internal/serializer/serialize.d.ts +0 -3
- package/dist/internal/serializer/serialize.js +0 -89
- package/dist/internal/tokenizer/mod.d.ts +0 -2
- package/dist/internal/tokenizer/mod.js +0 -1
- package/dist/internal/tokenizer/tokenize.d.ts +0 -2
- package/dist/internal/tokenizer/tokenize.js +0 -39
- package/dist/internal/tokenizer/tokens.d.ts +0 -23
- package/dist/internal/tree/build.d.ts +0 -2
- package/dist/internal/tree/build.js +0 -85
- package/dist/internal/tree/mod.d.ts +0 -2
- package/dist/internal/tree/mod.js +0 -1
- package/dist/internal/tree/types.d.ts +0 -25
- package/dist/internal/vendor/csstree/LICENSE +0 -19
- package/dist/internal/vendor/csstree/csstree.esm.js +0 -12
- package/dist/internal/version.d.ts +0 -1
- package/dist/internal/version.js +0 -1
- package/dist/public/index.d.ts +0 -1
- package/dist/public/index.js +0 -1
- /package/dist/internal/{tokenizer/tokens.js → grammar/catalog-types.js} +0 -0
- /package/dist/internal/{tree → selectors}/types.js +0 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import { ResourceGuard } from "../syntax/resources.js";
|
|
2
|
+
export class SelectorTreeError extends TypeError {
|
|
3
|
+
reason;
|
|
4
|
+
code = "CSS_SELECTOR_INVALID_TREE";
|
|
5
|
+
constructor(reason) {
|
|
6
|
+
super(`Selector trees cannot contain a ${reason === "cycle" ? "cycle" : "shared node"}.`);
|
|
7
|
+
this.reason = reason;
|
|
8
|
+
this.name = "SelectorTreeError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function known(decision) {
|
|
12
|
+
return Object.freeze({ decision, reasons: Object.freeze([]) });
|
|
13
|
+
}
|
|
14
|
+
function unknown(reason) {
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
decision: "unknown",
|
|
17
|
+
reasons: Object.freeze([reason])
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function uniqueReasons(values) {
|
|
21
|
+
const keys = new Set();
|
|
22
|
+
const result = [];
|
|
23
|
+
for (const value of values) {
|
|
24
|
+
const key = `${value.code}:${value.name}:${String(value.span.start.offset)}`;
|
|
25
|
+
if (keys.has(key))
|
|
26
|
+
continue;
|
|
27
|
+
keys.add(key);
|
|
28
|
+
result.push(value);
|
|
29
|
+
}
|
|
30
|
+
return Object.freeze(result);
|
|
31
|
+
}
|
|
32
|
+
function and(left, right) {
|
|
33
|
+
if (left.decision === "no-match" || right.decision === "no-match") {
|
|
34
|
+
return known("no-match");
|
|
35
|
+
}
|
|
36
|
+
if (left.decision === "match" && right.decision === "match") {
|
|
37
|
+
return known("match");
|
|
38
|
+
}
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
decision: "unknown",
|
|
41
|
+
reasons: uniqueReasons([...left.reasons, ...right.reasons])
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function or(values) {
|
|
45
|
+
const reasons = [];
|
|
46
|
+
for (const value of values) {
|
|
47
|
+
if (value.decision === "match")
|
|
48
|
+
return known("match");
|
|
49
|
+
if (value.decision === "unknown")
|
|
50
|
+
reasons.push(...value.reasons);
|
|
51
|
+
}
|
|
52
|
+
return reasons.length === 0
|
|
53
|
+
? known("no-match")
|
|
54
|
+
: Object.freeze({
|
|
55
|
+
decision: "unknown",
|
|
56
|
+
reasons: uniqueReasons(reasons)
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function invert(value) {
|
|
60
|
+
if (value.decision === "unknown")
|
|
61
|
+
return value;
|
|
62
|
+
return known(value.decision === "match" ? "no-match" : "match");
|
|
63
|
+
}
|
|
64
|
+
function lowerAscii(value) {
|
|
65
|
+
return value.replace(/[A-Z]/gu, (character) => character.toLowerCase());
|
|
66
|
+
}
|
|
67
|
+
function equalAsciiInsensitive(left, right) {
|
|
68
|
+
return lowerAscii(left) === lowerAscii(right);
|
|
69
|
+
}
|
|
70
|
+
function isAsciiWhitespace(character) {
|
|
71
|
+
return (character === "\t" ||
|
|
72
|
+
character === "\n" ||
|
|
73
|
+
character === "\f" ||
|
|
74
|
+
character === "\r" ||
|
|
75
|
+
character === " ");
|
|
76
|
+
}
|
|
77
|
+
function whitespaceTokens(value) {
|
|
78
|
+
const result = [];
|
|
79
|
+
let token = "";
|
|
80
|
+
for (const character of value) {
|
|
81
|
+
if (isAsciiWhitespace(character)) {
|
|
82
|
+
if (token.length > 0)
|
|
83
|
+
result.push(token);
|
|
84
|
+
token = "";
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
token += character;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (token.length > 0)
|
|
91
|
+
result.push(token);
|
|
92
|
+
return Object.freeze(result);
|
|
93
|
+
}
|
|
94
|
+
function buildTreeIndex(root, environment, guard) {
|
|
95
|
+
const parent = new Map();
|
|
96
|
+
const children = new Map();
|
|
97
|
+
const elements = [];
|
|
98
|
+
const active = new Set();
|
|
99
|
+
const stack = [{ node: root, parent: null, depth: 0, leaving: false }];
|
|
100
|
+
while (stack.length > 0) {
|
|
101
|
+
guard.step();
|
|
102
|
+
const frame = stack.pop();
|
|
103
|
+
if (frame === undefined)
|
|
104
|
+
continue;
|
|
105
|
+
if (frame.leaving) {
|
|
106
|
+
active.delete(frame.node);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (active.has(frame.node))
|
|
110
|
+
throw new SelectorTreeError("cycle");
|
|
111
|
+
if (parent.has(frame.node))
|
|
112
|
+
throw new SelectorTreeError("shared-node");
|
|
113
|
+
active.add(frame.node);
|
|
114
|
+
parent.set(frame.node, frame.parent);
|
|
115
|
+
guard.createNode(frame.depth);
|
|
116
|
+
if (environment.tree.data(frame.node).kind === "element") {
|
|
117
|
+
elements.push(frame.node);
|
|
118
|
+
}
|
|
119
|
+
const nodeChildren = Object.freeze([
|
|
120
|
+
...environment.tree.children(frame.node)
|
|
121
|
+
]);
|
|
122
|
+
children.set(frame.node, nodeChildren);
|
|
123
|
+
stack.push({ ...frame, leaving: true });
|
|
124
|
+
for (let index = nodeChildren.length - 1; index >= 0; index -= 1) {
|
|
125
|
+
const child = nodeChildren[index];
|
|
126
|
+
if (child !== undefined) {
|
|
127
|
+
stack.push({
|
|
128
|
+
node: child,
|
|
129
|
+
parent: frame.node,
|
|
130
|
+
depth: frame.depth + 1,
|
|
131
|
+
leaving: false
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return Object.freeze({
|
|
137
|
+
root,
|
|
138
|
+
parent,
|
|
139
|
+
children,
|
|
140
|
+
elements: Object.freeze(elements)
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
class SelectorMatcher {
|
|
144
|
+
environment;
|
|
145
|
+
options;
|
|
146
|
+
#guard;
|
|
147
|
+
#index;
|
|
148
|
+
#scopes;
|
|
149
|
+
constructor(root, environment, options) {
|
|
150
|
+
this.environment = environment;
|
|
151
|
+
this.options = options;
|
|
152
|
+
this.#guard = new ResourceGuard(options.limits, options.signal);
|
|
153
|
+
this.#index = buildTreeIndex(root, environment, this.#guard);
|
|
154
|
+
this.#scopes = options.scopes === undefined
|
|
155
|
+
? new Set([root])
|
|
156
|
+
: new Set([...options.scopes].filter((node) => this.#index.parent.has(node)));
|
|
157
|
+
}
|
|
158
|
+
usage() {
|
|
159
|
+
return this.#guard.snapshot();
|
|
160
|
+
}
|
|
161
|
+
elements() {
|
|
162
|
+
return this.#index.elements;
|
|
163
|
+
}
|
|
164
|
+
matches(list, node) {
|
|
165
|
+
if (!this.#index.parent.has(node))
|
|
166
|
+
return known("no-match");
|
|
167
|
+
return or(list.selectors.map((selector) => this.#complex(selector, node, null)));
|
|
168
|
+
}
|
|
169
|
+
#complex(selector, node, anchor) {
|
|
170
|
+
return this.#complexAt(selector, selector.compounds.length - 1, node, anchor);
|
|
171
|
+
}
|
|
172
|
+
#complexAt(selector, index, node, anchor) {
|
|
173
|
+
this.#guard.step();
|
|
174
|
+
const compound = selector.compounds[index];
|
|
175
|
+
if (compound === undefined)
|
|
176
|
+
return known("no-match");
|
|
177
|
+
const own = this.#compound(compound, node);
|
|
178
|
+
if (own.decision === "no-match")
|
|
179
|
+
return own;
|
|
180
|
+
if (index === 0) {
|
|
181
|
+
const relation = anchor === null
|
|
182
|
+
? known("match")
|
|
183
|
+
: this.#anchorRelation(selector.leadingCombinator ?? " ", anchor, node);
|
|
184
|
+
return and(own, relation);
|
|
185
|
+
}
|
|
186
|
+
const combinator = selector.combinators[index - 1];
|
|
187
|
+
const candidates = this.#leftCandidates(node, combinator);
|
|
188
|
+
const related = or(candidates.map((candidate) => this.#complexAt(selector, index - 1, candidate, anchor)));
|
|
189
|
+
return and(own, related);
|
|
190
|
+
}
|
|
191
|
+
#compound(compound, node) {
|
|
192
|
+
const data = this.environment.tree.data(node);
|
|
193
|
+
if (data.kind !== "element")
|
|
194
|
+
return known("no-match");
|
|
195
|
+
let result = compound.type === null
|
|
196
|
+
? known("match")
|
|
197
|
+
: this.#type(compound.type, data);
|
|
198
|
+
for (const simple of compound.simples) {
|
|
199
|
+
result = and(result, this.#simple(simple, node, data));
|
|
200
|
+
if (result.decision === "no-match")
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
#type(selector, element) {
|
|
206
|
+
const namespace = this.#namespace(selector.namespace, true, element, selector.name, selector.span);
|
|
207
|
+
if (namespace.decision !== "match")
|
|
208
|
+
return namespace;
|
|
209
|
+
if (selector.name === "*")
|
|
210
|
+
return known("match");
|
|
211
|
+
const equal = this.environment.documentMode.syntax === "html" &&
|
|
212
|
+
element.namespace === "http://www.w3.org/1999/xhtml"
|
|
213
|
+
? equalAsciiInsensitive(element.localName, selector.name)
|
|
214
|
+
: element.localName === selector.name;
|
|
215
|
+
return known(equal ? "match" : "no-match");
|
|
216
|
+
}
|
|
217
|
+
#simple(simple, node, element) {
|
|
218
|
+
this.#guard.step();
|
|
219
|
+
switch (simple.kind) {
|
|
220
|
+
case "id":
|
|
221
|
+
return known(this.environment.idValues(node, element).some((value) => this.#identityEqual(value, simple.value))
|
|
222
|
+
? "match"
|
|
223
|
+
: "no-match");
|
|
224
|
+
case "class": {
|
|
225
|
+
return known(this.environment.classNames(node, element).some((value) => this.#identityEqual(value, simple.value))
|
|
226
|
+
? "match"
|
|
227
|
+
: "no-match");
|
|
228
|
+
}
|
|
229
|
+
case "attribute":
|
|
230
|
+
return this.#attributeSelector(simple, element);
|
|
231
|
+
case "pseudo-class":
|
|
232
|
+
return this.#pseudo(simple, node, element);
|
|
233
|
+
case "pseudo-element":
|
|
234
|
+
return known("no-match");
|
|
235
|
+
case "nesting":
|
|
236
|
+
return this.options.nesting === undefined
|
|
237
|
+
? known(this.#scopes.has(node) ? "match" : "no-match")
|
|
238
|
+
: this.matches(this.options.nesting, node);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
#identityEqual(left, right) {
|
|
242
|
+
return this.environment.documentMode.syntax === "html" &&
|
|
243
|
+
this.environment.documentMode.quirks === "quirks"
|
|
244
|
+
? equalAsciiInsensitive(left, right)
|
|
245
|
+
: left === right;
|
|
246
|
+
}
|
|
247
|
+
#attributeSelector(selector, element) {
|
|
248
|
+
const namespace = this.#attributeNamespace(selector);
|
|
249
|
+
if (namespace.status === "unknown") {
|
|
250
|
+
return unknown({
|
|
251
|
+
code: "namespace-prefix",
|
|
252
|
+
name: selector.namespace ?? "",
|
|
253
|
+
span: selector.span
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
const attribute = this.#attribute(element, namespace.namespace, selector.name);
|
|
257
|
+
if (attribute === null)
|
|
258
|
+
return known("no-match");
|
|
259
|
+
if (selector.matcher === null)
|
|
260
|
+
return known("match");
|
|
261
|
+
const expected = selector.value;
|
|
262
|
+
if (expected === null)
|
|
263
|
+
return known("no-match");
|
|
264
|
+
const sensitivity = selector.modifier === "i"
|
|
265
|
+
? "ascii-insensitive"
|
|
266
|
+
: selector.modifier === "s"
|
|
267
|
+
? "sensitive"
|
|
268
|
+
: this.environment.attributeValueCaseSensitivity(element, attribute);
|
|
269
|
+
const left = sensitivity === "ascii-insensitive"
|
|
270
|
+
? lowerAscii(attribute.value)
|
|
271
|
+
: attribute.value;
|
|
272
|
+
const right = sensitivity === "ascii-insensitive"
|
|
273
|
+
? lowerAscii(expected)
|
|
274
|
+
: expected;
|
|
275
|
+
if (right.length === 0 &&
|
|
276
|
+
(selector.matcher === "^=" ||
|
|
277
|
+
selector.matcher === "$=" ||
|
|
278
|
+
selector.matcher === "*=")) {
|
|
279
|
+
return known("no-match");
|
|
280
|
+
}
|
|
281
|
+
switch (selector.matcher) {
|
|
282
|
+
case "=":
|
|
283
|
+
return known(left === right ? "match" : "no-match");
|
|
284
|
+
case "~=":
|
|
285
|
+
return known(whitespaceTokens(left).includes(right) ? "match" : "no-match");
|
|
286
|
+
case "|=":
|
|
287
|
+
return known(left === right || left.startsWith(`${right}-`)
|
|
288
|
+
? "match"
|
|
289
|
+
: "no-match");
|
|
290
|
+
case "^=":
|
|
291
|
+
return known(left.startsWith(right) ? "match" : "no-match");
|
|
292
|
+
case "$=":
|
|
293
|
+
return known(left.endsWith(right) ? "match" : "no-match");
|
|
294
|
+
case "*=":
|
|
295
|
+
return known(left.includes(right) ? "match" : "no-match");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
#pseudo(pseudo, node, element) {
|
|
299
|
+
const name = pseudo.name;
|
|
300
|
+
if ((name === "is" || name === "where") &&
|
|
301
|
+
pseudo.argument.kind === "selector-list") {
|
|
302
|
+
return or(pseudo.argument.selectors.map((selector) => this.#complex(selector, node, null)));
|
|
303
|
+
}
|
|
304
|
+
if (name === "not" &&
|
|
305
|
+
pseudo.argument.kind === "selector-list") {
|
|
306
|
+
return invert(or(pseudo.argument.selectors.map((selector) => this.#complex(selector, node, null))));
|
|
307
|
+
}
|
|
308
|
+
if (name === "has" &&
|
|
309
|
+
pseudo.argument.kind === "selector-list") {
|
|
310
|
+
const results = [];
|
|
311
|
+
for (const selector of pseudo.argument.selectors) {
|
|
312
|
+
for (const candidate of this.#index.elements) {
|
|
313
|
+
results.push(this.#complex(selector, candidate, node));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return or(results);
|
|
317
|
+
}
|
|
318
|
+
if (name === "scope") {
|
|
319
|
+
if (pseudo.argument.kind !== "none")
|
|
320
|
+
return known("no-match");
|
|
321
|
+
return known(this.#scopes.has(node) ? "match" : "no-match");
|
|
322
|
+
}
|
|
323
|
+
if (name === "root") {
|
|
324
|
+
if (pseudo.argument.kind !== "none")
|
|
325
|
+
return known("no-match");
|
|
326
|
+
const parent = this.#index.parent.get(node) ?? null;
|
|
327
|
+
return known(parent === null ||
|
|
328
|
+
this.environment.tree.data(parent).kind !== "element"
|
|
329
|
+
? "match"
|
|
330
|
+
: "no-match");
|
|
331
|
+
}
|
|
332
|
+
if (name === "empty") {
|
|
333
|
+
return pseudo.argument.kind === "none"
|
|
334
|
+
? this.#empty(node)
|
|
335
|
+
: known("no-match");
|
|
336
|
+
}
|
|
337
|
+
const indexed = this.#indexedPseudo(name, pseudo, node, element);
|
|
338
|
+
if (indexed !== null)
|
|
339
|
+
return indexed;
|
|
340
|
+
const decision = this.environment.matchPseudoClass(node, pseudo, Object.freeze({
|
|
341
|
+
root: this.#index.root,
|
|
342
|
+
scopes: this.#scopes
|
|
343
|
+
}));
|
|
344
|
+
return decision === "unknown"
|
|
345
|
+
? unknown({
|
|
346
|
+
code: "pseudo-class",
|
|
347
|
+
name,
|
|
348
|
+
span: pseudo.span
|
|
349
|
+
})
|
|
350
|
+
: known(decision);
|
|
351
|
+
}
|
|
352
|
+
#empty(node) {
|
|
353
|
+
for (const child of this.#index.children.get(node) ?? []) {
|
|
354
|
+
const data = this.environment.tree.data(child);
|
|
355
|
+
if (data.kind === "element")
|
|
356
|
+
return known("no-match");
|
|
357
|
+
if (data.kind === "text" && data.value.length > 0) {
|
|
358
|
+
return known("no-match");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return known("match");
|
|
362
|
+
}
|
|
363
|
+
#indexedPseudo(name, pseudo, node, element) {
|
|
364
|
+
const aliases = {
|
|
365
|
+
"first-child": { a: 0, b: 1, fromEnd: false, sameType: false },
|
|
366
|
+
"last-child": { a: 0, b: 1, fromEnd: true, sameType: false },
|
|
367
|
+
"only-child": { a: 0, b: 1, fromEnd: false, sameType: false },
|
|
368
|
+
"first-of-type": { a: 0, b: 1, fromEnd: false, sameType: true },
|
|
369
|
+
"last-of-type": { a: 0, b: 1, fromEnd: true, sameType: true },
|
|
370
|
+
"only-of-type": { a: 0, b: 1, fromEnd: false, sameType: true }
|
|
371
|
+
};
|
|
372
|
+
const alias = aliases[name];
|
|
373
|
+
if (alias !== undefined) {
|
|
374
|
+
if (pseudo.argument.kind !== "none")
|
|
375
|
+
return known("no-match");
|
|
376
|
+
const first = this.#indexPosition(node, element, alias.sameType, alias.fromEnd, Object.freeze([]));
|
|
377
|
+
if (name === "only-child" ||
|
|
378
|
+
name === "only-of-type") {
|
|
379
|
+
const last = this.#indexPosition(node, element, alias.sameType, true, Object.freeze([]));
|
|
380
|
+
const combined = and(first.result, last.result);
|
|
381
|
+
return combined.decision === "match" &&
|
|
382
|
+
first.index === 1 &&
|
|
383
|
+
last.index === 1
|
|
384
|
+
? known("match")
|
|
385
|
+
: combined.decision === "unknown"
|
|
386
|
+
? combined
|
|
387
|
+
: known("no-match");
|
|
388
|
+
}
|
|
389
|
+
return first.result.decision === "match" && first.index === 1
|
|
390
|
+
? known("match")
|
|
391
|
+
: first.result.decision === "unknown"
|
|
392
|
+
? first.result
|
|
393
|
+
: known("no-match");
|
|
394
|
+
}
|
|
395
|
+
if (pseudo.argument.kind !== "nth")
|
|
396
|
+
return null;
|
|
397
|
+
const modes = {
|
|
398
|
+
"nth-child": { fromEnd: false, sameType: false },
|
|
399
|
+
"nth-last-child": { fromEnd: true, sameType: false },
|
|
400
|
+
"nth-of-type": { fromEnd: false, sameType: true },
|
|
401
|
+
"nth-last-of-type": { fromEnd: true, sameType: true }
|
|
402
|
+
};
|
|
403
|
+
const mode = modes[name];
|
|
404
|
+
if (mode === undefined)
|
|
405
|
+
return null;
|
|
406
|
+
const position = this.#indexPosition(node, element, mode.sameType, mode.fromEnd, pseudo.argument.of);
|
|
407
|
+
if (position.result.decision !== "match")
|
|
408
|
+
return position.result;
|
|
409
|
+
return known(matchesAnPlusB(position.index, pseudo.argument.a, pseudo.argument.b)
|
|
410
|
+
? "match"
|
|
411
|
+
: "no-match");
|
|
412
|
+
}
|
|
413
|
+
#indexPosition(node, element, sameType, fromEnd, filter) {
|
|
414
|
+
const parent = this.#index.parent.get(node) ?? null;
|
|
415
|
+
if (parent === null)
|
|
416
|
+
return { index: 0, result: known("no-match") };
|
|
417
|
+
const siblings = (this.#index.children.get(parent) ?? []).filter((sibling) => {
|
|
418
|
+
const data = this.environment.tree.data(sibling);
|
|
419
|
+
return data.kind === "element" &&
|
|
420
|
+
(!sameType ||
|
|
421
|
+
(data.namespace === element.namespace &&
|
|
422
|
+
this.#sameElementType(data, element)));
|
|
423
|
+
});
|
|
424
|
+
const ordered = fromEnd ? [...siblings].reverse() : siblings;
|
|
425
|
+
let index = 0;
|
|
426
|
+
const reasons = [];
|
|
427
|
+
for (const sibling of ordered) {
|
|
428
|
+
let included;
|
|
429
|
+
if (filter.length === 0) {
|
|
430
|
+
included = known("match");
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
included = or(filter.map((selector) => this.#complex(selector, sibling, null)));
|
|
434
|
+
}
|
|
435
|
+
if (included.decision === "unknown")
|
|
436
|
+
reasons.push(...included.reasons);
|
|
437
|
+
if (included.decision === "match")
|
|
438
|
+
index += 1;
|
|
439
|
+
if (sibling === node) {
|
|
440
|
+
if (included.decision === "no-match") {
|
|
441
|
+
return { index: 0, result: known("no-match") };
|
|
442
|
+
}
|
|
443
|
+
return reasons.length > 0
|
|
444
|
+
? {
|
|
445
|
+
index,
|
|
446
|
+
result: Object.freeze({
|
|
447
|
+
decision: "unknown",
|
|
448
|
+
reasons: uniqueReasons(reasons)
|
|
449
|
+
})
|
|
450
|
+
}
|
|
451
|
+
: { index, result: known("match") };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return { index: 0, result: known("no-match") };
|
|
455
|
+
}
|
|
456
|
+
#attribute(element, namespace, name) {
|
|
457
|
+
for (const attribute of element.attributes) {
|
|
458
|
+
const nameEqual = this.environment.documentMode.syntax === "html" &&
|
|
459
|
+
element.namespace === "http://www.w3.org/1999/xhtml" &&
|
|
460
|
+
attribute.namespace === null
|
|
461
|
+
? equalAsciiInsensitive(attribute.localName, name)
|
|
462
|
+
: attribute.localName === name;
|
|
463
|
+
if ((namespace === "*" || attribute.namespace === namespace) &&
|
|
464
|
+
nameEqual) {
|
|
465
|
+
return attribute;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
#sameElementType(left, right) {
|
|
471
|
+
return this.environment.documentMode.syntax === "html" &&
|
|
472
|
+
left.namespace === "http://www.w3.org/1999/xhtml"
|
|
473
|
+
? equalAsciiInsensitive(left.localName, right.localName)
|
|
474
|
+
: left.localName === right.localName;
|
|
475
|
+
}
|
|
476
|
+
#attributeNamespace(selector) {
|
|
477
|
+
if (selector.namespace === null || selector.namespace === "") {
|
|
478
|
+
return Object.freeze({ status: "resolved", namespace: null });
|
|
479
|
+
}
|
|
480
|
+
if (selector.namespace === "*") {
|
|
481
|
+
return Object.freeze({ status: "resolved", namespace: "*" });
|
|
482
|
+
}
|
|
483
|
+
return this.environment.resolveNamespacePrefix(selector.namespace);
|
|
484
|
+
}
|
|
485
|
+
#namespace(selectorNamespace, useDefault, element, name, span) {
|
|
486
|
+
if (selectorNamespace === "*")
|
|
487
|
+
return known("match");
|
|
488
|
+
let resolution;
|
|
489
|
+
if (selectorNamespace === null) {
|
|
490
|
+
if (!useDefault) {
|
|
491
|
+
resolution = Object.freeze({ status: "resolved", namespace: null });
|
|
492
|
+
}
|
|
493
|
+
else if (this.environment.defaultNamespace.kind === "any") {
|
|
494
|
+
return known("match");
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
resolution = Object.freeze({
|
|
498
|
+
status: "resolved",
|
|
499
|
+
namespace: this.environment.defaultNamespace.namespace
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
else if (selectorNamespace === "") {
|
|
504
|
+
resolution = Object.freeze({ status: "resolved", namespace: null });
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
resolution = this.environment.resolveNamespacePrefix(selectorNamespace);
|
|
508
|
+
}
|
|
509
|
+
if (resolution.status === "unknown") {
|
|
510
|
+
return unknown({
|
|
511
|
+
code: "namespace-prefix",
|
|
512
|
+
name: selectorNamespace ?? name,
|
|
513
|
+
span
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
return known(resolution.namespace === element.namespace ? "match" : "no-match");
|
|
517
|
+
}
|
|
518
|
+
#leftCandidates(node, combinator) {
|
|
519
|
+
const parent = this.#index.parent.get(node) ?? null;
|
|
520
|
+
if (combinator === ">") {
|
|
521
|
+
return parent !== null &&
|
|
522
|
+
this.environment.tree.data(parent).kind === "element"
|
|
523
|
+
? Object.freeze([parent])
|
|
524
|
+
: Object.freeze([]);
|
|
525
|
+
}
|
|
526
|
+
if (combinator === "+" || combinator === "~") {
|
|
527
|
+
if (parent === null)
|
|
528
|
+
return Object.freeze([]);
|
|
529
|
+
const siblings = this.#index.children.get(parent) ?? [];
|
|
530
|
+
const nodeIndex = siblings.indexOf(node);
|
|
531
|
+
const previous = siblings
|
|
532
|
+
.slice(0, nodeIndex)
|
|
533
|
+
.filter((sibling) => this.environment.tree.data(sibling).kind === "element")
|
|
534
|
+
.reverse();
|
|
535
|
+
return Object.freeze(combinator === "+" ? previous.slice(0, 1) : previous);
|
|
536
|
+
}
|
|
537
|
+
const ancestors = [];
|
|
538
|
+
let candidate = parent;
|
|
539
|
+
while (candidate !== null) {
|
|
540
|
+
if (this.environment.tree.data(candidate).kind === "element") {
|
|
541
|
+
ancestors.push(candidate);
|
|
542
|
+
}
|
|
543
|
+
candidate = this.#index.parent.get(candidate) ?? null;
|
|
544
|
+
}
|
|
545
|
+
return Object.freeze(ancestors);
|
|
546
|
+
}
|
|
547
|
+
#anchorRelation(combinator, anchor, node) {
|
|
548
|
+
if (combinator === ">") {
|
|
549
|
+
return known(this.#index.parent.get(node) === anchor ? "match" : "no-match");
|
|
550
|
+
}
|
|
551
|
+
const parent = this.#index.parent.get(anchor) ?? null;
|
|
552
|
+
if (combinator === "+" || combinator === "~") {
|
|
553
|
+
if (parent === null || this.#index.parent.get(node) !== parent) {
|
|
554
|
+
return known("no-match");
|
|
555
|
+
}
|
|
556
|
+
const siblings = (this.#index.children.get(parent) ?? []).filter((sibling) => this.environment.tree.data(sibling).kind === "element");
|
|
557
|
+
const anchorIndex = siblings.indexOf(anchor);
|
|
558
|
+
const nodeIndex = siblings.indexOf(node);
|
|
559
|
+
return known(combinator === "+"
|
|
560
|
+
? nodeIndex === anchorIndex + 1
|
|
561
|
+
? "match"
|
|
562
|
+
: "no-match"
|
|
563
|
+
: nodeIndex > anchorIndex
|
|
564
|
+
? "match"
|
|
565
|
+
: "no-match");
|
|
566
|
+
}
|
|
567
|
+
let candidate = this.#index.parent.get(node) ?? null;
|
|
568
|
+
while (candidate !== null) {
|
|
569
|
+
if (candidate === anchor)
|
|
570
|
+
return known("match");
|
|
571
|
+
candidate = this.#index.parent.get(candidate) ?? null;
|
|
572
|
+
}
|
|
573
|
+
return known("no-match");
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function matchesAnPlusB(index, a, b) {
|
|
577
|
+
if (a === 0)
|
|
578
|
+
return index === b;
|
|
579
|
+
const quotient = (index - b) / a;
|
|
580
|
+
return Number.isInteger(quotient) && quotient >= 0;
|
|
581
|
+
}
|
|
582
|
+
function publicResult(result, usage) {
|
|
583
|
+
if (result.decision === "unknown") {
|
|
584
|
+
return Object.freeze({
|
|
585
|
+
status: "unknown",
|
|
586
|
+
reasons: result.reasons,
|
|
587
|
+
usage
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
return Object.freeze({ status: result.decision, usage });
|
|
591
|
+
}
|
|
592
|
+
export function matchSelectorList(selector, node, root, environment, options = {}) {
|
|
593
|
+
const matcher = new SelectorMatcher(root, environment, options);
|
|
594
|
+
return publicResult(matcher.matches(selector, node), matcher.usage());
|
|
595
|
+
}
|
|
596
|
+
export function querySelectorList(selector, root, environment, options = {}) {
|
|
597
|
+
const matcher = new SelectorMatcher(root, environment, options);
|
|
598
|
+
const matches = [];
|
|
599
|
+
const unknownResults = [];
|
|
600
|
+
for (const node of matcher.elements()) {
|
|
601
|
+
const result = matcher.matches(selector, node);
|
|
602
|
+
if (result.decision === "match")
|
|
603
|
+
matches.push(node);
|
|
604
|
+
else if (result.decision === "unknown") {
|
|
605
|
+
unknownResults.push(Object.freeze({
|
|
606
|
+
node,
|
|
607
|
+
reasons: result.reasons
|
|
608
|
+
}));
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return Object.freeze({
|
|
612
|
+
matches: Object.freeze(matches),
|
|
613
|
+
unknown: Object.freeze(unknownResults),
|
|
614
|
+
usage: matcher.usage()
|
|
615
|
+
});
|
|
616
|
+
}
|