@ismail-elkorchi/css-parser 0.1.1 → 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 +64 -78
- 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 -69
- package/dist/public/mod.js +12 -1794
- 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 -56
- 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,581 @@
|
|
|
1
|
+
import { ResourceGuard } from "./resources.js";
|
|
2
|
+
import { TokenStream } from "./token-stream.js";
|
|
3
|
+
import { CssTokenizer } from "./tokenizer.js";
|
|
4
|
+
const CSS_SYNTAX = "https://drafts.csswg.org/css-syntax/";
|
|
5
|
+
function freezeArray(values) {
|
|
6
|
+
return Object.freeze(values);
|
|
7
|
+
}
|
|
8
|
+
function lowerAscii(value) {
|
|
9
|
+
return value.replace(/[A-Z]/gu, (character) => character.toLowerCase());
|
|
10
|
+
}
|
|
11
|
+
function positionAtEnd(input) {
|
|
12
|
+
let line = 1;
|
|
13
|
+
let column = 1;
|
|
14
|
+
for (let offset = 0; offset < input.length;) {
|
|
15
|
+
const first = input.charCodeAt(offset);
|
|
16
|
+
if (first === 0x0d) {
|
|
17
|
+
offset += input.charCodeAt(offset + 1) === 0x0a ? 2 : 1;
|
|
18
|
+
line += 1;
|
|
19
|
+
column = 1;
|
|
20
|
+
}
|
|
21
|
+
else if (first === 0x0a || first === 0x0c) {
|
|
22
|
+
offset += 1;
|
|
23
|
+
line += 1;
|
|
24
|
+
column = 1;
|
|
25
|
+
}
|
|
26
|
+
else if (first >= 0xd800 &&
|
|
27
|
+
first <= 0xdbff &&
|
|
28
|
+
input.charCodeAt(offset + 1) >= 0xdc00 &&
|
|
29
|
+
input.charCodeAt(offset + 1) <= 0xdfff) {
|
|
30
|
+
offset += 2;
|
|
31
|
+
column += 1;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
offset += 1;
|
|
35
|
+
column += 1;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return Object.freeze({ offset: input.length, line, column });
|
|
39
|
+
}
|
|
40
|
+
function emptySpan(input) {
|
|
41
|
+
const end = positionAtEnd(input);
|
|
42
|
+
return Object.freeze({ start: end, end });
|
|
43
|
+
}
|
|
44
|
+
function isClosingToken(token) {
|
|
45
|
+
return (token.kind === "close-square" ||
|
|
46
|
+
token.kind === "close-paren" ||
|
|
47
|
+
token.kind === "close-curly");
|
|
48
|
+
}
|
|
49
|
+
function isPreservedToken(token) {
|
|
50
|
+
return (token.kind !== "eof" &&
|
|
51
|
+
token.kind !== "function" &&
|
|
52
|
+
token.kind !== "open-square" &&
|
|
53
|
+
token.kind !== "open-paren" &&
|
|
54
|
+
token.kind !== "open-curly");
|
|
55
|
+
}
|
|
56
|
+
function isWhitespace(value) {
|
|
57
|
+
return value.kind === "whitespace";
|
|
58
|
+
}
|
|
59
|
+
function remapPosition(local, base) {
|
|
60
|
+
return Object.freeze({
|
|
61
|
+
offset: base.offset + local.offset,
|
|
62
|
+
line: base.line + local.line - 1,
|
|
63
|
+
column: local.line === 1 ? base.column + local.column - 1 : local.column
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function remapSpan(local, base) {
|
|
67
|
+
return Object.freeze({
|
|
68
|
+
start: remapPosition(local.start, base),
|
|
69
|
+
end: remapPosition(local.end, base)
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function remapToken(token, base) {
|
|
73
|
+
return Object.freeze({
|
|
74
|
+
...token,
|
|
75
|
+
span: remapSpan(token.span, base)
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function closingKind(opening) {
|
|
79
|
+
switch (opening) {
|
|
80
|
+
case "open-square":
|
|
81
|
+
return "close-square";
|
|
82
|
+
case "open-paren":
|
|
83
|
+
return "close-paren";
|
|
84
|
+
case "open-curly":
|
|
85
|
+
return "close-curly";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export class CssSyntaxParser {
|
|
89
|
+
#input;
|
|
90
|
+
#guard;
|
|
91
|
+
#diagnostics = [];
|
|
92
|
+
#tokenizerErrors = [];
|
|
93
|
+
#nextNodeId = 1;
|
|
94
|
+
constructor(input, options = {}) {
|
|
95
|
+
this.#input = input;
|
|
96
|
+
this.#guard = new ResourceGuard(options.limits, options.signal);
|
|
97
|
+
}
|
|
98
|
+
parseStylesheet() {
|
|
99
|
+
const { stream, tokenizerErrors } = this.#tokenize();
|
|
100
|
+
this.#tokenizerErrors = tokenizerErrors;
|
|
101
|
+
const start = stream.index;
|
|
102
|
+
const rules = this.#consumeStylesheetContents(stream, 2);
|
|
103
|
+
const stylesheet = this.#stylesheet(rules, stream.spanFrom(start), 1);
|
|
104
|
+
return this.#success(stylesheet);
|
|
105
|
+
}
|
|
106
|
+
parseStylesheetContents() {
|
|
107
|
+
const { stream, tokenizerErrors } = this.#tokenize();
|
|
108
|
+
this.#tokenizerErrors = tokenizerErrors;
|
|
109
|
+
return this.#success(this.#consumeStylesheetContents(stream, 1));
|
|
110
|
+
}
|
|
111
|
+
parseBlockContents() {
|
|
112
|
+
const { stream, tokenizerErrors } = this.#tokenize();
|
|
113
|
+
this.#tokenizerErrors = tokenizerErrors;
|
|
114
|
+
return this.#success(this.#consumeBlockContents(stream, 1));
|
|
115
|
+
}
|
|
116
|
+
parseRule() {
|
|
117
|
+
const { stream, tokenizerErrors } = this.#tokenize();
|
|
118
|
+
this.#tokenizerErrors = tokenizerErrors;
|
|
119
|
+
stream.discardWhitespace();
|
|
120
|
+
if (stream.empty) {
|
|
121
|
+
this.#diagnostic("empty-input", "A rule was requested from empty input.", emptySpan(this.#input), "#parse-a-rule");
|
|
122
|
+
return this.#failure();
|
|
123
|
+
}
|
|
124
|
+
const rule = stream.next.kind === "at-keyword"
|
|
125
|
+
? this.#consumeAtRule(stream, false, 1)
|
|
126
|
+
: this.#consumeQualifiedRule(stream, false, undefined, 1);
|
|
127
|
+
if (rule === null) {
|
|
128
|
+
this.#diagnostic("invalid-rule", "The input does not contain a complete rule.", stream.next.span, "#parse-a-rule");
|
|
129
|
+
return this.#failure();
|
|
130
|
+
}
|
|
131
|
+
stream.discardWhitespace();
|
|
132
|
+
if (this.#nextKind(stream) !== "eof") {
|
|
133
|
+
this.#diagnostic("trailing-input", "A single-rule input contains trailing tokens.", stream.next.span, "#parse-a-rule");
|
|
134
|
+
return this.#failure();
|
|
135
|
+
}
|
|
136
|
+
return this.#success(rule);
|
|
137
|
+
}
|
|
138
|
+
parseDeclaration() {
|
|
139
|
+
const { stream, tokenizerErrors } = this.#tokenize();
|
|
140
|
+
this.#tokenizerErrors = tokenizerErrors;
|
|
141
|
+
stream.discardWhitespace();
|
|
142
|
+
const declaration = this.#consumeDeclaration(stream, false, 1);
|
|
143
|
+
if (declaration === null) {
|
|
144
|
+
this.#diagnostic("invalid-declaration", "The input does not contain a syntactically valid declaration.", stream.next.span, "#parse-a-declaration");
|
|
145
|
+
return this.#failure();
|
|
146
|
+
}
|
|
147
|
+
return this.#success(declaration);
|
|
148
|
+
}
|
|
149
|
+
parseComponentValue() {
|
|
150
|
+
const { stream, tokenizerErrors } = this.#tokenize();
|
|
151
|
+
this.#tokenizerErrors = tokenizerErrors;
|
|
152
|
+
stream.discardWhitespace();
|
|
153
|
+
if (stream.empty) {
|
|
154
|
+
this.#diagnostic("empty-input", "A component value was requested from empty input.", emptySpan(this.#input), "#parse-a-component-value");
|
|
155
|
+
return this.#failure();
|
|
156
|
+
}
|
|
157
|
+
const value = this.#consumeComponentValue(stream, 1);
|
|
158
|
+
stream.discardWhitespace();
|
|
159
|
+
if (this.#nextKind(stream) !== "eof") {
|
|
160
|
+
this.#diagnostic("trailing-input", "A single-component-value input contains trailing tokens.", stream.next.span, "#parse-a-component-value");
|
|
161
|
+
return this.#failure();
|
|
162
|
+
}
|
|
163
|
+
return this.#success(value);
|
|
164
|
+
}
|
|
165
|
+
parseComponentValues() {
|
|
166
|
+
const { stream, tokenizerErrors } = this.#tokenize();
|
|
167
|
+
this.#tokenizerErrors = tokenizerErrors;
|
|
168
|
+
return this.#success(this.#consumeComponentValues(stream, undefined, false, 1));
|
|
169
|
+
}
|
|
170
|
+
parseCommaSeparatedComponentValues() {
|
|
171
|
+
const { stream, tokenizerErrors } = this.#tokenize();
|
|
172
|
+
this.#tokenizerErrors = tokenizerErrors;
|
|
173
|
+
const groups = [];
|
|
174
|
+
while (!stream.empty) {
|
|
175
|
+
groups.push(this.#consumeComponentValues(stream, "comma", false, 1));
|
|
176
|
+
stream.discard();
|
|
177
|
+
}
|
|
178
|
+
return this.#success(freezeArray(groups));
|
|
179
|
+
}
|
|
180
|
+
#tokenize() {
|
|
181
|
+
const tokenizer = new CssTokenizer(this.#input, { guard: this.#guard });
|
|
182
|
+
const result = tokenizer.tokenize();
|
|
183
|
+
return {
|
|
184
|
+
stream: new TokenStream(result.tokens, result.end),
|
|
185
|
+
tokenizerErrors: result.errors
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
#consumeStylesheetContents(stream, depth) {
|
|
189
|
+
const rules = [];
|
|
190
|
+
while (!stream.empty) {
|
|
191
|
+
const token = stream.next;
|
|
192
|
+
if (token.kind === "whitespace" ||
|
|
193
|
+
token.kind === "cdo" ||
|
|
194
|
+
token.kind === "cdc") {
|
|
195
|
+
stream.discard();
|
|
196
|
+
}
|
|
197
|
+
else if (token.kind === "at-keyword") {
|
|
198
|
+
rules.push(this.#consumeAtRule(stream, false, depth));
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
const start = stream.next.span;
|
|
202
|
+
const rule = this.#consumeQualifiedRule(stream, false, undefined, depth);
|
|
203
|
+
if (rule !== null) {
|
|
204
|
+
rules.push(rule);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
this.#diagnostic("invalid-rule", "The stylesheet contains an incomplete qualified rule.", start, "#consume-a-stylesheets-contents");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return freezeArray(rules);
|
|
212
|
+
}
|
|
213
|
+
#consumeAtRule(stream, nested, depth) {
|
|
214
|
+
const start = stream.index;
|
|
215
|
+
const nameToken = stream.consume();
|
|
216
|
+
if (nameToken.kind !== "at-keyword") {
|
|
217
|
+
throw new Error("at-rule consumption requires an at-keyword token");
|
|
218
|
+
}
|
|
219
|
+
const prelude = [];
|
|
220
|
+
for (;;) {
|
|
221
|
+
const token = stream.next;
|
|
222
|
+
if (token.kind === "semicolon") {
|
|
223
|
+
stream.discard();
|
|
224
|
+
return this.#atRule(nameToken.value, prelude, null, stream.spanFrom(start), depth);
|
|
225
|
+
}
|
|
226
|
+
if (token.kind === "eof" || (nested && token.kind === "close-curly")) {
|
|
227
|
+
return this.#atRule(nameToken.value, prelude, null, stream.spanFrom(start), depth);
|
|
228
|
+
}
|
|
229
|
+
if (token.kind === "open-curly") {
|
|
230
|
+
const block = this.#consumeBlock(stream, depth + 1);
|
|
231
|
+
return this.#atRule(nameToken.value, prelude, block, stream.spanFrom(start), depth);
|
|
232
|
+
}
|
|
233
|
+
prelude.push(this.#consumeComponentValue(stream, depth + 1));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
#consumeQualifiedRule(stream, nested, stopKind, depth) {
|
|
237
|
+
const start = stream.index;
|
|
238
|
+
const prelude = [];
|
|
239
|
+
for (;;) {
|
|
240
|
+
const token = stream.next;
|
|
241
|
+
if (token.kind === "eof" || token.kind === stopKind)
|
|
242
|
+
return null;
|
|
243
|
+
if (token.kind === "close-curly") {
|
|
244
|
+
if (nested)
|
|
245
|
+
return null;
|
|
246
|
+
this.#diagnostic("unexpected-closing-token", "A closing curly bracket has no matching block.", token.span, "#consume-a-qualified-rule");
|
|
247
|
+
}
|
|
248
|
+
if (token.kind === "open-curly") {
|
|
249
|
+
if (this.#looksLikeCustomPropertyRule(prelude)) {
|
|
250
|
+
this.#consumeBadDeclarationRemnants(stream, nested);
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
const block = this.#consumeBlock(stream, depth + 1);
|
|
254
|
+
return this.#qualifiedRule(prelude, block, stream.spanFrom(start), depth);
|
|
255
|
+
}
|
|
256
|
+
prelude.push(this.#consumeComponentValue(stream, depth + 1));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
#consumeBlock(stream, depth) {
|
|
260
|
+
const start = stream.index;
|
|
261
|
+
const opening = stream.consume();
|
|
262
|
+
if (opening.kind !== "open-curly") {
|
|
263
|
+
throw new Error("block consumption requires an opening curly token");
|
|
264
|
+
}
|
|
265
|
+
const items = this.#consumeBlockContents(stream, depth + 1);
|
|
266
|
+
if (this.#nextKind(stream) === "close-curly")
|
|
267
|
+
stream.discard();
|
|
268
|
+
this.#guard.createNode(depth);
|
|
269
|
+
return Object.freeze({
|
|
270
|
+
id: this.#takeNodeId(),
|
|
271
|
+
kind: "block",
|
|
272
|
+
items,
|
|
273
|
+
span: stream.spanFrom(start)
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
#consumeBlockContents(stream, depth) {
|
|
277
|
+
const items = [];
|
|
278
|
+
while (!stream.empty && stream.next.kind !== "close-curly") {
|
|
279
|
+
if (stream.next.kind === "whitespace" || stream.next.kind === "semicolon") {
|
|
280
|
+
stream.discard();
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (stream.next.kind === "at-keyword") {
|
|
284
|
+
items.push(this.#consumeAtRule(stream, true, depth));
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const start = stream.next.span;
|
|
288
|
+
const startsLikeDeclaration = stream.next.kind === "ident";
|
|
289
|
+
stream.mark();
|
|
290
|
+
const declaration = this.#consumeDeclaration(stream, true, depth);
|
|
291
|
+
if (declaration !== null) {
|
|
292
|
+
stream.discardMark();
|
|
293
|
+
items.push(declaration);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
stream.restore();
|
|
297
|
+
const rule = this.#consumeQualifiedRule(stream, true, "semicolon", depth);
|
|
298
|
+
if (rule !== null) {
|
|
299
|
+
items.push(rule);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
this.#diagnostic(startsLikeDeclaration ? "invalid-declaration" : "invalid-rule", startsLikeDeclaration
|
|
303
|
+
? "A declaration-like construct has no valid name, colon, or value."
|
|
304
|
+
: "Block content does not form a complete nested rule.", start, "#consume-a-blocks-contents");
|
|
305
|
+
if (this.#nextKind(stream) !== "semicolon" &&
|
|
306
|
+
this.#nextKind(stream) !== "close-curly") {
|
|
307
|
+
this.#consumeBadDeclarationRemnants(stream, true);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return freezeArray(items);
|
|
312
|
+
}
|
|
313
|
+
#consumeDeclaration(stream, nested, depth) {
|
|
314
|
+
const start = stream.index;
|
|
315
|
+
const nameToken = stream.next;
|
|
316
|
+
if (nameToken.kind !== "ident")
|
|
317
|
+
return null;
|
|
318
|
+
stream.discard();
|
|
319
|
+
stream.discardWhitespace();
|
|
320
|
+
if (stream.next.kind !== "colon")
|
|
321
|
+
return null;
|
|
322
|
+
stream.discard();
|
|
323
|
+
stream.discardWhitespace();
|
|
324
|
+
let value = [...this.#consumeComponentValues(stream, "semicolon", nested, depth + 1)];
|
|
325
|
+
const custom = nameToken.value.startsWith("--");
|
|
326
|
+
if (!custom && this.#hasAmbiguousCurlyBlock(value))
|
|
327
|
+
return null;
|
|
328
|
+
let important = false;
|
|
329
|
+
const nonWhitespace = value
|
|
330
|
+
.map((item, index) => ({ item, index }))
|
|
331
|
+
.filter(({ item }) => !isWhitespace(item));
|
|
332
|
+
const last = nonWhitespace.at(-1);
|
|
333
|
+
const previous = nonWhitespace.at(-2);
|
|
334
|
+
if (previous?.item.kind === "delim" &&
|
|
335
|
+
previous.item.value === 0x21 &&
|
|
336
|
+
last?.item.kind === "ident" &&
|
|
337
|
+
lowerAscii(last.item.value) === "important") {
|
|
338
|
+
value.splice(previous.index);
|
|
339
|
+
important = true;
|
|
340
|
+
}
|
|
341
|
+
while (value.at(-1)?.kind === "whitespace")
|
|
342
|
+
value.pop();
|
|
343
|
+
if (lowerAscii(nameToken.value) === "unicode-range") {
|
|
344
|
+
value = [...this.#parseUnicodeRangeValue(value, depth + 1)];
|
|
345
|
+
}
|
|
346
|
+
const span = stream.spanFrom(start);
|
|
347
|
+
const originalTextStart = value.at(0)?.span.start.offset;
|
|
348
|
+
const originalTextEnd = value.at(-1)?.span.end.offset;
|
|
349
|
+
this.#guard.createNode(depth);
|
|
350
|
+
return Object.freeze({
|
|
351
|
+
id: this.#takeNodeId(),
|
|
352
|
+
kind: "declaration",
|
|
353
|
+
name: nameToken.value,
|
|
354
|
+
value: freezeArray(value),
|
|
355
|
+
important,
|
|
356
|
+
...(custom
|
|
357
|
+
? {
|
|
358
|
+
originalText: originalTextStart === undefined || originalTextEnd === undefined
|
|
359
|
+
? ""
|
|
360
|
+
: this.#input.slice(originalTextStart, originalTextEnd)
|
|
361
|
+
}
|
|
362
|
+
: {}),
|
|
363
|
+
span
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
#consumeBadDeclarationRemnants(stream, nested) {
|
|
367
|
+
while (!stream.empty) {
|
|
368
|
+
if (stream.next.kind === "semicolon") {
|
|
369
|
+
stream.discard();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (stream.next.kind === "close-curly" && nested)
|
|
373
|
+
return;
|
|
374
|
+
this.#consumeComponentValue(stream, 1);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
#consumeComponentValues(stream, stopKind, nested, depth) {
|
|
378
|
+
const values = [];
|
|
379
|
+
while (!stream.empty && stream.next.kind !== stopKind) {
|
|
380
|
+
if (nested && stream.next.kind === "close-curly")
|
|
381
|
+
break;
|
|
382
|
+
values.push(this.#consumeComponentValue(stream, depth));
|
|
383
|
+
}
|
|
384
|
+
return freezeArray(values);
|
|
385
|
+
}
|
|
386
|
+
#consumeComponentValue(stream, depth) {
|
|
387
|
+
const token = stream.next;
|
|
388
|
+
if (token.kind === "open-square" ||
|
|
389
|
+
token.kind === "open-paren" ||
|
|
390
|
+
token.kind === "open-curly") {
|
|
391
|
+
return this.#consumeSimpleBlock(stream, depth);
|
|
392
|
+
}
|
|
393
|
+
if (token.kind === "function")
|
|
394
|
+
return this.#consumeFunction(stream, depth);
|
|
395
|
+
const consumed = stream.consume();
|
|
396
|
+
if (!isPreservedToken(consumed)) {
|
|
397
|
+
throw new Error("unexpected non-preserved CSS token");
|
|
398
|
+
}
|
|
399
|
+
if (isClosingToken(consumed)) {
|
|
400
|
+
this.#diagnostic("unexpected-closing-token", "A closing token has no matching block.", consumed.span, "#consume-a-list-of-component-values");
|
|
401
|
+
}
|
|
402
|
+
else if (consumed.kind === "bad-string") {
|
|
403
|
+
this.#diagnostic("bad-string-token", "A malformed string token was preserved for error recovery.", consumed.span, "#consume-a-string-token");
|
|
404
|
+
}
|
|
405
|
+
else if (consumed.kind === "bad-url") {
|
|
406
|
+
this.#diagnostic("bad-url-token", "A malformed URL token was preserved for error recovery.", consumed.span, "#consume-a-url-token");
|
|
407
|
+
}
|
|
408
|
+
return consumed;
|
|
409
|
+
}
|
|
410
|
+
#consumeSimpleBlock(stream, depth) {
|
|
411
|
+
const start = stream.index;
|
|
412
|
+
const opening = stream.consume();
|
|
413
|
+
if (opening.kind !== "open-square" &&
|
|
414
|
+
opening.kind !== "open-paren" &&
|
|
415
|
+
opening.kind !== "open-curly") {
|
|
416
|
+
throw new Error("simple-block consumption requires an opening token");
|
|
417
|
+
}
|
|
418
|
+
const endKind = closingKind(opening.kind);
|
|
419
|
+
const values = [];
|
|
420
|
+
while (!stream.empty && stream.next.kind !== endKind) {
|
|
421
|
+
values.push(this.#consumeComponentValue(stream, depth + 1));
|
|
422
|
+
}
|
|
423
|
+
if (stream.next.kind === endKind)
|
|
424
|
+
stream.discard();
|
|
425
|
+
this.#guard.createNode(depth);
|
|
426
|
+
return Object.freeze({
|
|
427
|
+
id: this.#takeNodeId(),
|
|
428
|
+
kind: "simple-block",
|
|
429
|
+
associatedToken: opening.kind,
|
|
430
|
+
value: freezeArray(values),
|
|
431
|
+
span: stream.spanFrom(start)
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
#consumeFunction(stream, depth) {
|
|
435
|
+
const start = stream.index;
|
|
436
|
+
const opening = stream.consume();
|
|
437
|
+
if (opening.kind !== "function") {
|
|
438
|
+
throw new Error("function consumption requires a function token");
|
|
439
|
+
}
|
|
440
|
+
const values = [];
|
|
441
|
+
while (!stream.empty && stream.next.kind !== "close-paren") {
|
|
442
|
+
values.push(this.#consumeComponentValue(stream, depth + 1));
|
|
443
|
+
}
|
|
444
|
+
if (stream.next.kind === "close-paren")
|
|
445
|
+
stream.discard();
|
|
446
|
+
this.#guard.createNode(depth);
|
|
447
|
+
return Object.freeze({
|
|
448
|
+
id: this.#takeNodeId(),
|
|
449
|
+
kind: "function-block",
|
|
450
|
+
name: opening.value,
|
|
451
|
+
value: freezeArray(values),
|
|
452
|
+
span: stream.spanFrom(start)
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
#parseUnicodeRangeValue(currentValue, depth) {
|
|
456
|
+
const first = currentValue.at(0);
|
|
457
|
+
const last = currentValue.at(-1);
|
|
458
|
+
if (first === undefined || last === undefined)
|
|
459
|
+
return currentValue;
|
|
460
|
+
const source = this.#input.slice(first.span.start.offset, last.span.end.offset);
|
|
461
|
+
const tokenizer = new CssTokenizer(source, {
|
|
462
|
+
guard: this.#guard,
|
|
463
|
+
unicodeRanges: true
|
|
464
|
+
});
|
|
465
|
+
const tokenized = tokenizer.tokenize();
|
|
466
|
+
const remappedTokens = tokenized.tokens.map((token) => remapToken(token, first.span.start));
|
|
467
|
+
if (tokenized.errors.length > 0) {
|
|
468
|
+
this.#tokenizerErrors = freezeArray([
|
|
469
|
+
...this.#tokenizerErrors,
|
|
470
|
+
...tokenized.errors.map((error) => Object.freeze({
|
|
471
|
+
...error,
|
|
472
|
+
span: remapSpan(error.span, first.span.start)
|
|
473
|
+
}))
|
|
474
|
+
]);
|
|
475
|
+
}
|
|
476
|
+
return this.#consumeComponentValues(new TokenStream(remappedTokens, remapPosition(tokenized.end, first.span.start)), undefined, false, depth);
|
|
477
|
+
}
|
|
478
|
+
#looksLikeCustomPropertyRule(prelude) {
|
|
479
|
+
const values = prelude.filter((value) => !isWhitespace(value));
|
|
480
|
+
return (values[0]?.kind === "ident" &&
|
|
481
|
+
values[0].value.startsWith("--") &&
|
|
482
|
+
values[1]?.kind === "colon");
|
|
483
|
+
}
|
|
484
|
+
#hasAmbiguousCurlyBlock(value) {
|
|
485
|
+
const significant = value.filter((item) => !isWhitespace(item));
|
|
486
|
+
const curlyBlocks = significant.filter((item) => item.kind === "simple-block" && item.associatedToken === "open-curly");
|
|
487
|
+
return curlyBlocks.length > 0 && significant.length !== 1;
|
|
488
|
+
}
|
|
489
|
+
#stylesheet(rules, span, depth) {
|
|
490
|
+
this.#guard.createNode(depth);
|
|
491
|
+
return Object.freeze({
|
|
492
|
+
id: this.#takeNodeId(),
|
|
493
|
+
kind: "stylesheet",
|
|
494
|
+
rules,
|
|
495
|
+
span
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
#atRule(name, prelude, block, span, depth) {
|
|
499
|
+
this.#guard.createNode(depth);
|
|
500
|
+
return Object.freeze({
|
|
501
|
+
id: this.#takeNodeId(),
|
|
502
|
+
kind: "at-rule",
|
|
503
|
+
name,
|
|
504
|
+
prelude: freezeArray(prelude),
|
|
505
|
+
block,
|
|
506
|
+
span
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
#qualifiedRule(prelude, block, span, depth) {
|
|
510
|
+
this.#guard.createNode(depth);
|
|
511
|
+
return Object.freeze({
|
|
512
|
+
id: this.#takeNodeId(),
|
|
513
|
+
kind: "qualified-rule",
|
|
514
|
+
prelude: freezeArray(prelude),
|
|
515
|
+
block,
|
|
516
|
+
span
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
#takeNodeId() {
|
|
520
|
+
const id = this.#nextNodeId;
|
|
521
|
+
this.#nextNodeId += 1;
|
|
522
|
+
return id;
|
|
523
|
+
}
|
|
524
|
+
#nextKind(stream) {
|
|
525
|
+
return stream.next.kind;
|
|
526
|
+
}
|
|
527
|
+
#diagnostic(code, message, span, anchor) {
|
|
528
|
+
this.#diagnostics.push(Object.freeze({
|
|
529
|
+
kind: "parser",
|
|
530
|
+
code,
|
|
531
|
+
message,
|
|
532
|
+
span,
|
|
533
|
+
specRef: `${CSS_SYNTAX}${anchor}`
|
|
534
|
+
}));
|
|
535
|
+
}
|
|
536
|
+
#errors() {
|
|
537
|
+
return freezeArray([...this.#tokenizerErrors, ...this.#diagnostics]);
|
|
538
|
+
}
|
|
539
|
+
#usage() {
|
|
540
|
+
return this.#guard.snapshot();
|
|
541
|
+
}
|
|
542
|
+
#success(value) {
|
|
543
|
+
return Object.freeze({
|
|
544
|
+
ok: true,
|
|
545
|
+
value,
|
|
546
|
+
errors: this.#errors(),
|
|
547
|
+
usage: this.#usage()
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
#failure() {
|
|
551
|
+
return Object.freeze({
|
|
552
|
+
ok: false,
|
|
553
|
+
errors: this.#errors(),
|
|
554
|
+
usage: this.#usage()
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
export function parseCssStylesheet(input, options = {}) {
|
|
559
|
+
return new CssSyntaxParser(input, options).parseStylesheet();
|
|
560
|
+
}
|
|
561
|
+
export function parseCssStylesheetContents(input, options = {}) {
|
|
562
|
+
return new CssSyntaxParser(input, options).parseStylesheetContents();
|
|
563
|
+
}
|
|
564
|
+
export function parseCssBlockContents(input, options = {}) {
|
|
565
|
+
return new CssSyntaxParser(input, options).parseBlockContents();
|
|
566
|
+
}
|
|
567
|
+
export function parseCssRule(input, options = {}) {
|
|
568
|
+
return new CssSyntaxParser(input, options).parseRule();
|
|
569
|
+
}
|
|
570
|
+
export function parseCssDeclaration(input, options = {}) {
|
|
571
|
+
return new CssSyntaxParser(input, options).parseDeclaration();
|
|
572
|
+
}
|
|
573
|
+
export function parseCssComponentValue(input, options = {}) {
|
|
574
|
+
return new CssSyntaxParser(input, options).parseComponentValue();
|
|
575
|
+
}
|
|
576
|
+
export function parseCssComponentValues(input, options = {}) {
|
|
577
|
+
return new CssSyntaxParser(input, options).parseComponentValues();
|
|
578
|
+
}
|
|
579
|
+
export function parseCssCommaSeparatedComponentValues(input, options = {}) {
|
|
580
|
+
return new CssSyntaxParser(input, options).parseCommaSeparatedComponentValues();
|
|
581
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ResourceLimitName, ResourceLimits, ResourceUsage } from "./types.js";
|
|
2
|
+
export declare class SyntaxResourceError extends Error {
|
|
3
|
+
readonly limitName: ResourceLimitName;
|
|
4
|
+
readonly limit: number;
|
|
5
|
+
readonly actual: number;
|
|
6
|
+
readonly code = "CSS_RESOURCE_LIMIT_EXCEEDED";
|
|
7
|
+
constructor(limitName: ResourceLimitName, limit: number, actual: number);
|
|
8
|
+
}
|
|
9
|
+
export declare class SyntaxAbortError extends Error {
|
|
10
|
+
readonly reason: unknown;
|
|
11
|
+
readonly code = "CSS_OPERATION_ABORTED";
|
|
12
|
+
constructor(reason: unknown);
|
|
13
|
+
}
|
|
14
|
+
export declare class ResourceGuard {
|
|
15
|
+
#private;
|
|
16
|
+
readonly signal?: AbortSignal | undefined;
|
|
17
|
+
readonly limits: ResourceLimits;
|
|
18
|
+
constructor(limits?: ResourceLimits, signal?: AbortSignal | undefined, initialUsage?: ResourceUsage);
|
|
19
|
+
assertActive(): void;
|
|
20
|
+
setInputBytes(value: number): void;
|
|
21
|
+
observeBufferedBytes(value: number): void;
|
|
22
|
+
emitToken(count?: number): void;
|
|
23
|
+
createNode(depth: number): void;
|
|
24
|
+
step(count?: number): void;
|
|
25
|
+
snapshot(): ResourceUsage;
|
|
26
|
+
assertIncrement(value: number): void;
|
|
27
|
+
assertObservedValue(value: number, name: string): void;
|
|
28
|
+
}
|