@ismail-elkorchi/css-parser 0.1.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/LICENSE +21 -0
- package/README.md +100 -0
- package/THIRD_PARTY_NOTICES.md +19 -0
- package/dist/internal/csstree-runtime.d.ts +20 -0
- package/dist/internal/csstree-runtime.js +21 -0
- package/dist/internal/encoding/mod.d.ts +1 -0
- package/dist/internal/encoding/mod.js +1 -0
- package/dist/internal/encoding/sniff.d.ts +14 -0
- package/dist/internal/encoding/sniff.js +95 -0
- package/dist/internal/serializer/mod.d.ts +1 -0
- package/dist/internal/serializer/mod.js +1 -0
- package/dist/internal/serializer/serialize.d.ts +3 -0
- package/dist/internal/serializer/serialize.js +89 -0
- package/dist/internal/tokenizer/mod.d.ts +2 -0
- package/dist/internal/tokenizer/mod.js +1 -0
- package/dist/internal/tokenizer/tokenize.d.ts +2 -0
- package/dist/internal/tokenizer/tokenize.js +39 -0
- package/dist/internal/tokenizer/tokens.d.ts +23 -0
- package/dist/internal/tokenizer/tokens.js +1 -0
- package/dist/internal/tree/build.d.ts +2 -0
- package/dist/internal/tree/build.js +85 -0
- package/dist/internal/tree/mod.d.ts +2 -0
- package/dist/internal/tree/mod.js +1 -0
- package/dist/internal/tree/types.d.ts +25 -0
- package/dist/internal/tree/types.js +1 -0
- package/dist/internal/vendor/csstree/LICENSE +19 -0
- package/dist/internal/vendor/csstree/csstree.esm.js +12 -0
- package/dist/internal/version.d.ts +1 -0
- package/dist/internal/version.js +1 -0
- package/dist/mod.d.ts +1 -0
- package/dist/mod.js +1 -0
- package/dist/public/index.d.ts +1 -0
- package/dist/public/index.js +1 -0
- package/dist/public/mod.d.ts +35 -0
- package/dist/public/mod.js +1740 -0
- package/dist/public/types.d.ts +279 -0
- package/dist/public/types.js +1 -0
- package/package.json +87 -0
|
@@ -0,0 +1,1740 @@
|
|
|
1
|
+
import { decodeCssBytes, sniffCssEncoding } from "../internal/encoding/mod.js";
|
|
2
|
+
import { sanitizeCssNode, serializeTreeNode } from "../internal/serializer/mod.js";
|
|
3
|
+
import { tokenize as tokenizeInternal } from "../internal/tokenizer/mod.js";
|
|
4
|
+
import { buildTreeFromCss } from "../internal/tree/mod.js";
|
|
5
|
+
const STREAM_ENCODING_PRESCAN_BYTES = 1024;
|
|
6
|
+
const CSS_PARSE_ERRORS_SECTION_URL = "https://drafts.csswg.org/css-syntax/#error-handling";
|
|
7
|
+
const SUPPORTED_PARSE_CONTEXTS = new Set([
|
|
8
|
+
"stylesheet",
|
|
9
|
+
"atrule",
|
|
10
|
+
"atrulePrelude",
|
|
11
|
+
"mediaQueryList",
|
|
12
|
+
"mediaQuery",
|
|
13
|
+
"condition",
|
|
14
|
+
"rule",
|
|
15
|
+
"selectorList",
|
|
16
|
+
"selector",
|
|
17
|
+
"block",
|
|
18
|
+
"declarationList",
|
|
19
|
+
"declaration",
|
|
20
|
+
"value"
|
|
21
|
+
]);
|
|
22
|
+
export class BudgetExceededError extends Error {
|
|
23
|
+
payload;
|
|
24
|
+
constructor(payload) {
|
|
25
|
+
super(`Budget exceeded: ${payload.budget} limit=${String(payload.limit)} actual=${String(payload.actual)}`);
|
|
26
|
+
this.name = "BudgetExceededError";
|
|
27
|
+
this.payload = payload;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class PatchPlanningError extends Error {
|
|
31
|
+
payload;
|
|
32
|
+
constructor(payload) {
|
|
33
|
+
super(`Patch planning failed: ${payload.code}${payload.target === undefined ? "" : ` target=${String(payload.target)}`}`);
|
|
34
|
+
this.name = "PatchPlanningError";
|
|
35
|
+
this.payload = payload;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function isRecord(value) {
|
|
39
|
+
return value !== null && typeof value === "object";
|
|
40
|
+
}
|
|
41
|
+
function isPublicNode(value) {
|
|
42
|
+
return isRecord(value) && typeof value["id"] === "number" && typeof value["type"] === "string";
|
|
43
|
+
}
|
|
44
|
+
function normalizeParseErrorId(message) {
|
|
45
|
+
const normalized = message.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
46
|
+
return normalized.length > 0 ? normalized : "css-syntax-error";
|
|
47
|
+
}
|
|
48
|
+
function normalizeFragmentContext(contextTagName) {
|
|
49
|
+
const normalized = contextTagName.trim().toLowerCase();
|
|
50
|
+
switch (normalized) {
|
|
51
|
+
case "stylesheet":
|
|
52
|
+
return "stylesheet";
|
|
53
|
+
case "atrule":
|
|
54
|
+
case "at-rule":
|
|
55
|
+
return "atrule";
|
|
56
|
+
case "atruleprelude":
|
|
57
|
+
case "at-rule-prelude":
|
|
58
|
+
return "atrulePrelude";
|
|
59
|
+
case "mediaquerylist":
|
|
60
|
+
case "media-query-list":
|
|
61
|
+
return "mediaQueryList";
|
|
62
|
+
case "mediaquery":
|
|
63
|
+
case "media-query":
|
|
64
|
+
return "mediaQuery";
|
|
65
|
+
case "condition":
|
|
66
|
+
return "condition";
|
|
67
|
+
case "rule":
|
|
68
|
+
case "rulelist":
|
|
69
|
+
case "rule-list":
|
|
70
|
+
return "rule";
|
|
71
|
+
case "selectorlist":
|
|
72
|
+
case "selector-list":
|
|
73
|
+
return "selectorList";
|
|
74
|
+
case "selector":
|
|
75
|
+
return "selector";
|
|
76
|
+
case "block":
|
|
77
|
+
return "block";
|
|
78
|
+
case "declarationlist":
|
|
79
|
+
case "declaration-list":
|
|
80
|
+
return "declarationList";
|
|
81
|
+
case "declaration":
|
|
82
|
+
return "declaration";
|
|
83
|
+
case "value":
|
|
84
|
+
return "value";
|
|
85
|
+
default:
|
|
86
|
+
throw new Error(`Unsupported parse context: ${contextTagName}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function enforceBudget(budget, limit, actual) {
|
|
90
|
+
if (limit === undefined || actual <= limit) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
throw new BudgetExceededError({
|
|
94
|
+
code: "BUDGET_EXCEEDED",
|
|
95
|
+
budget,
|
|
96
|
+
limit,
|
|
97
|
+
actual
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function eventSize(event) {
|
|
101
|
+
return JSON.stringify(event).length;
|
|
102
|
+
}
|
|
103
|
+
function pushTrace(trace, event, budgets) {
|
|
104
|
+
if (!trace) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
const nextEvent = {
|
|
108
|
+
seq: trace.length + 1,
|
|
109
|
+
...event
|
|
110
|
+
};
|
|
111
|
+
const next = [...trace, nextEvent];
|
|
112
|
+
enforceBudget("maxTraceEvents", budgets?.maxTraceEvents, next.length);
|
|
113
|
+
const bytes = next.reduce((total, item) => total + eventSize(item), 0);
|
|
114
|
+
enforceBudget("maxTraceBytes", budgets?.maxTraceBytes, bytes);
|
|
115
|
+
return next;
|
|
116
|
+
}
|
|
117
|
+
function pushBudgetTrace(trace, budget, limit, actual, budgets) {
|
|
118
|
+
return pushTrace(trace, {
|
|
119
|
+
kind: "budget",
|
|
120
|
+
budget,
|
|
121
|
+
limit: limit ?? null,
|
|
122
|
+
actual,
|
|
123
|
+
status: limit === undefined || actual <= limit ? "ok" : "exceeded"
|
|
124
|
+
}, budgets);
|
|
125
|
+
}
|
|
126
|
+
function toPublicSpanFromLoc(loc, captureSpans) {
|
|
127
|
+
if (!captureSpans || !isRecord(loc)) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const start = loc["start"];
|
|
131
|
+
const end = loc["end"];
|
|
132
|
+
if (!isRecord(start) || !isRecord(end)) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const startOffset = start["offset"];
|
|
136
|
+
const endOffset = end["offset"];
|
|
137
|
+
if (typeof startOffset !== "number" ||
|
|
138
|
+
typeof endOffset !== "number" ||
|
|
139
|
+
startOffset < 0 ||
|
|
140
|
+
endOffset < startOffset) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
start: startOffset,
|
|
145
|
+
end: endOffset
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function annotateAnyWithSpans(value, nodeIdState) {
|
|
149
|
+
if (value === null || typeof value !== "object") {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (Array.isArray(value)) {
|
|
153
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
154
|
+
annotateAnyWithSpans(value[index], nodeIdState);
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const mutable = value;
|
|
159
|
+
const isTypedNode = typeof mutable["type"] === "string";
|
|
160
|
+
for (const key in mutable) {
|
|
161
|
+
if (isTypedNode && (key === "loc" || key === "type")) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const entry = mutable[key];
|
|
165
|
+
if (entry === null || typeof entry !== "object") {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (Array.isArray(entry)) {
|
|
169
|
+
const children = entry;
|
|
170
|
+
for (let index = 0; index < children.length; index += 1) {
|
|
171
|
+
const child = children[index];
|
|
172
|
+
if (child !== null && typeof child === "object") {
|
|
173
|
+
annotateAnyWithSpans(child, nodeIdState);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
annotateAnyWithSpans(entry, nodeIdState);
|
|
179
|
+
}
|
|
180
|
+
if (!isTypedNode) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
mutable["id"] = nodeIdState.next;
|
|
184
|
+
nodeIdState.next += 1;
|
|
185
|
+
const span = toPublicSpanFromLoc(mutable["loc"], true);
|
|
186
|
+
mutable["spanProvenance"] = span ? "input" : "none";
|
|
187
|
+
if (span) {
|
|
188
|
+
mutable["span"] = span;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function annotateAnyNoSpans(value, nodeIdState) {
|
|
192
|
+
if (value === null || typeof value !== "object") {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (Array.isArray(value)) {
|
|
196
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
197
|
+
annotateAnyNoSpans(value[index], nodeIdState);
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const mutable = value;
|
|
202
|
+
const isTypedNode = typeof mutable["type"] === "string";
|
|
203
|
+
for (const key in mutable) {
|
|
204
|
+
if (isTypedNode && (key === "loc" || key === "type")) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const entry = mutable[key];
|
|
208
|
+
if (entry === null || typeof entry !== "object") {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(entry)) {
|
|
212
|
+
const children = entry;
|
|
213
|
+
for (let index = 0; index < children.length; index += 1) {
|
|
214
|
+
const child = children[index];
|
|
215
|
+
if (child !== null && typeof child === "object") {
|
|
216
|
+
annotateAnyNoSpans(child, nodeIdState);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
annotateAnyNoSpans(entry, nodeIdState);
|
|
222
|
+
}
|
|
223
|
+
if (!isTypedNode) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
mutable["id"] = nodeIdState.next;
|
|
227
|
+
nodeIdState.next += 1;
|
|
228
|
+
}
|
|
229
|
+
function annotateNode(rawNode, nodeIdState, captureSpans) {
|
|
230
|
+
if (captureSpans) {
|
|
231
|
+
annotateAnyWithSpans(rawNode, nodeIdState);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
annotateAnyNoSpans(rawNode, nodeIdState);
|
|
235
|
+
}
|
|
236
|
+
return rawNode;
|
|
237
|
+
}
|
|
238
|
+
function collectChildNodes(value, into) {
|
|
239
|
+
if (isPublicNode(value)) {
|
|
240
|
+
into.push(value);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (Array.isArray(value)) {
|
|
244
|
+
for (const entry of value) {
|
|
245
|
+
collectChildNodes(entry, into);
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (!isRecord(value)) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
for (const entry of Object.values(value)) {
|
|
253
|
+
collectChildNodes(entry, into);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function childNodes(node) {
|
|
257
|
+
const result = [];
|
|
258
|
+
for (const [key, value] of Object.entries(node)) {
|
|
259
|
+
if (key === "id" || key === "type" || key === "span" || key === "spanProvenance") {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
collectChildNodes(value, result);
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
function collectMetrics(root, depth) {
|
|
267
|
+
let nodes = 1;
|
|
268
|
+
let maxDepth = depth;
|
|
269
|
+
for (const child of childNodes(root)) {
|
|
270
|
+
const childMetrics = collectMetrics(child, depth + 1);
|
|
271
|
+
nodes += childMetrics.nodes;
|
|
272
|
+
if (childMetrics.maxDepth > maxDepth) {
|
|
273
|
+
maxDepth = childMetrics.maxDepth;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return { nodes, maxDepth };
|
|
277
|
+
}
|
|
278
|
+
function toParseErrors(errors) {
|
|
279
|
+
return errors.map((error) => {
|
|
280
|
+
const offset = typeof error.offset === "number" && error.offset >= 0 ? error.offset : null;
|
|
281
|
+
const parseErrorId = normalizeParseErrorId(error.message);
|
|
282
|
+
return {
|
|
283
|
+
code: "PARSER_ERROR",
|
|
284
|
+
parseErrorId,
|
|
285
|
+
message: error.message,
|
|
286
|
+
...(offset !== null
|
|
287
|
+
? {
|
|
288
|
+
span: {
|
|
289
|
+
start: offset,
|
|
290
|
+
end: offset + 1
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
: {}),
|
|
294
|
+
...(typeof error.line === "number" ? { line: error.line } : {}),
|
|
295
|
+
...(typeof error.column === "number" ? { column: error.column } : {})
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
function tokenizerBudgetsFromParseOptions(budgets) {
|
|
300
|
+
if (!budgets) {
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
const next = {
|
|
304
|
+
...(budgets.maxTokens !== undefined ? { maxTokens: budgets.maxTokens } : {}),
|
|
305
|
+
...(budgets.maxTimeMs !== undefined ? { maxTimeMs: budgets.maxTimeMs } : {})
|
|
306
|
+
};
|
|
307
|
+
return Object.keys(next).length > 0 ? next : undefined;
|
|
308
|
+
}
|
|
309
|
+
function toPublicToken(token) {
|
|
310
|
+
return {
|
|
311
|
+
kind: token.kind,
|
|
312
|
+
rawKind: token.rawKind,
|
|
313
|
+
value: token.value,
|
|
314
|
+
start: token.start,
|
|
315
|
+
end: token.end
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function topLevelChildren(root) {
|
|
319
|
+
const maybeChildren = root["children"];
|
|
320
|
+
if (!Array.isArray(maybeChildren)) {
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
return maybeChildren;
|
|
324
|
+
}
|
|
325
|
+
function isStyleSheetTree(value) {
|
|
326
|
+
return (isRecord(value) &&
|
|
327
|
+
value["kind"] === "stylesheet" &&
|
|
328
|
+
isPublicNode(value["root"]));
|
|
329
|
+
}
|
|
330
|
+
function isFragmentTree(value) {
|
|
331
|
+
return (isRecord(value) &&
|
|
332
|
+
value["kind"] === "fragment" &&
|
|
333
|
+
isPublicNode(value["root"]));
|
|
334
|
+
}
|
|
335
|
+
function parseInternal(css, context, options = {}) {
|
|
336
|
+
const budgets = options.budgets;
|
|
337
|
+
const maxTimeMs = budgets?.maxTimeMs;
|
|
338
|
+
const hasTimeBudget = maxTimeMs !== undefined;
|
|
339
|
+
const startedAt = hasTimeBudget ? Date.now() : 0;
|
|
340
|
+
const captureSpans = options.captureSpans ?? options.includeSpans ?? false;
|
|
341
|
+
let trace = options.trace ? [] : undefined;
|
|
342
|
+
const needsTokenization = options.trace === true ||
|
|
343
|
+
budgets?.maxTokens !== undefined ||
|
|
344
|
+
budgets?.maxTimeMs !== undefined;
|
|
345
|
+
const needsMetrics = options.trace === true ||
|
|
346
|
+
budgets?.maxNodes !== undefined ||
|
|
347
|
+
budgets?.maxDepth !== undefined;
|
|
348
|
+
enforceBudget("maxInputBytes", budgets?.maxInputBytes, css.length);
|
|
349
|
+
if (trace) {
|
|
350
|
+
trace =
|
|
351
|
+
pushTrace(trace, {
|
|
352
|
+
kind: "decode",
|
|
353
|
+
source: "input",
|
|
354
|
+
encoding: "utf-8",
|
|
355
|
+
sniffSource: "input"
|
|
356
|
+
}, budgets) ?? trace;
|
|
357
|
+
trace =
|
|
358
|
+
pushBudgetTrace(trace, "maxInputBytes", budgets?.maxInputBytes, css.length, budgets) ?? trace;
|
|
359
|
+
}
|
|
360
|
+
let tokenCount = null;
|
|
361
|
+
if (needsTokenization) {
|
|
362
|
+
const tokenizerBudgets = tokenizerBudgetsFromParseOptions(budgets);
|
|
363
|
+
const tokenized = tokenizerBudgets
|
|
364
|
+
? tokenizeInternal(css, { budgets: tokenizerBudgets })
|
|
365
|
+
: tokenizeInternal(css);
|
|
366
|
+
tokenCount = tokenized.tokens.length;
|
|
367
|
+
enforceBudget("maxTokens", budgets?.maxTokens, tokenCount);
|
|
368
|
+
trace = pushTrace(trace, {
|
|
369
|
+
kind: "token",
|
|
370
|
+
count: tokenCount
|
|
371
|
+
}, budgets);
|
|
372
|
+
}
|
|
373
|
+
const built = buildTreeFromCss(css, {
|
|
374
|
+
context,
|
|
375
|
+
captureSpans
|
|
376
|
+
});
|
|
377
|
+
const nodeIdState = { next: 1 };
|
|
378
|
+
const treeId = nodeIdState.next;
|
|
379
|
+
nodeIdState.next += 1;
|
|
380
|
+
const root = annotateNode(built.root, nodeIdState, captureSpans);
|
|
381
|
+
const children = topLevelChildren(root);
|
|
382
|
+
let metrics = null;
|
|
383
|
+
if (needsMetrics) {
|
|
384
|
+
metrics = collectMetrics(root, 1);
|
|
385
|
+
enforceBudget("maxNodes", budgets?.maxNodes, metrics.nodes);
|
|
386
|
+
enforceBudget("maxDepth", budgets?.maxDepth, metrics.maxDepth);
|
|
387
|
+
}
|
|
388
|
+
if (hasTimeBudget) {
|
|
389
|
+
enforceBudget("maxTimeMs", maxTimeMs, Date.now() - startedAt);
|
|
390
|
+
}
|
|
391
|
+
const publicErrors = toParseErrors(built.errors);
|
|
392
|
+
if (trace) {
|
|
393
|
+
trace = pushTrace(trace, {
|
|
394
|
+
kind: "parse",
|
|
395
|
+
context,
|
|
396
|
+
nodeCount: metrics?.nodes ?? 0,
|
|
397
|
+
errorCount: built.errors.length
|
|
398
|
+
}, budgets);
|
|
399
|
+
for (const parseError of publicErrors) {
|
|
400
|
+
trace = pushTrace(trace, {
|
|
401
|
+
kind: "parseError",
|
|
402
|
+
parseErrorId: parseError.parseErrorId,
|
|
403
|
+
startOffset: parseError.span?.start ?? null,
|
|
404
|
+
endOffset: parseError.span?.end ?? null
|
|
405
|
+
}, budgets);
|
|
406
|
+
}
|
|
407
|
+
if (tokenCount !== null) {
|
|
408
|
+
trace = pushBudgetTrace(trace, "maxTokens", budgets?.maxTokens, tokenCount, budgets);
|
|
409
|
+
}
|
|
410
|
+
if (metrics) {
|
|
411
|
+
trace = pushBudgetTrace(trace, "maxNodes", budgets?.maxNodes, metrics.nodes, budgets);
|
|
412
|
+
trace = pushBudgetTrace(trace, "maxDepth", budgets?.maxDepth, metrics.maxDepth, budgets);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
root: Object.assign(root, { id: treeId }),
|
|
417
|
+
children,
|
|
418
|
+
errors: publicErrors,
|
|
419
|
+
...(trace ? { trace } : {})
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
export function parse(css, options = {}) {
|
|
423
|
+
if (options.context !== undefined && options.context !== "stylesheet") {
|
|
424
|
+
throw new Error("parse() only supports context \"stylesheet\"; use parseFragment() for other contexts");
|
|
425
|
+
}
|
|
426
|
+
const parsed = parseInternal(css, "stylesheet", options);
|
|
427
|
+
return {
|
|
428
|
+
id: parsed.root.id,
|
|
429
|
+
kind: "stylesheet",
|
|
430
|
+
context: "stylesheet",
|
|
431
|
+
root: parsed.root,
|
|
432
|
+
children: parsed.children,
|
|
433
|
+
errors: parsed.errors,
|
|
434
|
+
...(parsed.trace ? { trace: parsed.trace } : {})
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
export function parseFragment(css, contextTagName, options = {}) {
|
|
438
|
+
const context = normalizeFragmentContext(contextTagName);
|
|
439
|
+
if (!SUPPORTED_PARSE_CONTEXTS.has(context)) {
|
|
440
|
+
throw new Error(`Unsupported parse context: ${contextTagName}`);
|
|
441
|
+
}
|
|
442
|
+
const parsed = parseInternal(css, context, options);
|
|
443
|
+
return {
|
|
444
|
+
id: parsed.root.id,
|
|
445
|
+
kind: "fragment",
|
|
446
|
+
context,
|
|
447
|
+
root: parsed.root,
|
|
448
|
+
children: parsed.children,
|
|
449
|
+
errors: parsed.errors,
|
|
450
|
+
...(parsed.trace ? { trace: parsed.trace } : {})
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
export function parseRuleList(css, options = {}) {
|
|
454
|
+
return parseFragment(css, "rule", options);
|
|
455
|
+
}
|
|
456
|
+
export function parseDeclarationList(css, options = {}) {
|
|
457
|
+
return parseFragment(css, "declarationList", options);
|
|
458
|
+
}
|
|
459
|
+
export function getParseErrorSpecRef(parseErrorId) {
|
|
460
|
+
void parseErrorId;
|
|
461
|
+
return CSS_PARSE_ERRORS_SECTION_URL;
|
|
462
|
+
}
|
|
463
|
+
function decodeStreamError(error) {
|
|
464
|
+
if (error instanceof Error) {
|
|
465
|
+
return new Error(`STREAM_READ_FAILED: ${error.message}`);
|
|
466
|
+
}
|
|
467
|
+
return new Error(`STREAM_READ_FAILED: ${String(error)}`);
|
|
468
|
+
}
|
|
469
|
+
async function decodeStreamToText(stream, options) {
|
|
470
|
+
const startedAt = Date.now();
|
|
471
|
+
const budgets = options.budgets;
|
|
472
|
+
const reader = stream.getReader();
|
|
473
|
+
let total = 0;
|
|
474
|
+
const pendingChunks = [];
|
|
475
|
+
let pendingBytes = 0;
|
|
476
|
+
let maxBufferedObserved = 0;
|
|
477
|
+
let sniff = null;
|
|
478
|
+
let decoder;
|
|
479
|
+
const decodedParts = [];
|
|
480
|
+
const readPendingBytes = () => {
|
|
481
|
+
if (pendingBytes === 0) {
|
|
482
|
+
return new Uint8Array(0);
|
|
483
|
+
}
|
|
484
|
+
const combined = new Uint8Array(pendingBytes);
|
|
485
|
+
let offset = 0;
|
|
486
|
+
for (const chunk of pendingChunks) {
|
|
487
|
+
combined.set(chunk, offset);
|
|
488
|
+
offset += chunk.byteLength;
|
|
489
|
+
}
|
|
490
|
+
return combined;
|
|
491
|
+
};
|
|
492
|
+
try {
|
|
493
|
+
for (;;) {
|
|
494
|
+
const next = await reader.read();
|
|
495
|
+
if (next.done) {
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
const chunkValue = next.value;
|
|
499
|
+
total += chunkValue.byteLength;
|
|
500
|
+
enforceBudget("maxInputBytes", budgets?.maxInputBytes, total);
|
|
501
|
+
enforceBudget("maxTimeMs", budgets?.maxTimeMs, Date.now() - startedAt);
|
|
502
|
+
if (!sniff) {
|
|
503
|
+
pendingChunks.push(chunkValue);
|
|
504
|
+
pendingBytes += chunkValue.byteLength;
|
|
505
|
+
maxBufferedObserved = Math.max(maxBufferedObserved, pendingBytes);
|
|
506
|
+
enforceBudget("maxBufferedBytes", budgets?.maxBufferedBytes, pendingBytes);
|
|
507
|
+
const shouldSniffNow = options.transportEncodingLabel !== undefined || pendingBytes >= STREAM_ENCODING_PRESCAN_BYTES;
|
|
508
|
+
if (!shouldSniffNow) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const bufferedBytes = readPendingBytes();
|
|
512
|
+
sniff = sniffCssEncoding(bufferedBytes, {
|
|
513
|
+
...(options.transportEncodingLabel !== undefined
|
|
514
|
+
? { transportEncodingLabel: options.transportEncodingLabel }
|
|
515
|
+
: {}),
|
|
516
|
+
maxPrescanBytes: STREAM_ENCODING_PRESCAN_BYTES
|
|
517
|
+
});
|
|
518
|
+
decoder = new TextDecoder(sniff.encoding);
|
|
519
|
+
const decoded = decoder.decode(bufferedBytes, { stream: true });
|
|
520
|
+
if (decoded.length > 0) {
|
|
521
|
+
decodedParts.push(decoded);
|
|
522
|
+
}
|
|
523
|
+
pendingChunks.length = 0;
|
|
524
|
+
pendingBytes = 0;
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
maxBufferedObserved = Math.max(maxBufferedObserved, chunkValue.byteLength);
|
|
528
|
+
enforceBudget("maxBufferedBytes", budgets?.maxBufferedBytes, chunkValue.byteLength);
|
|
529
|
+
if (!decoder) {
|
|
530
|
+
throw new Error("stream decoder unavailable");
|
|
531
|
+
}
|
|
532
|
+
const decoded = decoder.decode(chunkValue, { stream: true });
|
|
533
|
+
if (decoded.length > 0) {
|
|
534
|
+
decodedParts.push(decoded);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
if (error instanceof BudgetExceededError) {
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
throw decodeStreamError(error);
|
|
543
|
+
}
|
|
544
|
+
if (!sniff) {
|
|
545
|
+
const bufferedBytes = readPendingBytes();
|
|
546
|
+
sniff = sniffCssEncoding(bufferedBytes, {
|
|
547
|
+
...(options.transportEncodingLabel !== undefined
|
|
548
|
+
? { transportEncodingLabel: options.transportEncodingLabel }
|
|
549
|
+
: {}),
|
|
550
|
+
maxPrescanBytes: STREAM_ENCODING_PRESCAN_BYTES
|
|
551
|
+
});
|
|
552
|
+
decoder = new TextDecoder(sniff.encoding);
|
|
553
|
+
const decoded = decoder.decode(bufferedBytes, { stream: true });
|
|
554
|
+
if (decoded.length > 0) {
|
|
555
|
+
decodedParts.push(decoded);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (!decoder) {
|
|
559
|
+
throw new Error("stream decoder initialization failed");
|
|
560
|
+
}
|
|
561
|
+
const decodedTail = decoder.decode();
|
|
562
|
+
if (decodedTail.length > 0) {
|
|
563
|
+
decodedParts.push(decodedTail);
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
text: decodedParts.join(""),
|
|
567
|
+
sniff,
|
|
568
|
+
totalBytes: total,
|
|
569
|
+
maxBufferedObserved
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
export function tokenize(css, options = {}) {
|
|
573
|
+
enforceBudget("maxInputBytes", options.budgets?.maxInputBytes, css.length);
|
|
574
|
+
const tokenized = tokenizeInternal(css, {
|
|
575
|
+
budgets: {
|
|
576
|
+
...(options.budgets?.maxTokens !== undefined ? { maxTokens: options.budgets.maxTokens } : {}),
|
|
577
|
+
...(options.budgets?.maxTimeMs !== undefined ? { maxTimeMs: options.budgets.maxTimeMs } : {})
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
enforceBudget("maxTokens", options.budgets?.maxTokens, tokenized.tokens.length);
|
|
581
|
+
return tokenized.tokens.map((token) => toPublicToken(token));
|
|
582
|
+
}
|
|
583
|
+
export async function* tokenizeStream(stream, options = {}) {
|
|
584
|
+
const decoded = await decodeStreamToText(stream, options);
|
|
585
|
+
const tokens = tokenize(decoded.text, options);
|
|
586
|
+
for (const token of tokens) {
|
|
587
|
+
yield token;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
export function parseBytes(bytes, options = {}) {
|
|
591
|
+
enforceBudget("maxInputBytes", options.budgets?.maxInputBytes, bytes.byteLength);
|
|
592
|
+
const decoded = decodeCssBytes(bytes, {
|
|
593
|
+
...(options.transportEncodingLabel ? { transportEncodingLabel: options.transportEncodingLabel } : {})
|
|
594
|
+
});
|
|
595
|
+
const parsed = parse(decoded.text, options);
|
|
596
|
+
if (!parsed.trace) {
|
|
597
|
+
return parsed;
|
|
598
|
+
}
|
|
599
|
+
const withDecodeTrace = pushTrace([...parsed.trace], {
|
|
600
|
+
kind: "decode",
|
|
601
|
+
source: "sniff",
|
|
602
|
+
encoding: decoded.sniff.encoding,
|
|
603
|
+
sniffSource: decoded.sniff.source
|
|
604
|
+
}, options.budgets);
|
|
605
|
+
if (!withDecodeTrace) {
|
|
606
|
+
return parsed;
|
|
607
|
+
}
|
|
608
|
+
return {
|
|
609
|
+
...parsed,
|
|
610
|
+
trace: withDecodeTrace
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
export async function parseStream(stream, options = {}) {
|
|
614
|
+
const budgets = options.budgets;
|
|
615
|
+
const decoded = await decodeStreamToText(stream, options);
|
|
616
|
+
const parsed = parse(decoded.text, options);
|
|
617
|
+
if (!parsed.trace) {
|
|
618
|
+
return parsed;
|
|
619
|
+
}
|
|
620
|
+
let trace = [...parsed.trace];
|
|
621
|
+
trace =
|
|
622
|
+
pushTrace(trace, {
|
|
623
|
+
kind: "decode",
|
|
624
|
+
source: "sniff",
|
|
625
|
+
encoding: decoded.sniff.encoding,
|
|
626
|
+
sniffSource: decoded.sniff.source
|
|
627
|
+
}, budgets) ?? trace;
|
|
628
|
+
trace =
|
|
629
|
+
pushTrace(trace, {
|
|
630
|
+
kind: "stream",
|
|
631
|
+
bytesRead: decoded.totalBytes
|
|
632
|
+
}, budgets) ?? trace;
|
|
633
|
+
trace =
|
|
634
|
+
pushBudgetTrace(trace, "maxBufferedBytes", budgets?.maxBufferedBytes, decoded.maxBufferedObserved, budgets) ?? trace;
|
|
635
|
+
return {
|
|
636
|
+
...parsed,
|
|
637
|
+
trace
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function nodeFromTree(treeOrNode) {
|
|
641
|
+
if (isStyleSheetTree(treeOrNode)) {
|
|
642
|
+
return treeOrNode.root;
|
|
643
|
+
}
|
|
644
|
+
if (isFragmentTree(treeOrNode)) {
|
|
645
|
+
return treeOrNode.root;
|
|
646
|
+
}
|
|
647
|
+
return treeOrNode;
|
|
648
|
+
}
|
|
649
|
+
export function serialize(treeOrNode) {
|
|
650
|
+
const node = nodeFromTree(treeOrNode);
|
|
651
|
+
const sanitized = sanitizeCssNode(node);
|
|
652
|
+
return serializeTreeNode(sanitized);
|
|
653
|
+
}
|
|
654
|
+
function walkNode(node, depth, visitor) {
|
|
655
|
+
visitor(node, depth);
|
|
656
|
+
for (const child of childNodes(node)) {
|
|
657
|
+
walkNode(child, depth + 1, visitor);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
export function walk(tree, visitor) {
|
|
661
|
+
walkNode(tree.root, 0, visitor);
|
|
662
|
+
}
|
|
663
|
+
export function walkByType(tree, type, visitor) {
|
|
664
|
+
const normalizedType = type.toLowerCase();
|
|
665
|
+
walk(tree, (node, depth) => {
|
|
666
|
+
if (node.type.toLowerCase() === normalizedType) {
|
|
667
|
+
visitor(node, depth);
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
export function findById(tree, id) {
|
|
672
|
+
let matched = null;
|
|
673
|
+
walk(tree, (node) => {
|
|
674
|
+
if (matched === null && node.id === id) {
|
|
675
|
+
matched = node;
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
return matched;
|
|
679
|
+
}
|
|
680
|
+
export function* findAllByType(tree, type) {
|
|
681
|
+
const normalizedType = type.toLowerCase();
|
|
682
|
+
const found = [];
|
|
683
|
+
walk(tree, (node) => {
|
|
684
|
+
if (node.type.toLowerCase() === normalizedType) {
|
|
685
|
+
found.push(node);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
for (const node of found) {
|
|
689
|
+
yield node;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
export function outline(tree) {
|
|
693
|
+
const entries = [];
|
|
694
|
+
walk(tree, (node, depth) => {
|
|
695
|
+
if (node.type === "Rule" ||
|
|
696
|
+
node.type === "Atrule" ||
|
|
697
|
+
node.type === "Declaration" ||
|
|
698
|
+
node.type === "Selector") {
|
|
699
|
+
entries.push({
|
|
700
|
+
nodeId: node.id,
|
|
701
|
+
depth,
|
|
702
|
+
type: node.type,
|
|
703
|
+
text: serialize(node).slice(0, 200)
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
return {
|
|
708
|
+
entries
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
function countNodes(node) {
|
|
712
|
+
let total = 1;
|
|
713
|
+
for (const child of childNodes(node)) {
|
|
714
|
+
total += countNodes(child);
|
|
715
|
+
}
|
|
716
|
+
return total;
|
|
717
|
+
}
|
|
718
|
+
function topLevelNodes(tree) {
|
|
719
|
+
return tree.children.length > 0 ? tree.children : [tree.root];
|
|
720
|
+
}
|
|
721
|
+
export function chunk(tree, options = {}) {
|
|
722
|
+
const maxChars = options.maxChars ?? 8192;
|
|
723
|
+
const maxNodes = options.maxNodes ?? 256;
|
|
724
|
+
const maxBytes = options.maxBytes ?? Number.POSITIVE_INFINITY;
|
|
725
|
+
const encoder = new TextEncoder();
|
|
726
|
+
const chunks = [];
|
|
727
|
+
let activeContent = "";
|
|
728
|
+
let activeNodes = 0;
|
|
729
|
+
let activeBytes = 0;
|
|
730
|
+
let activeNodeId = null;
|
|
731
|
+
let index = 0;
|
|
732
|
+
const flush = () => {
|
|
733
|
+
if (activeNodeId === null) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
chunks.push({
|
|
737
|
+
index,
|
|
738
|
+
nodeId: activeNodeId,
|
|
739
|
+
content: activeContent,
|
|
740
|
+
nodes: activeNodes
|
|
741
|
+
});
|
|
742
|
+
index += 1;
|
|
743
|
+
activeContent = "";
|
|
744
|
+
activeNodes = 0;
|
|
745
|
+
activeBytes = 0;
|
|
746
|
+
activeNodeId = null;
|
|
747
|
+
};
|
|
748
|
+
for (const node of topLevelNodes(tree)) {
|
|
749
|
+
const content = serialize(node);
|
|
750
|
+
const nodes = countNodes(node);
|
|
751
|
+
const bytes = encoder.encode(content).length;
|
|
752
|
+
const nextChars = activeContent.length + content.length;
|
|
753
|
+
const nextNodes = activeNodes + nodes;
|
|
754
|
+
const nextBytes = activeBytes + bytes;
|
|
755
|
+
if (activeNodeId !== null && (nextChars > maxChars || nextNodes > maxNodes || nextBytes > maxBytes)) {
|
|
756
|
+
flush();
|
|
757
|
+
}
|
|
758
|
+
if (activeNodeId === null) {
|
|
759
|
+
activeNodeId = node.id;
|
|
760
|
+
}
|
|
761
|
+
activeContent += content;
|
|
762
|
+
activeNodes += nodes;
|
|
763
|
+
activeBytes += bytes;
|
|
764
|
+
}
|
|
765
|
+
flush();
|
|
766
|
+
return chunks;
|
|
767
|
+
}
|
|
768
|
+
function indexNodeSpans(node, into) {
|
|
769
|
+
into.set(node.id, {
|
|
770
|
+
provenance: node.spanProvenance ?? "none",
|
|
771
|
+
...(node.span ? { span: node.span } : {})
|
|
772
|
+
});
|
|
773
|
+
for (const child of childNodes(node)) {
|
|
774
|
+
indexNodeSpans(child, into);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
function indexNodes(node, into) {
|
|
778
|
+
into.set(node.id, node);
|
|
779
|
+
for (const child of childNodes(node)) {
|
|
780
|
+
indexNodes(child, into);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function failPatchPlanning(payload) {
|
|
784
|
+
throw new PatchPlanningError(payload);
|
|
785
|
+
}
|
|
786
|
+
function requireNode(nodeById, target) {
|
|
787
|
+
const node = nodeById.get(target);
|
|
788
|
+
if (!node) {
|
|
789
|
+
failPatchPlanning({ code: "NODE_NOT_FOUND", target });
|
|
790
|
+
}
|
|
791
|
+
return node;
|
|
792
|
+
}
|
|
793
|
+
function requireNodeSpan(spanByNode, target) {
|
|
794
|
+
const indexedSpan = spanByNode.get(target);
|
|
795
|
+
if (!indexedSpan) {
|
|
796
|
+
failPatchPlanning({ code: "MISSING_NODE_SPAN", target });
|
|
797
|
+
}
|
|
798
|
+
if (indexedSpan.provenance !== "input") {
|
|
799
|
+
failPatchPlanning({ code: "NON_INPUT_SPAN_PROVENANCE", target, detail: indexedSpan.provenance });
|
|
800
|
+
}
|
|
801
|
+
if (!indexedSpan.span) {
|
|
802
|
+
failPatchPlanning({ code: "MISSING_NODE_SPAN", target });
|
|
803
|
+
}
|
|
804
|
+
return indexedSpan.span;
|
|
805
|
+
}
|
|
806
|
+
function buildReplacement(spanByNode, nodeById, edit, sourceIndex) {
|
|
807
|
+
if (edit.kind === "removeNode") {
|
|
808
|
+
const span = requireNodeSpan(spanByNode, edit.target);
|
|
809
|
+
return {
|
|
810
|
+
sourceIndex,
|
|
811
|
+
target: edit.target,
|
|
812
|
+
start: span.start,
|
|
813
|
+
end: span.end,
|
|
814
|
+
replacementCss: ""
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
if (edit.kind === "replaceNode") {
|
|
818
|
+
requireNode(nodeById, edit.target);
|
|
819
|
+
const span = requireNodeSpan(spanByNode, edit.target);
|
|
820
|
+
return {
|
|
821
|
+
sourceIndex,
|
|
822
|
+
target: edit.target,
|
|
823
|
+
start: span.start,
|
|
824
|
+
end: span.end,
|
|
825
|
+
replacementCss: edit.css
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
if (edit.kind === "insertCssBefore") {
|
|
829
|
+
requireNode(nodeById, edit.target);
|
|
830
|
+
const span = requireNodeSpan(spanByNode, edit.target);
|
|
831
|
+
return {
|
|
832
|
+
sourceIndex,
|
|
833
|
+
target: edit.target,
|
|
834
|
+
start: span.start,
|
|
835
|
+
end: span.start,
|
|
836
|
+
replacementCss: edit.css
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
requireNode(nodeById, edit.target);
|
|
840
|
+
const span = requireNodeSpan(spanByNode, edit.target);
|
|
841
|
+
return {
|
|
842
|
+
sourceIndex,
|
|
843
|
+
target: edit.target,
|
|
844
|
+
start: span.end,
|
|
845
|
+
end: span.end,
|
|
846
|
+
replacementCss: edit.css
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
export function applyPatchPlan(originalCss, plan) {
|
|
850
|
+
let cursor = 0;
|
|
851
|
+
let output = "";
|
|
852
|
+
for (const step of plan.steps) {
|
|
853
|
+
if (step.kind === "slice") {
|
|
854
|
+
if (step.start < cursor || step.end < step.start || step.end > originalCss.length) {
|
|
855
|
+
throw new Error("invalid patch slice bounds");
|
|
856
|
+
}
|
|
857
|
+
output += originalCss.slice(step.start, step.end);
|
|
858
|
+
cursor = step.end;
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
if (step.at !== cursor || step.at > originalCss.length) {
|
|
862
|
+
throw new Error("invalid patch insertion offset");
|
|
863
|
+
}
|
|
864
|
+
output += step.text;
|
|
865
|
+
}
|
|
866
|
+
return output;
|
|
867
|
+
}
|
|
868
|
+
export function computePatch(originalCss, edits) {
|
|
869
|
+
if (edits.length === 0) {
|
|
870
|
+
const steps = [
|
|
871
|
+
{
|
|
872
|
+
kind: "slice",
|
|
873
|
+
start: 0,
|
|
874
|
+
end: originalCss.length
|
|
875
|
+
}
|
|
876
|
+
];
|
|
877
|
+
return {
|
|
878
|
+
steps,
|
|
879
|
+
result: originalCss
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
const parsed = parse(originalCss, { captureSpans: true });
|
|
883
|
+
const spanByNode = new Map();
|
|
884
|
+
const nodeById = new Map();
|
|
885
|
+
indexNodeSpans(parsed.root, spanByNode);
|
|
886
|
+
indexNodes(parsed.root, nodeById);
|
|
887
|
+
const replacements = edits.map((edit, sourceIndex) => buildReplacement(spanByNode, nodeById, edit, sourceIndex));
|
|
888
|
+
replacements.sort((left, right) => {
|
|
889
|
+
if (left.start !== right.start) {
|
|
890
|
+
return left.start - right.start;
|
|
891
|
+
}
|
|
892
|
+
if (left.end !== right.end) {
|
|
893
|
+
return left.end - right.end;
|
|
894
|
+
}
|
|
895
|
+
return left.sourceIndex - right.sourceIndex;
|
|
896
|
+
});
|
|
897
|
+
let previousEnd = 0;
|
|
898
|
+
for (const replacement of replacements) {
|
|
899
|
+
if (replacement.start < 0 ||
|
|
900
|
+
replacement.end < replacement.start ||
|
|
901
|
+
replacement.end > originalCss.length) {
|
|
902
|
+
failPatchPlanning({ code: "OVERLAPPING_EDITS", target: replacement.target, detail: "invalid replacement bounds" });
|
|
903
|
+
}
|
|
904
|
+
if (replacement.start < previousEnd) {
|
|
905
|
+
failPatchPlanning({ code: "OVERLAPPING_EDITS", target: replacement.target });
|
|
906
|
+
}
|
|
907
|
+
previousEnd = Math.max(previousEnd, replacement.end);
|
|
908
|
+
}
|
|
909
|
+
const steps = [];
|
|
910
|
+
let cursor = 0;
|
|
911
|
+
for (const replacement of replacements) {
|
|
912
|
+
if (cursor < replacement.start) {
|
|
913
|
+
steps.push({
|
|
914
|
+
kind: "slice",
|
|
915
|
+
start: cursor,
|
|
916
|
+
end: replacement.start
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
steps.push({
|
|
920
|
+
kind: "insert",
|
|
921
|
+
at: replacement.start,
|
|
922
|
+
text: replacement.replacementCss
|
|
923
|
+
});
|
|
924
|
+
cursor = replacement.end;
|
|
925
|
+
}
|
|
926
|
+
if (cursor < originalCss.length) {
|
|
927
|
+
steps.push({
|
|
928
|
+
kind: "slice",
|
|
929
|
+
start: cursor,
|
|
930
|
+
end: originalCss.length
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
const result = applyPatchPlan(originalCss, {
|
|
934
|
+
steps,
|
|
935
|
+
result: ""
|
|
936
|
+
});
|
|
937
|
+
return {
|
|
938
|
+
steps,
|
|
939
|
+
result
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
function isSelectorRecord(value) {
|
|
943
|
+
return value !== null && typeof value === "object";
|
|
944
|
+
}
|
|
945
|
+
function selectorNodeKind(node) {
|
|
946
|
+
if (typeof node.kind === "string") {
|
|
947
|
+
return node.kind.toLowerCase();
|
|
948
|
+
}
|
|
949
|
+
if (typeof node.type === "string") {
|
|
950
|
+
return node.type.toLowerCase();
|
|
951
|
+
}
|
|
952
|
+
return "";
|
|
953
|
+
}
|
|
954
|
+
function isSelectorElementNode(node) {
|
|
955
|
+
return selectorNodeKind(node) === "element";
|
|
956
|
+
}
|
|
957
|
+
function selectorChildrenFromNode(node) {
|
|
958
|
+
if (!Array.isArray(node.children)) {
|
|
959
|
+
return [];
|
|
960
|
+
}
|
|
961
|
+
const children = [];
|
|
962
|
+
for (const child of node.children) {
|
|
963
|
+
if (isSelectorRecord(child)) {
|
|
964
|
+
children.push(child);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return children;
|
|
968
|
+
}
|
|
969
|
+
function selectorTagName(node) {
|
|
970
|
+
if (!isSelectorElementNode(node) || typeof node.tagName !== "string") {
|
|
971
|
+
return null;
|
|
972
|
+
}
|
|
973
|
+
return node.tagName.toLowerCase();
|
|
974
|
+
}
|
|
975
|
+
function isAsciiWhitespaceCode(code) {
|
|
976
|
+
return code === 9 || code === 10 || code === 12 || code === 13 || code === 32;
|
|
977
|
+
}
|
|
978
|
+
function lowerAsciiCode(code) {
|
|
979
|
+
if (code >= 65 && code <= 90) {
|
|
980
|
+
return code + 32;
|
|
981
|
+
}
|
|
982
|
+
return code;
|
|
983
|
+
}
|
|
984
|
+
function equalsAsciiCaseInsensitive(left, right) {
|
|
985
|
+
if (left.length !== right.length) {
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
989
|
+
if (lowerAsciiCode(left.charCodeAt(index)) !== lowerAsciiCode(right.charCodeAt(index))) {
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return true;
|
|
994
|
+
}
|
|
995
|
+
function includesWhitespaceSeparatedToken(input, expectedToken) {
|
|
996
|
+
let tokenStart = -1;
|
|
997
|
+
for (let index = 0; index <= input.length; index += 1) {
|
|
998
|
+
const code = index < input.length ? input.charCodeAt(index) : 32;
|
|
999
|
+
const atBoundary = index === input.length || isAsciiWhitespaceCode(code);
|
|
1000
|
+
if (tokenStart === -1) {
|
|
1001
|
+
if (!atBoundary) {
|
|
1002
|
+
tokenStart = index;
|
|
1003
|
+
}
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
if (!atBoundary) {
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
if (index - tokenStart === expectedToken.length && input.slice(tokenStart, index) === expectedToken) {
|
|
1010
|
+
return true;
|
|
1011
|
+
}
|
|
1012
|
+
tokenStart = -1;
|
|
1013
|
+
}
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
function selectorAttributeValue(node, name) {
|
|
1017
|
+
if (!isSelectorElementNode(node) || !Array.isArray(node.attributes)) {
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
const target = name.toLowerCase();
|
|
1021
|
+
for (const attribute of node.attributes) {
|
|
1022
|
+
if (!isSelectorRecord(attribute)) {
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (typeof attribute.name !== "string" || typeof attribute.value !== "string") {
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
if (equalsAsciiCaseInsensitive(attribute.name, target)) {
|
|
1029
|
+
return attribute.value;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
1034
|
+
function selectorAttributeMatch(actualValue, matcher, expectedValue, flags) {
|
|
1035
|
+
if (actualValue === null) {
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
if (matcher === null) {
|
|
1039
|
+
return true;
|
|
1040
|
+
}
|
|
1041
|
+
if (expectedValue === null) {
|
|
1042
|
+
return false;
|
|
1043
|
+
}
|
|
1044
|
+
const caseInsensitive = (flags ?? "").toLowerCase().includes("i");
|
|
1045
|
+
const left = caseInsensitive ? actualValue.toLowerCase() : actualValue;
|
|
1046
|
+
const right = caseInsensitive ? expectedValue.toLowerCase() : expectedValue;
|
|
1047
|
+
switch (matcher) {
|
|
1048
|
+
case "=":
|
|
1049
|
+
return left === right;
|
|
1050
|
+
case "~=":
|
|
1051
|
+
return includesWhitespaceSeparatedToken(left, right);
|
|
1052
|
+
case "|=":
|
|
1053
|
+
return left === right || left.startsWith(`${right}-`);
|
|
1054
|
+
case "^=":
|
|
1055
|
+
return left.startsWith(right);
|
|
1056
|
+
case "$=":
|
|
1057
|
+
return left.endsWith(right);
|
|
1058
|
+
case "*=":
|
|
1059
|
+
return left.includes(right);
|
|
1060
|
+
default:
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
function matchesSelectorSimple(node, simple) {
|
|
1065
|
+
if (!isSelectorElementNode(node)) {
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
if (simple.kind === "type") {
|
|
1069
|
+
if (simple.universal) {
|
|
1070
|
+
return true;
|
|
1071
|
+
}
|
|
1072
|
+
return selectorTagName(node) === simple.name.toLowerCase();
|
|
1073
|
+
}
|
|
1074
|
+
if (simple.kind === "id") {
|
|
1075
|
+
return selectorAttributeValue(node, "id") === simple.value;
|
|
1076
|
+
}
|
|
1077
|
+
if (simple.kind === "class") {
|
|
1078
|
+
const classes = selectorAttributeValue(node, "class");
|
|
1079
|
+
if (classes === null) {
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
return includesWhitespaceSeparatedToken(classes, simple.value);
|
|
1083
|
+
}
|
|
1084
|
+
return selectorAttributeMatch(selectorAttributeValue(node, simple.name), simple.matcher, simple.value, simple.flags);
|
|
1085
|
+
}
|
|
1086
|
+
function matchesSelectorCompound(node, compound) {
|
|
1087
|
+
if (compound.simpleSelectors.length === 0) {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
for (const simple of compound.simpleSelectors) {
|
|
1091
|
+
if (!matchesSelectorSimple(node, simple)) {
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return true;
|
|
1096
|
+
}
|
|
1097
|
+
function buildSelectorTreeIndex(root, options) {
|
|
1098
|
+
const parentByNode = new Map();
|
|
1099
|
+
const elements = [];
|
|
1100
|
+
const stack = [{ node: root, parent: null }];
|
|
1101
|
+
let visited = 0;
|
|
1102
|
+
while (stack.length > 0) {
|
|
1103
|
+
const current = stack.pop();
|
|
1104
|
+
if (!current) {
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
if (parentByNode.has(current.node)) {
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
visited += 1;
|
|
1111
|
+
if (options.maxVisitedNodes !== undefined && visited > options.maxVisitedNodes) {
|
|
1112
|
+
throw new BudgetExceededError({
|
|
1113
|
+
code: "BUDGET_EXCEEDED",
|
|
1114
|
+
budget: "maxNodes",
|
|
1115
|
+
limit: options.maxVisitedNodes,
|
|
1116
|
+
actual: visited
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
parentByNode.set(current.node, current.parent);
|
|
1120
|
+
if (isSelectorElementNode(current.node)) {
|
|
1121
|
+
elements.push(current.node);
|
|
1122
|
+
}
|
|
1123
|
+
const children = selectorChildrenFromNode(current.node);
|
|
1124
|
+
for (let index = children.length - 1; index >= 0; index -= 1) {
|
|
1125
|
+
const child = children[index];
|
|
1126
|
+
if (!child) {
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
stack.push({
|
|
1130
|
+
node: child,
|
|
1131
|
+
parent: current.node
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
parentByNode,
|
|
1137
|
+
elements
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
function matchesCompiledSelector(selector, node, treeIndex) {
|
|
1141
|
+
if (!selector.supported || selector.compounds.length === 0) {
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
const matchAt = (compoundIndex, candidate) => {
|
|
1145
|
+
const compound = selector.compounds[compoundIndex];
|
|
1146
|
+
if (!compound) {
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
if (!matchesSelectorCompound(candidate, compound)) {
|
|
1150
|
+
return false;
|
|
1151
|
+
}
|
|
1152
|
+
if (compoundIndex === 0) {
|
|
1153
|
+
return true;
|
|
1154
|
+
}
|
|
1155
|
+
const combinator = selector.combinators[compoundIndex - 1];
|
|
1156
|
+
if (combinator === ">") {
|
|
1157
|
+
const parent = treeIndex.parentByNode.get(candidate) ?? null;
|
|
1158
|
+
if (parent === null || !isSelectorElementNode(parent)) {
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
return matchAt(compoundIndex - 1, parent);
|
|
1162
|
+
}
|
|
1163
|
+
let parent = treeIndex.parentByNode.get(candidate) ?? null;
|
|
1164
|
+
while (parent !== null) {
|
|
1165
|
+
if (isSelectorElementNode(parent) && matchAt(compoundIndex - 1, parent)) {
|
|
1166
|
+
return true;
|
|
1167
|
+
}
|
|
1168
|
+
parent = treeIndex.parentByNode.get(parent) ?? null;
|
|
1169
|
+
}
|
|
1170
|
+
return false;
|
|
1171
|
+
};
|
|
1172
|
+
return matchAt(selector.compounds.length - 1, node);
|
|
1173
|
+
}
|
|
1174
|
+
function matchesCompiledSelectorInTraversal(selector, frame) {
|
|
1175
|
+
if (!selector.supported || selector.compounds.length === 0) {
|
|
1176
|
+
return false;
|
|
1177
|
+
}
|
|
1178
|
+
const matchAt = (compoundIndex, candidate) => {
|
|
1179
|
+
const compound = selector.compounds[compoundIndex];
|
|
1180
|
+
if (!compound) {
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
if (!matchesSelectorCompound(candidate.node, compound)) {
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
if (compoundIndex === 0) {
|
|
1187
|
+
return true;
|
|
1188
|
+
}
|
|
1189
|
+
const combinator = selector.combinators[compoundIndex - 1];
|
|
1190
|
+
if (combinator === ">") {
|
|
1191
|
+
const parent = candidate.parentElement;
|
|
1192
|
+
if (!parent) {
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
return matchAt(compoundIndex - 1, parent);
|
|
1196
|
+
}
|
|
1197
|
+
let cursor = candidate.parentElement;
|
|
1198
|
+
while (cursor) {
|
|
1199
|
+
if (matchAt(compoundIndex - 1, cursor)) {
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
cursor = cursor.parentElement;
|
|
1203
|
+
}
|
|
1204
|
+
return false;
|
|
1205
|
+
};
|
|
1206
|
+
return matchAt(selector.compounds.length - 1, frame);
|
|
1207
|
+
}
|
|
1208
|
+
function assertSelectorStrict(compiled, strict) {
|
|
1209
|
+
if (!strict) {
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
if (compiled.parseErrors.length > 0) {
|
|
1213
|
+
throw new Error(`selector parse failed in strict mode: ${compiled.parseErrors[0]?.message ?? "unknown error"}`);
|
|
1214
|
+
}
|
|
1215
|
+
if (compiled.unsupportedParts.length > 0) {
|
|
1216
|
+
const sample = compiled.unsupportedParts[0];
|
|
1217
|
+
throw new Error(`selector unsupported in strict mode: ${sample?.partType ?? "unknown"} ${sample?.detail ?? ""}`.trim());
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
function selectorSimpleFromNode(selectorNode, selectorIndex, unsupported) {
|
|
1221
|
+
const type = typeof selectorNode.type === "string" ? selectorNode.type : "";
|
|
1222
|
+
if (type === "TypeSelector") {
|
|
1223
|
+
const rawName = typeof selectorNode.name === "string" ? selectorNode.name : "";
|
|
1224
|
+
if (rawName.length === 0) {
|
|
1225
|
+
unsupported.push({
|
|
1226
|
+
selectorIndex,
|
|
1227
|
+
partType: type,
|
|
1228
|
+
detail: "missing type name"
|
|
1229
|
+
});
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
return {
|
|
1233
|
+
kind: "type",
|
|
1234
|
+
name: rawName.toLowerCase(),
|
|
1235
|
+
universal: rawName === "*"
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
if (type === "IdSelector") {
|
|
1239
|
+
const value = typeof selectorNode.name === "string" ? selectorNode.name : "";
|
|
1240
|
+
if (value.length === 0) {
|
|
1241
|
+
unsupported.push({
|
|
1242
|
+
selectorIndex,
|
|
1243
|
+
partType: type,
|
|
1244
|
+
detail: "missing id value"
|
|
1245
|
+
});
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
return {
|
|
1249
|
+
kind: "id",
|
|
1250
|
+
value
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
if (type === "ClassSelector") {
|
|
1254
|
+
const value = typeof selectorNode.name === "string" ? selectorNode.name : "";
|
|
1255
|
+
if (value.length === 0) {
|
|
1256
|
+
unsupported.push({
|
|
1257
|
+
selectorIndex,
|
|
1258
|
+
partType: type,
|
|
1259
|
+
detail: "missing class value"
|
|
1260
|
+
});
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
return {
|
|
1264
|
+
kind: "class",
|
|
1265
|
+
value
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
if (type === "AttributeSelector") {
|
|
1269
|
+
const rawNameNode = isSelectorRecord(selectorNode.name) ? selectorNode.name : null;
|
|
1270
|
+
const rawName = rawNameNode && typeof rawNameNode.name === "string" ? rawNameNode.name : "";
|
|
1271
|
+
if (rawName.length === 0) {
|
|
1272
|
+
unsupported.push({
|
|
1273
|
+
selectorIndex,
|
|
1274
|
+
partType: type,
|
|
1275
|
+
detail: "missing attribute name"
|
|
1276
|
+
});
|
|
1277
|
+
return null;
|
|
1278
|
+
}
|
|
1279
|
+
const rawMatcher = selectorNode.matcher;
|
|
1280
|
+
let matcher = null;
|
|
1281
|
+
if (rawMatcher !== null && rawMatcher !== undefined) {
|
|
1282
|
+
if (rawMatcher === "=" ||
|
|
1283
|
+
rawMatcher === "~=" ||
|
|
1284
|
+
rawMatcher === "|=" ||
|
|
1285
|
+
rawMatcher === "^=" ||
|
|
1286
|
+
rawMatcher === "$=" ||
|
|
1287
|
+
rawMatcher === "*=") {
|
|
1288
|
+
matcher = rawMatcher;
|
|
1289
|
+
}
|
|
1290
|
+
else {
|
|
1291
|
+
const rawMatcherDetail = typeof rawMatcher === "string" ? rawMatcher : typeof rawMatcher;
|
|
1292
|
+
unsupported.push({
|
|
1293
|
+
selectorIndex,
|
|
1294
|
+
partType: type,
|
|
1295
|
+
detail: `unsupported matcher ${rawMatcherDetail}`
|
|
1296
|
+
});
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
const rawValueNode = isSelectorRecord(selectorNode.value) ? selectorNode.value : null;
|
|
1301
|
+
const value = rawValueNode
|
|
1302
|
+
? (typeof rawValueNode.value === "string"
|
|
1303
|
+
? rawValueNode.value
|
|
1304
|
+
: (typeof rawValueNode.name === "string" ? rawValueNode.name : null))
|
|
1305
|
+
: null;
|
|
1306
|
+
const flags = typeof selectorNode.flags === "string" ? selectorNode.flags : null;
|
|
1307
|
+
return {
|
|
1308
|
+
kind: "attribute",
|
|
1309
|
+
name: rawName.toLowerCase(),
|
|
1310
|
+
matcher,
|
|
1311
|
+
value,
|
|
1312
|
+
flags
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
unsupported.push({
|
|
1316
|
+
selectorIndex,
|
|
1317
|
+
partType: type.length > 0 ? type : "unknown",
|
|
1318
|
+
detail: "unsupported simple selector"
|
|
1319
|
+
});
|
|
1320
|
+
return null;
|
|
1321
|
+
}
|
|
1322
|
+
function normalizeSelectorCombinator(rawName, selectorIndex, unsupported) {
|
|
1323
|
+
const combinatorName = typeof rawName === "string" ? rawName : "";
|
|
1324
|
+
const trimmed = combinatorName.trim();
|
|
1325
|
+
if (trimmed === ">") {
|
|
1326
|
+
return ">";
|
|
1327
|
+
}
|
|
1328
|
+
if (trimmed.length === 0) {
|
|
1329
|
+
return " ";
|
|
1330
|
+
}
|
|
1331
|
+
unsupported.push({
|
|
1332
|
+
selectorIndex,
|
|
1333
|
+
partType: "Combinator",
|
|
1334
|
+
detail: `unsupported combinator ${trimmed}`
|
|
1335
|
+
});
|
|
1336
|
+
return " ";
|
|
1337
|
+
}
|
|
1338
|
+
function compileSingleSelector(selectorNode, selectorIndex) {
|
|
1339
|
+
const unsupported = [];
|
|
1340
|
+
const compounds = [];
|
|
1341
|
+
const combinators = [];
|
|
1342
|
+
let currentSimpleSelectors = [];
|
|
1343
|
+
const children = Array.isArray(selectorNode.children) ? selectorNode.children : [];
|
|
1344
|
+
for (const child of children) {
|
|
1345
|
+
if (!isSelectorRecord(child)) {
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
const childType = typeof child.type === "string" ? child.type : "";
|
|
1349
|
+
if (childType === "Combinator") {
|
|
1350
|
+
if (currentSimpleSelectors.length === 0) {
|
|
1351
|
+
unsupported.push({
|
|
1352
|
+
selectorIndex,
|
|
1353
|
+
partType: "Combinator",
|
|
1354
|
+
detail: "combinator without left compound"
|
|
1355
|
+
});
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
compounds.push({
|
|
1359
|
+
simpleSelectors: currentSimpleSelectors
|
|
1360
|
+
});
|
|
1361
|
+
combinators.push(normalizeSelectorCombinator(child.name, selectorIndex, unsupported));
|
|
1362
|
+
currentSimpleSelectors = [];
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
const simple = selectorSimpleFromNode(child, selectorIndex, unsupported);
|
|
1366
|
+
if (simple) {
|
|
1367
|
+
currentSimpleSelectors.push(simple);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
if (currentSimpleSelectors.length > 0) {
|
|
1371
|
+
compounds.push({
|
|
1372
|
+
simpleSelectors: currentSimpleSelectors
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
else if (combinators.length > 0) {
|
|
1376
|
+
unsupported.push({
|
|
1377
|
+
selectorIndex,
|
|
1378
|
+
partType: "Combinator",
|
|
1379
|
+
detail: "selector ends with combinator"
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
if (compounds.length === 0) {
|
|
1383
|
+
unsupported.push({
|
|
1384
|
+
selectorIndex,
|
|
1385
|
+
partType: "Selector",
|
|
1386
|
+
detail: "empty selector"
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
if (compounds.length > 0 && combinators.length !== compounds.length - 1) {
|
|
1390
|
+
unsupported.push({
|
|
1391
|
+
selectorIndex,
|
|
1392
|
+
partType: "Selector",
|
|
1393
|
+
detail: "invalid combinator count"
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
return {
|
|
1397
|
+
selectorIndex,
|
|
1398
|
+
compounds,
|
|
1399
|
+
combinators,
|
|
1400
|
+
supported: unsupported.length === 0,
|
|
1401
|
+
unsupportedParts: unsupported
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
export function compileSelectorList(selectorText) {
|
|
1405
|
+
const parsed = parseFragment(selectorText, "selectorList");
|
|
1406
|
+
const selectorNodes = Array.isArray(parsed.root.children)
|
|
1407
|
+
? parsed.root.children.filter((child) => isSelectorRecord(child) && child.type === "Selector")
|
|
1408
|
+
: [];
|
|
1409
|
+
const selectors = selectorNodes.map((selectorNode, selectorIndex) => compileSingleSelector(selectorNode, selectorIndex));
|
|
1410
|
+
const unsupportedParts = selectors.flatMap((selector) => selector.unsupportedParts);
|
|
1411
|
+
const supported = parsed.errors.length === 0 && selectors.length > 0 && unsupportedParts.length === 0;
|
|
1412
|
+
return {
|
|
1413
|
+
source: selectorText,
|
|
1414
|
+
parseErrors: parsed.errors,
|
|
1415
|
+
selectors,
|
|
1416
|
+
supported,
|
|
1417
|
+
unsupportedParts
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
function compareSpecificity(left, right) {
|
|
1421
|
+
if (left.a !== right.a) {
|
|
1422
|
+
return left.a - right.a;
|
|
1423
|
+
}
|
|
1424
|
+
if (left.b !== right.b) {
|
|
1425
|
+
return left.b - right.b;
|
|
1426
|
+
}
|
|
1427
|
+
return left.c - right.c;
|
|
1428
|
+
}
|
|
1429
|
+
function specificityForSimple(simple) {
|
|
1430
|
+
switch (simple.kind) {
|
|
1431
|
+
case "id":
|
|
1432
|
+
return { a: 1, b: 0, c: 0 };
|
|
1433
|
+
case "class":
|
|
1434
|
+
case "attribute":
|
|
1435
|
+
return { a: 0, b: 1, c: 0 };
|
|
1436
|
+
case "type":
|
|
1437
|
+
return { a: 0, b: 0, c: simple.universal ? 0 : 1 };
|
|
1438
|
+
default:
|
|
1439
|
+
return { a: 0, b: 0, c: 0 };
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
function specificityForCompiledSelector(selector) {
|
|
1443
|
+
let a = 0;
|
|
1444
|
+
let b = 0;
|
|
1445
|
+
let c = 0;
|
|
1446
|
+
for (const compound of selector.compounds) {
|
|
1447
|
+
for (const simple of compound.simpleSelectors) {
|
|
1448
|
+
const specificity = specificityForSimple(simple);
|
|
1449
|
+
a += specificity.a;
|
|
1450
|
+
b += specificity.b;
|
|
1451
|
+
c += specificity.c;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return { a, b, c };
|
|
1455
|
+
}
|
|
1456
|
+
function specificityMaxForSelectorList(selectorList) {
|
|
1457
|
+
let best = { a: 0, b: 0, c: 0 };
|
|
1458
|
+
for (const selector of selectorList.selectors) {
|
|
1459
|
+
const current = specificityForCompiledSelector(selector);
|
|
1460
|
+
if (compareSpecificity(current, best) > 0) {
|
|
1461
|
+
best = current;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
return best;
|
|
1465
|
+
}
|
|
1466
|
+
function declarationSignalFromNode(node, declarationOrder) {
|
|
1467
|
+
if (node.type !== "Declaration") {
|
|
1468
|
+
return null;
|
|
1469
|
+
}
|
|
1470
|
+
const propertyRaw = typeof node["property"] === "string" ? node["property"] : "";
|
|
1471
|
+
const property = propertyRaw.trim().toLowerCase();
|
|
1472
|
+
if (property.length === 0) {
|
|
1473
|
+
return null;
|
|
1474
|
+
}
|
|
1475
|
+
const valueNode = isPublicNode(node["value"]) ? node["value"] : null;
|
|
1476
|
+
const value = valueNode ? serialize(valueNode).trim() : "";
|
|
1477
|
+
const important = node["important"] === true;
|
|
1478
|
+
return {
|
|
1479
|
+
declarationNodeId: node.id,
|
|
1480
|
+
property,
|
|
1481
|
+
value,
|
|
1482
|
+
important,
|
|
1483
|
+
declarationOrder
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
function declarationSignalsFromContainer(container) {
|
|
1487
|
+
const signals = [];
|
|
1488
|
+
let declarationOrder = 0;
|
|
1489
|
+
for (const child of childNodes(container)) {
|
|
1490
|
+
const declaration = declarationSignalFromNode(child, declarationOrder);
|
|
1491
|
+
if (!declaration) {
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
signals.push(declaration);
|
|
1495
|
+
declarationOrder += 1;
|
|
1496
|
+
}
|
|
1497
|
+
return signals;
|
|
1498
|
+
}
|
|
1499
|
+
function normalizeDeclarationValueForRenderSignals(value) {
|
|
1500
|
+
return value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1501
|
+
}
|
|
1502
|
+
function classifyRenderSignalClass(declaration, options) {
|
|
1503
|
+
const includeControlAffordance = options.includeControlAffordance !== false;
|
|
1504
|
+
const includeVisibilitySignals = options.includeVisibilitySignals !== false;
|
|
1505
|
+
const property = declaration.property.toLowerCase();
|
|
1506
|
+
const normalizedValue = normalizeDeclarationValueForRenderSignals(declaration.value);
|
|
1507
|
+
if (includeVisibilitySignals) {
|
|
1508
|
+
if (property === "display" && normalizedValue === "none") {
|
|
1509
|
+
return "visibility-hidden-subtree";
|
|
1510
|
+
}
|
|
1511
|
+
if (property === "visibility" && (normalizedValue === "hidden" || normalizedValue === "collapse")) {
|
|
1512
|
+
return "visibility-hidden-subtree";
|
|
1513
|
+
}
|
|
1514
|
+
if (property === "content-visibility" && normalizedValue === "hidden") {
|
|
1515
|
+
return "visibility-hidden-subtree";
|
|
1516
|
+
}
|
|
1517
|
+
if (property === "opacity" && normalizedValue === "0") {
|
|
1518
|
+
return "visibility-hidden-self";
|
|
1519
|
+
}
|
|
1520
|
+
if (property === "font-size" && normalizedValue === "0") {
|
|
1521
|
+
return "visibility-hidden-self";
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
if (includeControlAffordance) {
|
|
1525
|
+
if (property === "cursor" && normalizedValue === "pointer") {
|
|
1526
|
+
return "control-affordance";
|
|
1527
|
+
}
|
|
1528
|
+
if (property === "appearance" && normalizedValue.includes("button")) {
|
|
1529
|
+
return "control-affordance";
|
|
1530
|
+
}
|
|
1531
|
+
if (property === "user-select" && normalizedValue === "none") {
|
|
1532
|
+
return "control-affordance";
|
|
1533
|
+
}
|
|
1534
|
+
if (property === "pointer-events" && normalizedValue === "none") {
|
|
1535
|
+
return "control-affordance";
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1540
|
+
export function extractStyleRuleSignals(cssOrTree, options = {}) {
|
|
1541
|
+
const includeUnsupportedSelectors = options.includeUnsupportedSelectors === true;
|
|
1542
|
+
const strictSelectors = options.strictSelectors === true;
|
|
1543
|
+
const tree = typeof cssOrTree === "string" ? parse(cssOrTree) : cssOrTree;
|
|
1544
|
+
const signals = [];
|
|
1545
|
+
let cascadeOrder = 0;
|
|
1546
|
+
walk(tree, (node) => {
|
|
1547
|
+
if (node.type !== "Rule") {
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
const ruleOrder = cascadeOrder;
|
|
1551
|
+
cascadeOrder += 1;
|
|
1552
|
+
const preludeNode = isPublicNode(node["prelude"]) ? node["prelude"] : null;
|
|
1553
|
+
const blockNode = isPublicNode(node["block"]) ? node["block"] : null;
|
|
1554
|
+
if (!preludeNode || !blockNode) {
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
const selectorText = serialize(preludeNode).trim();
|
|
1558
|
+
if (selectorText.length === 0) {
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
const selector = compileSelectorList(selectorText);
|
|
1562
|
+
if (strictSelectors && !selector.supported) {
|
|
1563
|
+
const unsupported = selector.unsupportedParts[0];
|
|
1564
|
+
const detail = unsupported
|
|
1565
|
+
? `${unsupported.partType} ${unsupported.detail}`.trim()
|
|
1566
|
+
: "unknown selector unsupported part";
|
|
1567
|
+
throw new Error(`unsupported selector in strict mode: ${detail}`);
|
|
1568
|
+
}
|
|
1569
|
+
if (!includeUnsupportedSelectors && !selector.supported) {
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
signals.push({
|
|
1573
|
+
ruleNodeId: node.id,
|
|
1574
|
+
selectorText,
|
|
1575
|
+
selector,
|
|
1576
|
+
selectorSupported: selector.supported,
|
|
1577
|
+
specificityMax: specificityMaxForSelectorList(selector),
|
|
1578
|
+
cascadeOrder: ruleOrder,
|
|
1579
|
+
declarations: declarationSignalsFromContainer(blockNode)
|
|
1580
|
+
});
|
|
1581
|
+
});
|
|
1582
|
+
return signals;
|
|
1583
|
+
}
|
|
1584
|
+
export function extractInlineStyleSignals(styleText) {
|
|
1585
|
+
const fragment = parseDeclarationList(styleText);
|
|
1586
|
+
const signals = [];
|
|
1587
|
+
let declarationOrder = 0;
|
|
1588
|
+
for (const node of fragment.children) {
|
|
1589
|
+
const declaration = declarationSignalFromNode(node, declarationOrder);
|
|
1590
|
+
if (!declaration) {
|
|
1591
|
+
continue;
|
|
1592
|
+
}
|
|
1593
|
+
signals.push(declaration);
|
|
1594
|
+
declarationOrder += 1;
|
|
1595
|
+
}
|
|
1596
|
+
return signals;
|
|
1597
|
+
}
|
|
1598
|
+
export function extractRenderSignals(cssOrTree, options = {}) {
|
|
1599
|
+
const ruleSignals = extractStyleRuleSignals(cssOrTree, options);
|
|
1600
|
+
const renderSignals = [];
|
|
1601
|
+
for (const ruleSignal of ruleSignals) {
|
|
1602
|
+
for (const declaration of ruleSignal.declarations) {
|
|
1603
|
+
const signalClass = classifyRenderSignalClass(declaration, options);
|
|
1604
|
+
if (!signalClass) {
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
renderSignals.push({
|
|
1608
|
+
signalClass,
|
|
1609
|
+
source: "rule",
|
|
1610
|
+
property: declaration.property,
|
|
1611
|
+
value: declaration.value,
|
|
1612
|
+
important: declaration.important,
|
|
1613
|
+
declarationOrder: declaration.declarationOrder,
|
|
1614
|
+
selectorText: ruleSignal.selectorText,
|
|
1615
|
+
declarationNodeId: declaration.declarationNodeId,
|
|
1616
|
+
ruleNodeId: ruleSignal.ruleNodeId,
|
|
1617
|
+
cascadeOrder: ruleSignal.cascadeOrder
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
return renderSignals;
|
|
1622
|
+
}
|
|
1623
|
+
export function extractInlineRenderSignals(styleText, options = {}) {
|
|
1624
|
+
const declarations = extractInlineStyleSignals(styleText);
|
|
1625
|
+
const renderSignals = [];
|
|
1626
|
+
for (const declaration of declarations) {
|
|
1627
|
+
const signalClass = classifyRenderSignalClass(declaration, options);
|
|
1628
|
+
if (!signalClass) {
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
renderSignals.push({
|
|
1632
|
+
signalClass,
|
|
1633
|
+
source: "inline",
|
|
1634
|
+
property: declaration.property,
|
|
1635
|
+
value: declaration.value,
|
|
1636
|
+
important: declaration.important,
|
|
1637
|
+
declarationOrder: declaration.declarationOrder,
|
|
1638
|
+
selectorText: null,
|
|
1639
|
+
declarationNodeId: declaration.declarationNodeId,
|
|
1640
|
+
ruleNodeId: null,
|
|
1641
|
+
cascadeOrder: null
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
return renderSignals;
|
|
1645
|
+
}
|
|
1646
|
+
const MAX_SELECTOR_COMPILE_CACHE_SIZE = 256;
|
|
1647
|
+
const selectorCompileCache = new Map();
|
|
1648
|
+
function resolveCompiledSelectorList(selector) {
|
|
1649
|
+
if (typeof selector !== "string") {
|
|
1650
|
+
return selector;
|
|
1651
|
+
}
|
|
1652
|
+
const cached = selectorCompileCache.get(selector);
|
|
1653
|
+
if (cached) {
|
|
1654
|
+
selectorCompileCache.delete(selector);
|
|
1655
|
+
selectorCompileCache.set(selector, cached);
|
|
1656
|
+
return cached;
|
|
1657
|
+
}
|
|
1658
|
+
const compiled = compileSelectorList(selector);
|
|
1659
|
+
selectorCompileCache.set(selector, compiled);
|
|
1660
|
+
if (selectorCompileCache.size > MAX_SELECTOR_COMPILE_CACHE_SIZE) {
|
|
1661
|
+
const oldestKey = selectorCompileCache.keys().next().value;
|
|
1662
|
+
if (typeof oldestKey === "string") {
|
|
1663
|
+
selectorCompileCache.delete(oldestKey);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
return compiled;
|
|
1667
|
+
}
|
|
1668
|
+
export function matchesSelector(selector, node, root, options = {}) {
|
|
1669
|
+
const compiled = resolveCompiledSelectorList(selector);
|
|
1670
|
+
assertSelectorStrict(compiled, options.strict === true);
|
|
1671
|
+
const treeIndex = buildSelectorTreeIndex(root, options);
|
|
1672
|
+
if (!treeIndex.parentByNode.has(node)) {
|
|
1673
|
+
return false;
|
|
1674
|
+
}
|
|
1675
|
+
if (!isSelectorElementNode(node)) {
|
|
1676
|
+
return false;
|
|
1677
|
+
}
|
|
1678
|
+
for (const selectorEntry of compiled.selectors) {
|
|
1679
|
+
if (matchesCompiledSelector(selectorEntry, node, treeIndex)) {
|
|
1680
|
+
return true;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
return false;
|
|
1684
|
+
}
|
|
1685
|
+
export function querySelectorAll(selector, root, options = {}) {
|
|
1686
|
+
const compiled = resolveCompiledSelectorList(selector);
|
|
1687
|
+
assertSelectorStrict(compiled, options.strict === true);
|
|
1688
|
+
const matches = [];
|
|
1689
|
+
const seen = new Set();
|
|
1690
|
+
const stack = [
|
|
1691
|
+
{
|
|
1692
|
+
node: root,
|
|
1693
|
+
parent: null,
|
|
1694
|
+
parentElement: null
|
|
1695
|
+
}
|
|
1696
|
+
];
|
|
1697
|
+
let visited = 0;
|
|
1698
|
+
while (stack.length > 0) {
|
|
1699
|
+
const current = stack.pop();
|
|
1700
|
+
if (!current) {
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
if (seen.has(current.node)) {
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
seen.add(current.node);
|
|
1707
|
+
visited += 1;
|
|
1708
|
+
if (options.maxVisitedNodes !== undefined && visited > options.maxVisitedNodes) {
|
|
1709
|
+
throw new BudgetExceededError({
|
|
1710
|
+
code: "BUDGET_EXCEEDED",
|
|
1711
|
+
budget: "maxNodes",
|
|
1712
|
+
limit: options.maxVisitedNodes,
|
|
1713
|
+
actual: visited
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
const currentIsElement = isSelectorElementNode(current.node);
|
|
1717
|
+
if (currentIsElement) {
|
|
1718
|
+
for (const selectorEntry of compiled.selectors) {
|
|
1719
|
+
if (matchesCompiledSelectorInTraversal(selectorEntry, current)) {
|
|
1720
|
+
matches.push(current.node);
|
|
1721
|
+
break;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
const children = selectorChildrenFromNode(current.node);
|
|
1726
|
+
const parentElement = currentIsElement ? current : current.parentElement;
|
|
1727
|
+
for (let index = children.length - 1; index >= 0; index -= 1) {
|
|
1728
|
+
const child = children[index];
|
|
1729
|
+
if (!child) {
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
stack.push({
|
|
1733
|
+
node: child,
|
|
1734
|
+
parent: current,
|
|
1735
|
+
parentElement
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
return matches;
|
|
1740
|
+
}
|