@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,702 @@
|
|
|
1
|
+
import { CSS_WEBREF_DATA } from "../generated/css-data.js";
|
|
2
|
+
import { ResourceGuard } from "../syntax/resources.js";
|
|
3
|
+
import { parseCssComponentValues } from "../syntax/parser.js";
|
|
4
|
+
const SELECTORS = "https://drafts.csswg.org/selectors/";
|
|
5
|
+
const LEGACY_PSEUDO_ELEMENTS = new Set([
|
|
6
|
+
"after",
|
|
7
|
+
"before",
|
|
8
|
+
"first-letter",
|
|
9
|
+
"first-line"
|
|
10
|
+
]);
|
|
11
|
+
const FORGIVING_SELECTOR_LIST_PSEUDOS = new Set(["is", "where"]);
|
|
12
|
+
const NTH_PSEUDOS = new Set([
|
|
13
|
+
"nth-child",
|
|
14
|
+
"nth-last-child",
|
|
15
|
+
"nth-of-type",
|
|
16
|
+
"nth-last-of-type"
|
|
17
|
+
]);
|
|
18
|
+
const EXCLUDED_OBSOLETE_PSEUDOS = new Set(["matches"]);
|
|
19
|
+
function knownPseudos(kind, functional) {
|
|
20
|
+
const prefix = kind === "class" ? ":" : "::";
|
|
21
|
+
const names = CSS_WEBREF_DATA.selectors.flatMap((selector) => {
|
|
22
|
+
if (!selector.startsWith(prefix) ||
|
|
23
|
+
(kind === "class" && selector.startsWith("::"))) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
const notation = selector.slice(prefix.length);
|
|
27
|
+
const isFunctional = notation.endsWith("()");
|
|
28
|
+
const name = isFunctional ? notation.slice(0, -2) : notation;
|
|
29
|
+
return isFunctional === functional && !EXCLUDED_OBSOLETE_PSEUDOS.has(name)
|
|
30
|
+
? [name]
|
|
31
|
+
: [];
|
|
32
|
+
});
|
|
33
|
+
return new Set(names);
|
|
34
|
+
}
|
|
35
|
+
const FUNCTIONAL_PSEUDO_CLASSES = knownPseudos("class", true);
|
|
36
|
+
const NON_FUNCTIONAL_PSEUDO_CLASSES = knownPseudos("class", false);
|
|
37
|
+
const FUNCTIONAL_PSEUDO_ELEMENTS = knownPseudos("element", true);
|
|
38
|
+
const NON_FUNCTIONAL_PSEUDO_ELEMENTS = knownPseudos("element", false);
|
|
39
|
+
function lowerAscii(value) {
|
|
40
|
+
return value.replace(/[A-Z]/gu, (character) => character.toLowerCase());
|
|
41
|
+
}
|
|
42
|
+
function frozen(values) {
|
|
43
|
+
return Object.freeze(values);
|
|
44
|
+
}
|
|
45
|
+
function isDelim(value, codePoint) {
|
|
46
|
+
return value?.kind === "delim" && value.value === codePoint;
|
|
47
|
+
}
|
|
48
|
+
function position(offset, line, column) {
|
|
49
|
+
return Object.freeze({ offset, line, column });
|
|
50
|
+
}
|
|
51
|
+
function emptySpan() {
|
|
52
|
+
const start = position(0, 1, 1);
|
|
53
|
+
return Object.freeze({ start, end: start });
|
|
54
|
+
}
|
|
55
|
+
function coveringSpan(values) {
|
|
56
|
+
const first = values[0];
|
|
57
|
+
const last = values.at(-1);
|
|
58
|
+
return first === undefined || last === undefined
|
|
59
|
+
? emptySpan()
|
|
60
|
+
: Object.freeze({ start: first.span.start, end: last.span.end });
|
|
61
|
+
}
|
|
62
|
+
function joinedSpan(start, end) {
|
|
63
|
+
return Object.freeze({ start: start.start, end: end.end });
|
|
64
|
+
}
|
|
65
|
+
function splitAtCommas(values) {
|
|
66
|
+
const groups = [[]];
|
|
67
|
+
for (const value of values) {
|
|
68
|
+
if (value.kind === "comma")
|
|
69
|
+
groups.push([]);
|
|
70
|
+
else
|
|
71
|
+
groups.at(-1)?.push(value);
|
|
72
|
+
}
|
|
73
|
+
return Object.freeze(groups.map((group) => Object.freeze(group)));
|
|
74
|
+
}
|
|
75
|
+
function significant(values) {
|
|
76
|
+
return Object.freeze(values.filter((value) => value.kind !== "whitespace"));
|
|
77
|
+
}
|
|
78
|
+
class SelectorParser {
|
|
79
|
+
values;
|
|
80
|
+
#guard;
|
|
81
|
+
#diagnostics = [];
|
|
82
|
+
#depth = 0;
|
|
83
|
+
#hasDepth = 0;
|
|
84
|
+
constructor(values, options, priorUsage) {
|
|
85
|
+
this.values = values;
|
|
86
|
+
this.#guard = new ResourceGuard(options.limits, options.signal, priorUsage);
|
|
87
|
+
}
|
|
88
|
+
parse(syntaxErrors) {
|
|
89
|
+
const parsed = this.#parseList(this.values, false, false, true);
|
|
90
|
+
const errors = Object.freeze([...syntaxErrors, ...this.#diagnostics]);
|
|
91
|
+
if (syntaxErrors.length > 0 ||
|
|
92
|
+
!parsed.valid ||
|
|
93
|
+
parsed.selectors.length === 0) {
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
ok: false,
|
|
96
|
+
errors,
|
|
97
|
+
usage: this.#usage()
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const value = Object.freeze({
|
|
101
|
+
selectors: parsed.selectors,
|
|
102
|
+
span: coveringSpan(this.values)
|
|
103
|
+
});
|
|
104
|
+
this.#node();
|
|
105
|
+
return Object.freeze({
|
|
106
|
+
ok: true,
|
|
107
|
+
value,
|
|
108
|
+
errors,
|
|
109
|
+
usage: this.#usage()
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
#parseList(values, forgiving, relative, allowPseudoElements) {
|
|
113
|
+
return this.#nested(() => {
|
|
114
|
+
const selectors = [];
|
|
115
|
+
let valid = true;
|
|
116
|
+
for (const group of splitAtCommas(values)) {
|
|
117
|
+
this.#guard.step();
|
|
118
|
+
const diagnosticStart = this.#diagnostics.length;
|
|
119
|
+
const localDiagnostics = [];
|
|
120
|
+
const parser = new ComplexSelectorParser(group, relative, allowPseudoElements, this.#guard, (code, message, span) => {
|
|
121
|
+
localDiagnostics.push(Object.freeze({
|
|
122
|
+
kind: "selector",
|
|
123
|
+
code,
|
|
124
|
+
message,
|
|
125
|
+
span,
|
|
126
|
+
specRef: `${SELECTORS}#grammar`
|
|
127
|
+
}));
|
|
128
|
+
}, (functionBlock, kind) => this.#pseudoArgument(functionBlock, kind));
|
|
129
|
+
const selector = parser.parse();
|
|
130
|
+
if (selector === null) {
|
|
131
|
+
valid = false;
|
|
132
|
+
if (forgiving) {
|
|
133
|
+
this.#diagnostics.splice(diagnosticStart);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
this.#diagnostics.push(...localDiagnostics);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
this.#diagnostics.push(...localDiagnostics);
|
|
141
|
+
selectors.push(selector);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return Object.freeze({
|
|
145
|
+
selectors: frozen(selectors),
|
|
146
|
+
valid: forgiving || valid
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
#pseudoArgument(value, kind) {
|
|
151
|
+
const name = lowerAscii(value.name);
|
|
152
|
+
if ((kind === "class" && NON_FUNCTIONAL_PSEUDO_CLASSES.has(name)) ||
|
|
153
|
+
(kind === "element" && NON_FUNCTIONAL_PSEUDO_ELEMENTS.has(name))) {
|
|
154
|
+
this.#diagnostic("invalid-pseudo", `:${kind === "element" ? ":" : ""}${name} is not functional.`, value.span);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
if (kind === "class" &&
|
|
158
|
+
FORGIVING_SELECTOR_LIST_PSEUDOS.has(name)) {
|
|
159
|
+
const parsed = this.#parseList(value.value, true, false, false);
|
|
160
|
+
return Object.freeze({
|
|
161
|
+
kind: "selector-list",
|
|
162
|
+
selectors: parsed.selectors,
|
|
163
|
+
forgiving: true,
|
|
164
|
+
relative: false
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (kind === "class" && name === "not") {
|
|
168
|
+
const parsed = this.#parseList(value.value, false, false, false);
|
|
169
|
+
if (!parsed.valid || parsed.selectors.length === 0) {
|
|
170
|
+
this.#diagnostic("invalid-pseudo", ":not() requires at least one valid selector.", value.span);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return Object.freeze({
|
|
174
|
+
kind: "selector-list",
|
|
175
|
+
selectors: parsed.selectors,
|
|
176
|
+
forgiving: false,
|
|
177
|
+
relative: false
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (kind === "class" && name === "has") {
|
|
181
|
+
if (this.#hasDepth > 0) {
|
|
182
|
+
this.#diagnostic("invalid-pseudo", ":has() cannot be nested.", value.span);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
this.#hasDepth += 1;
|
|
186
|
+
let parsed;
|
|
187
|
+
try {
|
|
188
|
+
parsed = this.#parseList(value.value, false, true, false);
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
this.#hasDepth -= 1;
|
|
192
|
+
}
|
|
193
|
+
if (!parsed.valid || parsed.selectors.length === 0) {
|
|
194
|
+
this.#diagnostic("invalid-pseudo", ":has() requires at least one valid relative selector.", value.span);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
return Object.freeze({
|
|
198
|
+
kind: "selector-list",
|
|
199
|
+
selectors: parsed.selectors,
|
|
200
|
+
forgiving: false,
|
|
201
|
+
relative: true
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (kind === "class" && NTH_PSEUDOS.has(name)) {
|
|
205
|
+
return this.#parseNth(value);
|
|
206
|
+
}
|
|
207
|
+
if (kind === "class" && name === "dir") {
|
|
208
|
+
const parts = significant(value.value);
|
|
209
|
+
if (parts.length !== 1 || parts[0]?.kind !== "ident") {
|
|
210
|
+
this.#diagnostic("invalid-pseudo", ":dir() requires one identifier.", value.span);
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (kind === "class" && name === "lang") {
|
|
215
|
+
const ranges = splitAtCommas(value.value);
|
|
216
|
+
if (ranges.length === 0 ||
|
|
217
|
+
ranges.some((range) => {
|
|
218
|
+
const parts = significant(range);
|
|
219
|
+
return parts.length !== 1 ||
|
|
220
|
+
(parts[0]?.kind !== "ident" && parts[0]?.kind !== "string");
|
|
221
|
+
})) {
|
|
222
|
+
this.#diagnostic("invalid-pseudo", ":lang() requires a comma-separated list of identifiers or strings.", value.span);
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (kind === "element" && name === "slotted") {
|
|
227
|
+
const parsed = this.#parseList(value.value, false, false, false);
|
|
228
|
+
if (!parsed.valid || parsed.selectors.length !== 1) {
|
|
229
|
+
this.#diagnostic("invalid-pseudo", "::slotted() requires one compound selector.", value.span);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
return Object.freeze({
|
|
233
|
+
kind: "selector-list",
|
|
234
|
+
selectors: parsed.selectors,
|
|
235
|
+
forgiving: false,
|
|
236
|
+
relative: false
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return Object.freeze({
|
|
240
|
+
kind: "raw",
|
|
241
|
+
value: Object.freeze([...value.value])
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
#parseNth(value) {
|
|
245
|
+
const parts = [...value.value];
|
|
246
|
+
let ofIndex = -1;
|
|
247
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
248
|
+
const part = parts[index];
|
|
249
|
+
if (part?.kind === "ident" && lowerAscii(part.value) === "of") {
|
|
250
|
+
ofIndex = index;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const formulaValues = significant(ofIndex < 0 ? parts : parts.slice(0, ofIndex));
|
|
255
|
+
const formula = formulaValues.map((part) => {
|
|
256
|
+
if (part.kind === "ident")
|
|
257
|
+
return part.value;
|
|
258
|
+
if (part.kind === "number")
|
|
259
|
+
return part.representation;
|
|
260
|
+
if (part.kind === "dimension") {
|
|
261
|
+
return `${part.representation}${part.unit}`;
|
|
262
|
+
}
|
|
263
|
+
return part.kind === "delim"
|
|
264
|
+
? String.fromCodePoint(part.value)
|
|
265
|
+
: "";
|
|
266
|
+
}).join("");
|
|
267
|
+
const coefficients = parseAnPlusB(formula);
|
|
268
|
+
if (coefficients === null) {
|
|
269
|
+
this.#diagnostic("invalid-nth", "Expected an An+B expression.", value.span);
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
let of = Object.freeze([]);
|
|
273
|
+
if (ofIndex >= 0) {
|
|
274
|
+
const name = lowerAscii(value.name);
|
|
275
|
+
if (name !== "nth-child" && name !== "nth-last-child") {
|
|
276
|
+
this.#diagnostic("invalid-nth", `:${name}() does not accept an of selector list.`, value.span);
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const parsed = this.#parseList(parts.slice(ofIndex + 1), false, false, false);
|
|
280
|
+
if (!parsed.valid || parsed.selectors.length === 0) {
|
|
281
|
+
this.#diagnostic("invalid-nth", "The of clause requires a selector list.", value.span);
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
of = parsed.selectors;
|
|
285
|
+
}
|
|
286
|
+
return Object.freeze({
|
|
287
|
+
kind: "nth",
|
|
288
|
+
...coefficients,
|
|
289
|
+
of
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
#diagnostic(code, message, span) {
|
|
293
|
+
this.#diagnostics.push(Object.freeze({
|
|
294
|
+
kind: "selector",
|
|
295
|
+
code,
|
|
296
|
+
message,
|
|
297
|
+
span,
|
|
298
|
+
specRef: `${SELECTORS}#grammar`
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
#node() {
|
|
302
|
+
this.#guard.createNode(this.#depth);
|
|
303
|
+
}
|
|
304
|
+
#usage() {
|
|
305
|
+
return this.#guard.snapshot();
|
|
306
|
+
}
|
|
307
|
+
#nested(operation) {
|
|
308
|
+
this.#depth += 1;
|
|
309
|
+
try {
|
|
310
|
+
this.#guard.createNode(this.#depth);
|
|
311
|
+
return operation();
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
this.#depth -= 1;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
class ComplexSelectorParser {
|
|
319
|
+
relative;
|
|
320
|
+
allowPseudoElements;
|
|
321
|
+
guard;
|
|
322
|
+
diagnostic;
|
|
323
|
+
pseudoArgument;
|
|
324
|
+
#values;
|
|
325
|
+
#index = 0;
|
|
326
|
+
#failed = false;
|
|
327
|
+
constructor(values, relative, allowPseudoElements, guard, diagnostic, pseudoArgument) {
|
|
328
|
+
this.relative = relative;
|
|
329
|
+
this.allowPseudoElements = allowPseudoElements;
|
|
330
|
+
this.guard = guard;
|
|
331
|
+
this.diagnostic = diagnostic;
|
|
332
|
+
this.pseudoArgument = pseudoArgument;
|
|
333
|
+
this.#values = values;
|
|
334
|
+
}
|
|
335
|
+
parse() {
|
|
336
|
+
this.#skipWhitespace();
|
|
337
|
+
const leadingCombinator = this.relative ? this.#explicitCombinator() : null;
|
|
338
|
+
if (leadingCombinator !== null)
|
|
339
|
+
this.#skipWhitespace();
|
|
340
|
+
const compounds = [];
|
|
341
|
+
const combinators = [];
|
|
342
|
+
const first = this.#compound();
|
|
343
|
+
if (first === null) {
|
|
344
|
+
if (!this.#failed) {
|
|
345
|
+
this.#fail("empty-selector", "Expected a compound selector.", coveringSpan(this.#values));
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
compounds.push(first);
|
|
350
|
+
while (this.#index < this.#values.length) {
|
|
351
|
+
const hadWhitespace = this.#skipWhitespace();
|
|
352
|
+
let combinator = this.#explicitCombinator();
|
|
353
|
+
if (combinator === null && hadWhitespace)
|
|
354
|
+
combinator = " ";
|
|
355
|
+
if (combinator === null) {
|
|
356
|
+
this.#fail("invalid-selector", "Expected a combinator or the end of the selector.", this.#currentSpan());
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
this.#skipWhitespace();
|
|
360
|
+
const compound = this.#compound();
|
|
361
|
+
if (compound === null) {
|
|
362
|
+
this.#fail("invalid-combinator", "A combinator must be followed by a compound selector.", this.#currentSpan());
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
combinators.push(combinator);
|
|
366
|
+
compounds.push(compound);
|
|
367
|
+
}
|
|
368
|
+
const span = joinedSpan(compounds[0]?.span ?? coveringSpan(this.#values), compounds.at(-1)?.span ?? coveringSpan(this.#values));
|
|
369
|
+
this.guard.createNode(1);
|
|
370
|
+
return Object.freeze({
|
|
371
|
+
leadingCombinator,
|
|
372
|
+
compounds: frozen(compounds),
|
|
373
|
+
combinators: frozen(combinators),
|
|
374
|
+
span
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
#compound() {
|
|
378
|
+
const start = this.#index;
|
|
379
|
+
const type = this.#typeSelector();
|
|
380
|
+
const simples = [];
|
|
381
|
+
let pseudoElementSeen = false;
|
|
382
|
+
for (;;) {
|
|
383
|
+
this.guard.step();
|
|
384
|
+
const simple = this.#simpleSelector();
|
|
385
|
+
if (simple === null)
|
|
386
|
+
break;
|
|
387
|
+
if (pseudoElementSeen &&
|
|
388
|
+
simple.kind !== "pseudo-class" &&
|
|
389
|
+
simple.kind !== "pseudo-element") {
|
|
390
|
+
this.#fail("invalid-selector", "Only pseudo selectors may follow a pseudo-element.", simple.span);
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
if (!this.allowPseudoElements &&
|
|
394
|
+
simple.kind === "pseudo-element") {
|
|
395
|
+
this.#fail("invalid-selector", "Pseudo-elements are not allowed in this selector list.", simple.span);
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
if (simple.kind === "pseudo-element")
|
|
399
|
+
pseudoElementSeen = true;
|
|
400
|
+
simples.push(simple);
|
|
401
|
+
}
|
|
402
|
+
if (type === null && simples.length === 0)
|
|
403
|
+
return null;
|
|
404
|
+
const consumed = this.#values.slice(start, this.#index);
|
|
405
|
+
this.guard.createNode(2);
|
|
406
|
+
return Object.freeze({
|
|
407
|
+
type,
|
|
408
|
+
simples: frozen(simples),
|
|
409
|
+
span: coveringSpan(consumed)
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
#typeSelector() {
|
|
413
|
+
const start = this.#index;
|
|
414
|
+
const first = this.#values[this.#index];
|
|
415
|
+
const firstName = this.#nameOrStar(first);
|
|
416
|
+
const startsEmptyNamespace = isDelim(first, 0x7c);
|
|
417
|
+
if (firstName === null && !startsEmptyNamespace)
|
|
418
|
+
return null;
|
|
419
|
+
let namespace = null;
|
|
420
|
+
let name = firstName;
|
|
421
|
+
if (startsEmptyNamespace) {
|
|
422
|
+
namespace = "";
|
|
423
|
+
this.#index += 1;
|
|
424
|
+
name = this.#nameOrStar(this.#values[this.#index]);
|
|
425
|
+
if (name === null) {
|
|
426
|
+
this.#index = start;
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
this.#index += 1;
|
|
430
|
+
}
|
|
431
|
+
else if (isDelim(this.#values[this.#index + 1], 0x7c) &&
|
|
432
|
+
!isDelim(this.#values[this.#index + 2], 0x7c)) {
|
|
433
|
+
namespace = firstName;
|
|
434
|
+
this.#index += 2;
|
|
435
|
+
name = this.#nameOrStar(this.#values[this.#index]);
|
|
436
|
+
if (name === null) {
|
|
437
|
+
this.#index = start;
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
this.#index += 1;
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
this.#index += 1;
|
|
444
|
+
}
|
|
445
|
+
if (name === null) {
|
|
446
|
+
this.#index = start;
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
const span = coveringSpan(this.#values.slice(start, this.#index));
|
|
450
|
+
this.guard.createNode(3);
|
|
451
|
+
return Object.freeze({
|
|
452
|
+
kind: "type",
|
|
453
|
+
namespace,
|
|
454
|
+
name,
|
|
455
|
+
universal: name === "*",
|
|
456
|
+
span
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
#simpleSelector() {
|
|
460
|
+
const value = this.#values[this.#index];
|
|
461
|
+
if (value?.kind === "hash" && value.hashType === "id") {
|
|
462
|
+
this.#index += 1;
|
|
463
|
+
this.guard.createNode(3);
|
|
464
|
+
return Object.freeze({ kind: "id", value: value.value, span: value.span });
|
|
465
|
+
}
|
|
466
|
+
if (isDelim(value, 0x2e)) {
|
|
467
|
+
const name = this.#values[this.#index + 1];
|
|
468
|
+
if (name?.kind !== "ident")
|
|
469
|
+
return null;
|
|
470
|
+
this.#index += 2;
|
|
471
|
+
this.guard.createNode(3);
|
|
472
|
+
return Object.freeze({
|
|
473
|
+
kind: "class",
|
|
474
|
+
value: name.value,
|
|
475
|
+
span: joinedSpan(value?.span ?? name.span, name.span)
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
if (value?.kind === "simple-block" &&
|
|
479
|
+
value.associatedToken === "open-square") {
|
|
480
|
+
this.#index += 1;
|
|
481
|
+
return this.#attribute(value);
|
|
482
|
+
}
|
|
483
|
+
if (value?.kind === "colon")
|
|
484
|
+
return this.#pseudo();
|
|
485
|
+
if (isDelim(value, 0x26)) {
|
|
486
|
+
this.#index += 1;
|
|
487
|
+
this.guard.createNode(3);
|
|
488
|
+
return Object.freeze({ kind: "nesting", span: value?.span ?? emptySpan() });
|
|
489
|
+
}
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
#attribute(block) {
|
|
493
|
+
const parts = significant(block.value);
|
|
494
|
+
let index = 0;
|
|
495
|
+
const firstName = this.#nameOrStar(parts[index]);
|
|
496
|
+
const emptyNamespace = isDelim(parts[index], 0x7c);
|
|
497
|
+
let namespace = null;
|
|
498
|
+
let name = firstName;
|
|
499
|
+
if (emptyNamespace) {
|
|
500
|
+
namespace = "";
|
|
501
|
+
index += 1;
|
|
502
|
+
name = this.#nameOrStar(parts[index]);
|
|
503
|
+
index += 1;
|
|
504
|
+
}
|
|
505
|
+
else if (isDelim(parts[index + 1], 0x7c) &&
|
|
506
|
+
!isDelim(parts[index + 2], 0x3d)) {
|
|
507
|
+
namespace = firstName;
|
|
508
|
+
index += 2;
|
|
509
|
+
name = this.#nameOrStar(parts[index]);
|
|
510
|
+
index += 1;
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
index += 1;
|
|
514
|
+
}
|
|
515
|
+
if (name === null || name === "*") {
|
|
516
|
+
this.#fail("invalid-attribute", "Expected an attribute name.", block.span);
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
let matcher = null;
|
|
520
|
+
let expected = null;
|
|
521
|
+
let modifier = null;
|
|
522
|
+
if (index < parts.length) {
|
|
523
|
+
const first = parts[index];
|
|
524
|
+
const second = parts[index + 1];
|
|
525
|
+
if (isDelim(first, 0x3d)) {
|
|
526
|
+
matcher = "=";
|
|
527
|
+
index += 1;
|
|
528
|
+
}
|
|
529
|
+
else if (first?.kind === "delim" &&
|
|
530
|
+
[0x7e, 0x7c, 0x5e, 0x24, 0x2a].includes(first.value) &&
|
|
531
|
+
isDelim(second, 0x3d)) {
|
|
532
|
+
matcher = `${String.fromCodePoint(first.value)}=`;
|
|
533
|
+
index += 2;
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
this.#fail("invalid-attribute", "Expected an attribute matcher.", block.span);
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
const expectedToken = parts[index];
|
|
540
|
+
if (expectedToken?.kind !== "ident" &&
|
|
541
|
+
expectedToken?.kind !== "string") {
|
|
542
|
+
this.#fail("invalid-attribute", "Expected an identifier or string attribute value.", block.span);
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
expected = expectedToken.value;
|
|
546
|
+
index += 1;
|
|
547
|
+
const modifierToken = parts[index];
|
|
548
|
+
if (modifierToken?.kind === "ident") {
|
|
549
|
+
const candidate = lowerAscii(modifierToken.value);
|
|
550
|
+
if (candidate === "i" || candidate === "s") {
|
|
551
|
+
modifier = candidate;
|
|
552
|
+
index += 1;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (index !== parts.length) {
|
|
557
|
+
this.#fail("invalid-attribute", "Unexpected content in attribute selector.", block.span);
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
this.guard.createNode(3);
|
|
561
|
+
return Object.freeze({
|
|
562
|
+
kind: "attribute",
|
|
563
|
+
namespace,
|
|
564
|
+
name,
|
|
565
|
+
matcher,
|
|
566
|
+
value: expected,
|
|
567
|
+
modifier,
|
|
568
|
+
span: block.span
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
#pseudo() {
|
|
572
|
+
const firstColon = this.#values[this.#index];
|
|
573
|
+
let kind = "class";
|
|
574
|
+
this.#index += 1;
|
|
575
|
+
if (this.#values[this.#index]?.kind === "colon") {
|
|
576
|
+
kind = "element";
|
|
577
|
+
this.#index += 1;
|
|
578
|
+
}
|
|
579
|
+
const nameValue = this.#values[this.#index];
|
|
580
|
+
if (nameValue?.kind !== "ident" && nameValue?.kind !== "function-block") {
|
|
581
|
+
this.#fail("invalid-pseudo", "Expected a pseudo selector name.", firstColon?.span ?? emptySpan());
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
this.#index += 1;
|
|
585
|
+
const name = lowerAscii(nameValue.kind === "ident" ? nameValue.value : nameValue.name);
|
|
586
|
+
if (kind === "class" && LEGACY_PSEUDO_ELEMENTS.has(name))
|
|
587
|
+
kind = "element";
|
|
588
|
+
const knownFunctional = kind === "class"
|
|
589
|
+
? FUNCTIONAL_PSEUDO_CLASSES
|
|
590
|
+
: FUNCTIONAL_PSEUDO_ELEMENTS;
|
|
591
|
+
const knownNonFunctional = kind === "class"
|
|
592
|
+
? NON_FUNCTIONAL_PSEUDO_CLASSES
|
|
593
|
+
: NON_FUNCTIONAL_PSEUDO_ELEMENTS;
|
|
594
|
+
if ((nameValue.kind === "ident" && !knownNonFunctional.has(name)) ||
|
|
595
|
+
(nameValue.kind === "function-block" && !knownFunctional.has(name))) {
|
|
596
|
+
this.#fail("invalid-pseudo", `Unknown pseudo-${kind}: ${name}.`, nameValue.span);
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
if (nameValue.kind === "ident" &&
|
|
600
|
+
knownFunctional.has(name)) {
|
|
601
|
+
this.#fail("invalid-pseudo", `:${kind === "element" ? ":" : ""}${name} requires arguments.`, nameValue.span);
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
const argument = nameValue.kind === "function-block"
|
|
605
|
+
? this.pseudoArgument(nameValue, kind)
|
|
606
|
+
: Object.freeze({ kind: "none" });
|
|
607
|
+
if (argument === null) {
|
|
608
|
+
this.#failed = true;
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
const span = joinedSpan(firstColon?.span ?? nameValue.span, nameValue.span);
|
|
612
|
+
this.guard.createNode(3);
|
|
613
|
+
return kind === "class"
|
|
614
|
+
? Object.freeze({
|
|
615
|
+
kind: "pseudo-class",
|
|
616
|
+
name,
|
|
617
|
+
argument,
|
|
618
|
+
span
|
|
619
|
+
})
|
|
620
|
+
: Object.freeze({
|
|
621
|
+
kind: "pseudo-element",
|
|
622
|
+
name,
|
|
623
|
+
argument,
|
|
624
|
+
span
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
#explicitCombinator() {
|
|
628
|
+
const value = this.#values[this.#index];
|
|
629
|
+
if (isDelim(value, 0x3e)) {
|
|
630
|
+
this.#index += 1;
|
|
631
|
+
return ">";
|
|
632
|
+
}
|
|
633
|
+
if (isDelim(value, 0x2b)) {
|
|
634
|
+
this.#index += 1;
|
|
635
|
+
return "+";
|
|
636
|
+
}
|
|
637
|
+
if (isDelim(value, 0x7e)) {
|
|
638
|
+
this.#index += 1;
|
|
639
|
+
return "~";
|
|
640
|
+
}
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
#skipWhitespace() {
|
|
644
|
+
const start = this.#index;
|
|
645
|
+
while (this.#values[this.#index]?.kind === "whitespace") {
|
|
646
|
+
this.guard.step();
|
|
647
|
+
this.#index += 1;
|
|
648
|
+
}
|
|
649
|
+
return this.#index !== start;
|
|
650
|
+
}
|
|
651
|
+
#nameOrStar(value) {
|
|
652
|
+
if (value?.kind === "ident")
|
|
653
|
+
return value.value;
|
|
654
|
+
if (isDelim(value, 0x2a))
|
|
655
|
+
return "*";
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
#currentSpan() {
|
|
659
|
+
return this.#values[this.#index]?.span ??
|
|
660
|
+
this.#values.at(-1)?.span ??
|
|
661
|
+
emptySpan();
|
|
662
|
+
}
|
|
663
|
+
#fail(code, message, span) {
|
|
664
|
+
this.#failed = true;
|
|
665
|
+
this.diagnostic(code, message, span);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function parseAnPlusB(source) {
|
|
669
|
+
const normalized = lowerAscii(source);
|
|
670
|
+
if (normalized === "odd")
|
|
671
|
+
return Object.freeze({ a: 2, b: 1 });
|
|
672
|
+
if (normalized === "even")
|
|
673
|
+
return Object.freeze({ a: 2, b: 0 });
|
|
674
|
+
if (/^[+-]?\d+$/u.test(normalized)) {
|
|
675
|
+
return Object.freeze({ a: 0, b: Number(normalized) });
|
|
676
|
+
}
|
|
677
|
+
const match = /^([+-]?\d*)n(?:([+-]\d+))?$/u.exec(normalized);
|
|
678
|
+
if (match === null)
|
|
679
|
+
return null;
|
|
680
|
+
const coefficient = match[1];
|
|
681
|
+
const a = coefficient === "" || coefficient === "+"
|
|
682
|
+
? 1
|
|
683
|
+
: coefficient === "-"
|
|
684
|
+
? -1
|
|
685
|
+
: Number(coefficient);
|
|
686
|
+
return Object.freeze({ a, b: Number(match[2] ?? 0) });
|
|
687
|
+
}
|
|
688
|
+
export function parseSelectorList(source, options = {}) {
|
|
689
|
+
const syntax = parseCssComponentValues(source, {
|
|
690
|
+
...(options.limits === undefined ? {} : { limits: options.limits }),
|
|
691
|
+
...(options.signal === undefined ? {} : { signal: options.signal })
|
|
692
|
+
});
|
|
693
|
+
if (!syntax.ok) {
|
|
694
|
+
return Object.freeze({
|
|
695
|
+
ok: false,
|
|
696
|
+
errors: syntax.errors,
|
|
697
|
+
usage: syntax.usage
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
const parser = new SelectorParser(syntax.value, options, syntax.usage);
|
|
701
|
+
return parser.parse(syntax.errors);
|
|
702
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ComplexSelector, SelectorList, SelectorSpecificity, SelectorSpecificityOptions } from "./types.js";
|
|
2
|
+
export declare function specificityOfComplexSelector(selector: ComplexSelector, options?: SelectorSpecificityOptions): SelectorSpecificity;
|
|
3
|
+
export declare function specificitiesOfSelectorList(list: SelectorList, options?: SelectorSpecificityOptions): readonly SelectorSpecificity[];
|