@luz-ds/check 0.0.0-stage → 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 +77 -2
- package/dist/agents/luz-accessibility-reviewer.md +64 -0
- package/dist/data/app/classes.json +23 -0
- package/dist/data/cms/classes.json +35 -0
- package/dist/data/core/classes.json +345 -0
- package/dist/data/core/custom-elements.json +5 -0
- package/dist/data/docs/classes.json +47 -0
- package/dist/data/index.json +33 -0
- package/dist/data/landing/classes.json +39 -0
- package/dist/data/site/classes.json +54 -0
- package/dist/data/site/custom-elements.json +20 -0
- package/dist/data/tokens/classes.json +153 -0
- package/dist/data/tokens/tokens.css +226 -0
- package/dist/luz-check.js +3013 -0
- package/package.json +50 -3
|
@@ -0,0 +1,3013 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// packages/check/src/main.js
|
|
4
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
5
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
6
|
+
|
|
7
|
+
// packages/check/src/cli.js
|
|
8
|
+
import { lstatSync as lstatSync2, readFileSync } from "node:fs";
|
|
9
|
+
import { dirname as dirname2, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
// packages/check/src/sanitize.js
|
|
13
|
+
var UNREADABLE = "<an unreadable name>";
|
|
14
|
+
var ECHO_NAME = /^[A-Za-z0-9_:.\-/]{1,64}$/;
|
|
15
|
+
var UNSAFE = [
|
|
16
|
+
[0, 31],
|
|
17
|
+
[127, 159],
|
|
18
|
+
[173, 173],
|
|
19
|
+
[1536, 1541],
|
|
20
|
+
[1564, 1564],
|
|
21
|
+
[1757, 1757],
|
|
22
|
+
[1807, 1807],
|
|
23
|
+
[6158, 6158],
|
|
24
|
+
[8203, 8207],
|
|
25
|
+
[8232, 8238],
|
|
26
|
+
[8288, 8303],
|
|
27
|
+
[55296, 63743],
|
|
28
|
+
[65279, 65279],
|
|
29
|
+
[65529, 65531],
|
|
30
|
+
[65534, 65535],
|
|
31
|
+
[119155, 119162],
|
|
32
|
+
[917504, 921599],
|
|
33
|
+
[983040, 1114111]
|
|
34
|
+
];
|
|
35
|
+
function isUnsafe(code2) {
|
|
36
|
+
for (const [from, to] of UNSAFE) if (code2 >= from && code2 <= to) return true;
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
function echoName(value) {
|
|
40
|
+
return typeof value === "string" && ECHO_NAME.test(value) ? value : UNREADABLE;
|
|
41
|
+
}
|
|
42
|
+
function displayPath(path) {
|
|
43
|
+
let out = "";
|
|
44
|
+
for (const char of String(path)) {
|
|
45
|
+
const code2 = char.codePointAt(0);
|
|
46
|
+
out += isUnsafe(code2) || code2 === 92 ? `\\u{${code2.toString(16)}}` : char;
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function cleanText(value, max = 80) {
|
|
51
|
+
const text = String(value);
|
|
52
|
+
const chars = [...text];
|
|
53
|
+
let out = "";
|
|
54
|
+
for (let i = 0; i < chars.length; i++) {
|
|
55
|
+
const code2 = chars[i].codePointAt(0);
|
|
56
|
+
if (code2 === 27) {
|
|
57
|
+
const next = chars[i + 1];
|
|
58
|
+
if (next === "[") {
|
|
59
|
+
i += 2;
|
|
60
|
+
while (i < chars.length && !(chars[i].codePointAt(0) >= 64 && chars[i].codePointAt(0) <= 126)) i++;
|
|
61
|
+
} else if (next === "]") {
|
|
62
|
+
i += 2;
|
|
63
|
+
while (i < chars.length && chars[i] !== "\x07" && !(chars[i] === "\x1B" && chars[i + 1] === "\\")) i++;
|
|
64
|
+
if (chars[i] === "\x1B") i++;
|
|
65
|
+
} else i += 1;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!isUnsafe(code2)) out += chars[i];
|
|
69
|
+
}
|
|
70
|
+
const kept = [...out];
|
|
71
|
+
if (kept.length <= max) return out;
|
|
72
|
+
return max > 1 ? `${kept.slice(0, max - 1).join("")}\u2026` : kept.slice(0, max).join("");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// packages/check/src/messages.js
|
|
76
|
+
var MAX_MESSAGE = 120;
|
|
77
|
+
var COLOUR = /^#[0-9a-fA-F]{6}$/;
|
|
78
|
+
var MESSAGES = {
|
|
79
|
+
// ---- blocks ----
|
|
80
|
+
"parse/error": {
|
|
81
|
+
severity: "block",
|
|
82
|
+
template: "{kind} is cut short here; add the missing end of the comment, tag, string or brace"
|
|
83
|
+
},
|
|
84
|
+
"parse/error:template": {
|
|
85
|
+
severity: "warn",
|
|
86
|
+
template: "{kind} may be cut short here, or a template tag breaks it; add the missing end of the tag, comment or string"
|
|
87
|
+
},
|
|
88
|
+
"class/unknown": {
|
|
89
|
+
severity: "block",
|
|
90
|
+
template: ".{name} is not a LUZ class; the closest is .{closest}"
|
|
91
|
+
},
|
|
92
|
+
"class/unknown:none": {
|
|
93
|
+
severity: "block",
|
|
94
|
+
template: ".{name} is not a LUZ class; see classes.json of @luz-ds/{package}"
|
|
95
|
+
},
|
|
96
|
+
"class/unknown:newer": {
|
|
97
|
+
severity: "warn",
|
|
98
|
+
template: ".{name} is not in LUZ {known}, and the page loads {linked}; see classes.json of @luz-ds/{package}"
|
|
99
|
+
},
|
|
100
|
+
"element/unknown": {
|
|
101
|
+
severity: "block",
|
|
102
|
+
template: "<{tag}> is not a LUZ element; the closest is <{closest}>"
|
|
103
|
+
},
|
|
104
|
+
"element/unknown:none": {
|
|
105
|
+
severity: "block",
|
|
106
|
+
template: "<{tag}> is not a LUZ element; see custom-elements.json of @luz-ds/{package}"
|
|
107
|
+
},
|
|
108
|
+
"element/unknown-attribute": {
|
|
109
|
+
severity: "block",
|
|
110
|
+
template: "{attribute} is not an attribute of <{tag}>; the closest is {closest}"
|
|
111
|
+
},
|
|
112
|
+
"element/unknown-attribute:none": {
|
|
113
|
+
severity: "block",
|
|
114
|
+
template: "{attribute} is not an attribute of <{tag}>; see custom-elements.json for the ones it reads"
|
|
115
|
+
},
|
|
116
|
+
"load/tokens-missing": {
|
|
117
|
+
severity: "block",
|
|
118
|
+
template: "{package} is linked and @luz-ds/tokens is not; add @luz-ds/tokens before it"
|
|
119
|
+
},
|
|
120
|
+
"load/package-missing": {
|
|
121
|
+
severity: "block",
|
|
122
|
+
template: ".{name} is in @luz-ds/{package}, which this page does not load; add its stylesheet after core"
|
|
123
|
+
},
|
|
124
|
+
"load/package-missing:tag": {
|
|
125
|
+
severity: "block",
|
|
126
|
+
template: "<{tag}> is in @luz-ds/{package}, which this page does not load; add its stylesheet after core"
|
|
127
|
+
},
|
|
128
|
+
"load/layer-statement": {
|
|
129
|
+
severity: "block",
|
|
130
|
+
template: "<{tag}> declares the layers after the LUZ link; move it before the first LUZ stylesheet"
|
|
131
|
+
},
|
|
132
|
+
"misuse/host-element": {
|
|
133
|
+
severity: "block",
|
|
134
|
+
template: ".{name} is on <{tag}>; use {hosts}"
|
|
135
|
+
},
|
|
136
|
+
"misuse/modifier-alone": {
|
|
137
|
+
severity: "block",
|
|
138
|
+
template: ".{modifier} needs .{block} on the same element; add .{block}"
|
|
139
|
+
},
|
|
140
|
+
"theme/contrast": {
|
|
141
|
+
severity: "block",
|
|
142
|
+
template: "{mode}: {foreground} on {background} is {ratio}:1, needs {minimum}:1 ({kind}); change one of the two"
|
|
143
|
+
},
|
|
144
|
+
"theme/contrast:layers": {
|
|
145
|
+
severity: "block",
|
|
146
|
+
template: "{mode}: {foreground} on {background} is {ratio}:1, needs {minimum}:1; change one of the two"
|
|
147
|
+
},
|
|
148
|
+
"theme/unreadable": {
|
|
149
|
+
severity: "block",
|
|
150
|
+
template: "{token} is set to a colour the check cannot read; write the sRGB value as #rrggbb"
|
|
151
|
+
},
|
|
152
|
+
// ---- warnings ----
|
|
153
|
+
"load/order": {
|
|
154
|
+
severity: "warn",
|
|
155
|
+
template: "{first} is linked before {second}; move {second} before {first}"
|
|
156
|
+
},
|
|
157
|
+
"load/no-stylesheet": {
|
|
158
|
+
severity: "warn",
|
|
159
|
+
template: "{count} checked files load no LUZ stylesheet; add the LUZ links to the page or its layout"
|
|
160
|
+
},
|
|
161
|
+
"load/no-stylesheet:one": {
|
|
162
|
+
severity: "warn",
|
|
163
|
+
template: "{count} checked file loads no LUZ stylesheet; add the LUZ links to the page or its layout"
|
|
164
|
+
},
|
|
165
|
+
"class/unknown-in-css": {
|
|
166
|
+
severity: "warn",
|
|
167
|
+
template: ".{name} is not a LUZ class; the closest is .{closest}"
|
|
168
|
+
},
|
|
169
|
+
"class/unknown-in-css:none": {
|
|
170
|
+
severity: "warn",
|
|
171
|
+
template: ".{name} is not a LUZ class; see classes.json of @luz-ds/{package}"
|
|
172
|
+
},
|
|
173
|
+
"class/reserved-prefix": {
|
|
174
|
+
severity: "warn",
|
|
175
|
+
template: ".{name} starts with luz-, the prefix LUZ keeps for itself; change it to your own prefix"
|
|
176
|
+
},
|
|
177
|
+
"misuse/state-class": {
|
|
178
|
+
severity: "warn",
|
|
179
|
+
template: ".{name} is a state that LUZ's script sets; remove it from the markup"
|
|
180
|
+
},
|
|
181
|
+
"misuse/part-orphan": {
|
|
182
|
+
severity: "warn",
|
|
183
|
+
template: ".{name} is a part of .{block}; add an element with .{block} around it"
|
|
184
|
+
},
|
|
185
|
+
"a11y/role-on-generic": {
|
|
186
|
+
severity: "warn",
|
|
187
|
+
template: "{role} is a role set on <{tag}>; use <{element}> instead"
|
|
188
|
+
},
|
|
189
|
+
"a11y/role-on-generic:onclick": {
|
|
190
|
+
severity: "warn",
|
|
191
|
+
template: "{attribute} on <{tag}> does not work from the keyboard; use <button> instead"
|
|
192
|
+
},
|
|
193
|
+
"a11y/link-no-href": {
|
|
194
|
+
severity: "warn",
|
|
195
|
+
template: "<{tag}> has .{name} and no href; add an href, or use <button> for an action"
|
|
196
|
+
},
|
|
197
|
+
"attr/unknown-hook": {
|
|
198
|
+
severity: "warn",
|
|
199
|
+
template: "{attribute} is not a data-luz-* attribute that LUZ reads; remove it or check the spelling"
|
|
200
|
+
},
|
|
201
|
+
"css/literal-colour": {
|
|
202
|
+
severity: "warn",
|
|
203
|
+
template: "{property} is set to {value}; use a token, such as var({token})"
|
|
204
|
+
},
|
|
205
|
+
"css/literal-colour:none": {
|
|
206
|
+
severity: "warn",
|
|
207
|
+
template: "{property} is set to a literal colour; use a token, such as var({token})"
|
|
208
|
+
},
|
|
209
|
+
"css/literal-size": {
|
|
210
|
+
severity: "warn",
|
|
211
|
+
template: "{property} is {value}; use a --luz-space-*, --luz-text-* or --luz-radius-* token"
|
|
212
|
+
},
|
|
213
|
+
"css/knob-in-px": {
|
|
214
|
+
severity: "warn",
|
|
215
|
+
template: "{property} is in px, which ignores the browser's text size; use rem"
|
|
216
|
+
},
|
|
217
|
+
"css/important": {
|
|
218
|
+
severity: "warn",
|
|
219
|
+
template: "{property} is set with !important; remove it, a rule outside a layer already wins"
|
|
220
|
+
},
|
|
221
|
+
"css/private-token": {
|
|
222
|
+
severity: "warn",
|
|
223
|
+
template: "{token} belongs to one component; use a token that LUZ sets on :root"
|
|
224
|
+
},
|
|
225
|
+
"css/unknown-token": {
|
|
226
|
+
severity: "warn",
|
|
227
|
+
template: "{token} is not a LUZ token; the closest is {closest}"
|
|
228
|
+
},
|
|
229
|
+
"css/unknown-token:none": {
|
|
230
|
+
severity: "warn",
|
|
231
|
+
template: "{token} is not a LUZ token; see the token pages of the docs"
|
|
232
|
+
},
|
|
233
|
+
"file/skipped": {
|
|
234
|
+
severity: "warn",
|
|
235
|
+
template: "{reason} kept this path from being read; see luz-check --help for the limits"
|
|
236
|
+
},
|
|
237
|
+
"theme/advisory": {
|
|
238
|
+
severity: "warn",
|
|
239
|
+
template: "{source} order is used for the theme files; use --theme to set the order"
|
|
240
|
+
},
|
|
241
|
+
"theme/advisory:unplaced": {
|
|
242
|
+
severity: "warn",
|
|
243
|
+
template: "{token} is set where no colour mode can place it; move it to :root or a [data-theme] rule"
|
|
244
|
+
},
|
|
245
|
+
"theme/advisory:wide": {
|
|
246
|
+
severity: "warn",
|
|
247
|
+
template: "{token} is a wide-gamut colour in @supports, which is not measured; see the contrast docs for what is read"
|
|
248
|
+
},
|
|
249
|
+
"theme/advisory:limit": {
|
|
250
|
+
severity: "warn",
|
|
251
|
+
template: "{count} custom themes past the first {limit} are not measured; remove the ones you do not ship"
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
var SITE_URL = "https://nandovejer.github.io/luz/";
|
|
255
|
+
var AI = "docs/get-started/ai/#rules";
|
|
256
|
+
var READ = "docs/get-started/how-to-read/";
|
|
257
|
+
var DOCS = {
|
|
258
|
+
"parse/error": AI,
|
|
259
|
+
"class/unknown": `${READ}#classes`,
|
|
260
|
+
"class/unknown-in-css": `${READ}#classes`,
|
|
261
|
+
"class/reserved-prefix": `${READ}#classes`,
|
|
262
|
+
"element/unknown": `${READ}#elements`,
|
|
263
|
+
"element/unknown-attribute": `${READ}#attributes`,
|
|
264
|
+
"attr/unknown-hook": `${READ}#attributes`,
|
|
265
|
+
"load/tokens-missing": "docs/get-started/#which-level",
|
|
266
|
+
"load/package-missing": "docs/get-started/#which-level",
|
|
267
|
+
"load/no-stylesheet": "docs/get-started/#which-level",
|
|
268
|
+
"load/order": "docs/get-started/layers/#order",
|
|
269
|
+
"load/layer-statement": "docs/get-started/layers/#order",
|
|
270
|
+
"misuse/host-element": `${READ}#classes`,
|
|
271
|
+
"misuse/modifier-alone": `${READ}#classes`,
|
|
272
|
+
"misuse/state-class": `${READ}#public`,
|
|
273
|
+
"misuse/part-orphan": `${READ}#classes`,
|
|
274
|
+
"a11y/role-on-generic": AI,
|
|
275
|
+
"a11y/link-no-href": AI,
|
|
276
|
+
"css/literal-colour": `${READ}#tokens`,
|
|
277
|
+
"css/literal-size": `${READ}#tokens`,
|
|
278
|
+
"css/knob-in-px": `${READ}#tokens`,
|
|
279
|
+
"css/important": "docs/get-started/layers/#unlayered",
|
|
280
|
+
"css/private-token": `${READ}#public`,
|
|
281
|
+
"css/unknown-token": `${READ}#tokens`,
|
|
282
|
+
"file/skipped": AI,
|
|
283
|
+
"theme/contrast": "docs/tokens/contrast/#checks",
|
|
284
|
+
"theme/unreadable": "docs/tokens/contrast/#reads",
|
|
285
|
+
"theme/advisory": "docs/tokens/contrast/#reads"
|
|
286
|
+
};
|
|
287
|
+
function docsUrl(rule) {
|
|
288
|
+
const base = String(rule).split(":")[0];
|
|
289
|
+
if (!Object.hasOwn(DOCS, base)) throw new Error(`no docs for the rule ${echoName(base)}`);
|
|
290
|
+
return `${SITE_URL}${DOCS[base]}`;
|
|
291
|
+
}
|
|
292
|
+
var SPLIT = /* @__PURE__ */ new Map();
|
|
293
|
+
function echoed(value) {
|
|
294
|
+
if (Array.isArray(value)) return value.map((item) => `<${echoName(item)}>`).join(", ");
|
|
295
|
+
const text = String(value);
|
|
296
|
+
if (COLOUR.test(text)) return text.toLowerCase();
|
|
297
|
+
return echoName(text);
|
|
298
|
+
}
|
|
299
|
+
function fit(values, room) {
|
|
300
|
+
let limit = Math.max(...values.map((value) => value.length));
|
|
301
|
+
while (limit > 2 && values.reduce((sum, value) => sum + Math.min(value.length, limit), 0) > room) limit--;
|
|
302
|
+
return values.map((value) => value.length > limit ? `${value.slice(0, limit - 1)}\u2026` : value);
|
|
303
|
+
}
|
|
304
|
+
function message(key, vars) {
|
|
305
|
+
if (typeof key !== "string" || !Object.hasOwn(MESSAGES, key)) throw new Error(`no message for the key ${echoName(key)}`);
|
|
306
|
+
const given = vars && typeof vars === "object" ? vars : {};
|
|
307
|
+
const one = `${key}:one`;
|
|
308
|
+
const chosen = given.count === "1" && Object.hasOwn(MESSAGES, one) ? one : key;
|
|
309
|
+
let known = SPLIT.get(chosen);
|
|
310
|
+
if (known === void 0) {
|
|
311
|
+
const parts2 = MESSAGES[chosen].template.split(/\{([A-Za-z][A-Za-z0-9]*)\}/);
|
|
312
|
+
known = { parts: parts2, fixed: parts2.reduce((sum, text, i) => i % 2 === 0 ? sum + text.length : sum, 0) };
|
|
313
|
+
SPLIT.set(chosen, known);
|
|
314
|
+
}
|
|
315
|
+
const { parts, fixed } = known;
|
|
316
|
+
let values = [];
|
|
317
|
+
for (let i = 1; i < parts.length; i += 2) {
|
|
318
|
+
const name = parts[i];
|
|
319
|
+
if (!Object.hasOwn(given, name) || given[name] === void 0 || given[name] === null) throw new Error(`the message ${key} needs the variable ${name}`);
|
|
320
|
+
values.push(echoed(given[name]));
|
|
321
|
+
}
|
|
322
|
+
if (fixed + values.reduce((sum, value) => sum + value.length, 0) > MAX_MESSAGE) values = fit(values, MAX_MESSAGE - fixed);
|
|
323
|
+
let out = "";
|
|
324
|
+
for (let i = 0; i < parts.length; i++) out += i % 2 === 0 ? parts[i] : values[(i - 1) / 2];
|
|
325
|
+
return out.length > MAX_MESSAGE ? `${out.slice(0, MAX_MESSAGE - 1)}\u2026` : out;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// packages/check/src/config.js
|
|
329
|
+
var MAX_CONFIG_BYTES = 65536;
|
|
330
|
+
var MAX_LIST = 1e3;
|
|
331
|
+
var KEYS = ["ignore", "rules", "themes", "strict"];
|
|
332
|
+
var ALWAYS_ON = /* @__PURE__ */ new Set(["file/skipped"]);
|
|
333
|
+
var OFFABLE = new Set(
|
|
334
|
+
Object.entries(MESSAGES).filter(([key, entry]) => entry.severity === "warn" && !key.includes(":") && !ALWAYS_ON.has(key)).map(([key]) => key)
|
|
335
|
+
);
|
|
336
|
+
var KNOWN = new Set(Object.keys(MESSAGES).map((key) => key.split(":")[0]));
|
|
337
|
+
var ConfigError = class extends Error {
|
|
338
|
+
constructor(file, message2) {
|
|
339
|
+
super(`${displayPath(file)}: ${message2}`);
|
|
340
|
+
this.name = "ConfigError";
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
var isList = (value) => Array.isArray(value) && value.length <= MAX_LIST && value.every((item) => typeof item === "string");
|
|
344
|
+
function parseConfig(text, file = "luz-check.json") {
|
|
345
|
+
const fail = (message2) => {
|
|
346
|
+
throw new ConfigError(file, message2);
|
|
347
|
+
};
|
|
348
|
+
if (typeof text !== "string") fail("is not text");
|
|
349
|
+
if (Buffer.byteLength(text, "utf-8") > MAX_CONFIG_BYTES) fail("is larger than 64 kB");
|
|
350
|
+
let data;
|
|
351
|
+
try {
|
|
352
|
+
data = JSON.parse(text);
|
|
353
|
+
} catch {
|
|
354
|
+
return fail("is not valid JSON");
|
|
355
|
+
}
|
|
356
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) return fail("must be a JSON object");
|
|
357
|
+
const result = { ignore: [], rules: /* @__PURE__ */ Object.create(null), themes: [], strict: false };
|
|
358
|
+
for (const key of Object.keys(data)) {
|
|
359
|
+
if (!KEYS.includes(key)) fail(`has a key luz-check does not know: ${echoName(key)}`);
|
|
360
|
+
}
|
|
361
|
+
if (Object.hasOwn(data, "ignore")) {
|
|
362
|
+
if (!isList(data.ignore)) fail(`"ignore" must be a list of patterns`);
|
|
363
|
+
result.ignore = data.ignore.slice();
|
|
364
|
+
}
|
|
365
|
+
if (Object.hasOwn(data, "themes")) {
|
|
366
|
+
if (!isList(data.themes)) fail(`"themes" must be a list of files`);
|
|
367
|
+
result.themes = data.themes.slice();
|
|
368
|
+
}
|
|
369
|
+
if (Object.hasOwn(data, "strict")) {
|
|
370
|
+
if (typeof data.strict !== "boolean") fail('"strict" must be true or false');
|
|
371
|
+
result.strict = data.strict;
|
|
372
|
+
}
|
|
373
|
+
if (Object.hasOwn(data, "rules")) {
|
|
374
|
+
const { rules } = data;
|
|
375
|
+
if (rules === null || typeof rules !== "object" || Array.isArray(rules)) fail('"rules" must be an object of rule ids');
|
|
376
|
+
for (const id of Object.keys(rules)) {
|
|
377
|
+
const shown2 = echoName(id);
|
|
378
|
+
if (!KNOWN.has(id)) fail(`"rules" names a rule that does not exist: ${shown2}`);
|
|
379
|
+
if (!OFFABLE.has(id)) fail(`"rules" cannot switch off ${shown2}; a blocking rule is silenced with "ignore" only`);
|
|
380
|
+
if (rules[id] !== "off") fail(`"rules" may set ${shown2} to "off" and nothing else`);
|
|
381
|
+
result.rules[id] = "off";
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return result;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// packages/check/src/html.js
|
|
388
|
+
var VOID = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"]);
|
|
389
|
+
var RAW_TEXT_END = {
|
|
390
|
+
script: /<\/script/gi,
|
|
391
|
+
style: /<\/style/gi,
|
|
392
|
+
textarea: /<\/textarea/gi,
|
|
393
|
+
title: /<\/title/gi
|
|
394
|
+
};
|
|
395
|
+
var FOREIGN = /* @__PURE__ */ new Set(["svg", "math"]);
|
|
396
|
+
var MAX_DEPTH = 256;
|
|
397
|
+
var ATTR = /([^\s"'>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
398
|
+
var ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: "\xA0" };
|
|
399
|
+
var NO_PROBLEMS = Object.freeze([]);
|
|
400
|
+
function decodeEntities(text) {
|
|
401
|
+
return text.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (whole, ref) => {
|
|
402
|
+
if (ref[0] === "#") {
|
|
403
|
+
const code2 = ref[1] === "x" || ref[1] === "X" ? parseInt(ref.slice(2), 16) : parseInt(ref.slice(1), 10);
|
|
404
|
+
return Number.isFinite(code2) && code2 <= 1114111 ? String.fromCodePoint(code2) : whole;
|
|
405
|
+
}
|
|
406
|
+
return ENTITIES[ref.toLowerCase()] ?? whole;
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
var HtmlError = class extends Error {
|
|
410
|
+
constructor(file, line, message2) {
|
|
411
|
+
super(`${file}:${line}: ${message2}`);
|
|
412
|
+
this.name = "HtmlError";
|
|
413
|
+
this.file = file;
|
|
414
|
+
this.line = line;
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
function lineStarts(html) {
|
|
418
|
+
const starts = [0];
|
|
419
|
+
for (let i = html.indexOf("\n"); i !== -1; i = html.indexOf("\n", i + 1)) starts.push(i + 1);
|
|
420
|
+
return starts;
|
|
421
|
+
}
|
|
422
|
+
function lineAt(starts, index) {
|
|
423
|
+
let lo = 0;
|
|
424
|
+
let hi = starts.length - 1;
|
|
425
|
+
while (lo < hi) {
|
|
426
|
+
const mid = lo + hi + 1 >> 1;
|
|
427
|
+
if (starts[mid] <= index) lo = mid;
|
|
428
|
+
else hi = mid - 1;
|
|
429
|
+
}
|
|
430
|
+
return lo + 1;
|
|
431
|
+
}
|
|
432
|
+
function* tokenize(html, file, lenient, detail) {
|
|
433
|
+
const starts = lineStarts(html);
|
|
434
|
+
const length = html.length;
|
|
435
|
+
let i = 0;
|
|
436
|
+
const problem = (index, message2) => {
|
|
437
|
+
const line = lineAt(starts, index);
|
|
438
|
+
if (!lenient) throw new HtmlError(file, line, message2);
|
|
439
|
+
return { type: "error", line, column: index - starts[line - 1] + 1, message: message2 };
|
|
440
|
+
};
|
|
441
|
+
while (i < length) {
|
|
442
|
+
const lt = html.indexOf("<", i);
|
|
443
|
+
if (lt === -1) return;
|
|
444
|
+
if (html.startsWith("<!--", lt)) {
|
|
445
|
+
const close = html.indexOf("-->", lt + 4);
|
|
446
|
+
if (close === -1) {
|
|
447
|
+
yield problem(lt, "comment is never closed");
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
i = close + 3;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (html[lt + 1] === "!" || html[lt + 1] === "?") {
|
|
454
|
+
const close = html.indexOf(">", lt);
|
|
455
|
+
i = close === -1 ? length : close + 1;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const line = lineAt(starts, lt);
|
|
459
|
+
const column = lt - starts[line - 1] + 1;
|
|
460
|
+
const endTag = /^<\/([a-zA-Z][a-zA-Z0-9-]*)\s*>/.exec(html.slice(lt, lt + 80));
|
|
461
|
+
if (endTag) {
|
|
462
|
+
const end = lt + endTag[0].length;
|
|
463
|
+
yield { type: "end", name: endTag[1].toLowerCase(), attrs: /* @__PURE__ */ new Map(), selfClosing: false, line, column, index: lt, end };
|
|
464
|
+
i = end;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const open = /^<([a-zA-Z][a-zA-Z0-9-]*)/.exec(html.slice(lt, lt + 80));
|
|
468
|
+
if (!open) {
|
|
469
|
+
i = lt + 1;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const nameEnd = lt + open[0].length;
|
|
473
|
+
let j = nameEnd;
|
|
474
|
+
let quote = null;
|
|
475
|
+
for (; j < length; j++) {
|
|
476
|
+
const c = html[j];
|
|
477
|
+
if (quote) {
|
|
478
|
+
if (c === quote) quote = null;
|
|
479
|
+
} else if (c === '"' || c === "'") quote = c;
|
|
480
|
+
else if (c === ">") break;
|
|
481
|
+
}
|
|
482
|
+
if (j >= length) {
|
|
483
|
+
yield problem(lt, `<${open[1]}> start tag is never closed with ">"`);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const inside2 = html.slice(nameEnd, j);
|
|
487
|
+
const selfClosing = /\/\s*$/.test(inside2);
|
|
488
|
+
const attrs = /* @__PURE__ */ new Map();
|
|
489
|
+
let problems = NO_PROBLEMS;
|
|
490
|
+
for (const m of inside2.replace(/\/\s*$/, "").matchAll(ATTR)) {
|
|
491
|
+
const attrName = m[1].toLowerCase();
|
|
492
|
+
const at2 = detail || lenient ? nameEnd + m.index : 0;
|
|
493
|
+
if (attrs.has(attrName)) {
|
|
494
|
+
if (!lenient) throw new HtmlError(file, line, `<${open[1]}> has the attribute "${attrName}" twice`);
|
|
495
|
+
const dupLine = lineAt(starts, at2);
|
|
496
|
+
if (problems === NO_PROBLEMS) problems = [];
|
|
497
|
+
problems.push({ code: "duplicate-attribute", name: attrName, line: dupLine, column: at2 - starts[dupLine - 1] + 1 });
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
const value = decodeEntities(m[2] ?? m[3] ?? m[4] ?? "");
|
|
501
|
+
if (detail) {
|
|
502
|
+
const attrLine = lineAt(starts, at2);
|
|
503
|
+
attrs.set(attrName, { value, line: attrLine, column: at2 - starts[attrLine - 1] + 1 });
|
|
504
|
+
} else attrs.set(attrName, value);
|
|
505
|
+
}
|
|
506
|
+
const name = open[1].toLowerCase();
|
|
507
|
+
const token = { type: "start", name, written: open[1], attrs, selfClosing, line, column, index: lt, end: j + 1 };
|
|
508
|
+
if (lenient) token.problems = problems;
|
|
509
|
+
i = j + 1;
|
|
510
|
+
if (RAW_TEXT_END[name] && !selfClosing) {
|
|
511
|
+
const re = RAW_TEXT_END[name];
|
|
512
|
+
re.lastIndex = i;
|
|
513
|
+
const found = re.exec(html);
|
|
514
|
+
if (detail) token.text = html.slice(i, found ? found.index : length);
|
|
515
|
+
yield token;
|
|
516
|
+
if (!found) {
|
|
517
|
+
yield problem(lt, `<${name}> is never closed`);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
i = found.index;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
yield token;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function* scan(html, file = "<html>") {
|
|
527
|
+
const stack = [];
|
|
528
|
+
const owners = [];
|
|
529
|
+
let foreign = 0;
|
|
530
|
+
for (const token of tokenize(html, file, true, true)) {
|
|
531
|
+
if (token.type === "error") {
|
|
532
|
+
yield { type: "error", line: token.line, column: token.column, message: token.message };
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const { name } = token;
|
|
536
|
+
if (token.type === "start") {
|
|
537
|
+
const parent = owners.length > 0 ? owners[owners.length - 1] : null;
|
|
538
|
+
const record = {
|
|
539
|
+
type: "element",
|
|
540
|
+
name,
|
|
541
|
+
written: token.written,
|
|
542
|
+
attrs: token.attrs,
|
|
543
|
+
parentChain: stack.slice(),
|
|
544
|
+
parent,
|
|
545
|
+
inert: token.attrs.has("inert") || parent !== null && parent.inert,
|
|
546
|
+
selfClosing: token.selfClosing,
|
|
547
|
+
line: token.line,
|
|
548
|
+
column: token.column,
|
|
549
|
+
problems: token.problems
|
|
550
|
+
};
|
|
551
|
+
if (token.text !== void 0) record.text = token.text;
|
|
552
|
+
yield record;
|
|
553
|
+
if (VOID.has(name) || stack.length >= MAX_DEPTH) continue;
|
|
554
|
+
if (token.selfClosing && (foreign > 0 || FOREIGN.has(name))) continue;
|
|
555
|
+
stack.push(name);
|
|
556
|
+
owners.push(record);
|
|
557
|
+
if (FOREIGN.has(name)) foreign++;
|
|
558
|
+
} else {
|
|
559
|
+
const at2 = stack.lastIndexOf(name);
|
|
560
|
+
if (at2 === -1) continue;
|
|
561
|
+
owners.length = at2;
|
|
562
|
+
while (stack.length > at2) if (FOREIGN.has(stack.pop())) foreign--;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// packages/check/src/css.js
|
|
568
|
+
var MAX_DEPTH2 = 64;
|
|
569
|
+
var MAX_FINDINGS = 100;
|
|
570
|
+
var CONDITIONAL = /* @__PURE__ */ new Set(["media", "supports", "container", "scope", "starting-style", "document", "layer"]);
|
|
571
|
+
var STATEMENTS = /* @__PURE__ */ new Set(["import", "charset", "namespace", "layer"]);
|
|
572
|
+
var DECLARATIONS = /* @__PURE__ */ new Set(["font-face", "property", "counter-style", "page", "font-feature-values", "font-palette-values", "view-transition", "position-try", "color-profile"]);
|
|
573
|
+
var MARGIN_BOX = /^(?:top|bottom|left|right)-/;
|
|
574
|
+
var TRANSPARENT = /* @__PURE__ */ new Set(["is", "where", "matches", "any"]);
|
|
575
|
+
var CssError = class extends Error {
|
|
576
|
+
constructor(file, line, column, message2) {
|
|
577
|
+
super(`${file}:${line}:${column}: ${message2}`);
|
|
578
|
+
this.name = "CssError";
|
|
579
|
+
this.file = file;
|
|
580
|
+
this.line = line;
|
|
581
|
+
this.column = column;
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
var Halt = class extends Error {
|
|
585
|
+
};
|
|
586
|
+
var TAB = 9;
|
|
587
|
+
var LF = 10;
|
|
588
|
+
var FF = 12;
|
|
589
|
+
var CR = 13;
|
|
590
|
+
var SPACE = 32;
|
|
591
|
+
var QUOTE2 = 34;
|
|
592
|
+
var QUOTE1 = 39;
|
|
593
|
+
var OPEN_PAREN = 40;
|
|
594
|
+
var CLOSE_PAREN = 41;
|
|
595
|
+
var STAR = 42;
|
|
596
|
+
var PLUS = 43;
|
|
597
|
+
var COMMA = 44;
|
|
598
|
+
var MINUS = 45;
|
|
599
|
+
var DOT = 46;
|
|
600
|
+
var SLASH = 47;
|
|
601
|
+
var COLON = 58;
|
|
602
|
+
var SEMI = 59;
|
|
603
|
+
var GT = 62;
|
|
604
|
+
var AT = 64;
|
|
605
|
+
var OPEN_BRACKET = 91;
|
|
606
|
+
var BACKSLASH = 92;
|
|
607
|
+
var CLOSE_BRACKET = 93;
|
|
608
|
+
var UNDERSCORE = 95;
|
|
609
|
+
var OPEN_BRACE = 123;
|
|
610
|
+
var PIPE = 124;
|
|
611
|
+
var CLOSE_BRACE = 125;
|
|
612
|
+
var TILDE = 126;
|
|
613
|
+
var AMP = 38;
|
|
614
|
+
var HASH = 35;
|
|
615
|
+
var isDigit = (c) => c >= 48 && c <= 57;
|
|
616
|
+
var isHex = (c) => isDigit(c) || c >= 65 && c <= 70 || c >= 97 && c <= 102;
|
|
617
|
+
var isIdent = (c) => c >= 97 && c <= 122 || c >= 65 && c <= 90 || isDigit(c) || c === MINUS || c === UNDERSCORE || c >= 128;
|
|
618
|
+
var isSpace = (c) => c === SPACE || c === TAB || c === LF || c === CR || c === FF;
|
|
619
|
+
var UNESCAPE = /\\([0-9a-fA-F]{1,6})(?:\r\n|[ \t\n\r\f])?|\\([\s\S])/g;
|
|
620
|
+
function unescapeCss(text) {
|
|
621
|
+
return text.replace(UNESCAPE, (_, hex, other) => {
|
|
622
|
+
if (other !== void 0) return other;
|
|
623
|
+
const code2 = parseInt(hex, 16);
|
|
624
|
+
return code2 === 0 || code2 > 1114111 || code2 >= 55296 && code2 <= 57343 ? "\uFFFD" : String.fromCodePoint(code2);
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
function readCss(text, file = "<css>", { strict = false } = {}) {
|
|
628
|
+
const starts = lineStarts(text);
|
|
629
|
+
const length = text.length;
|
|
630
|
+
const classes = /* @__PURE__ */ new Map();
|
|
631
|
+
const properties = /* @__PURE__ */ new Map();
|
|
632
|
+
const findings = [];
|
|
633
|
+
const declarations = [];
|
|
634
|
+
const HALT = new Halt();
|
|
635
|
+
const DECLS = { kind: "decls", layer: null, sel: null };
|
|
636
|
+
let pos = 0;
|
|
637
|
+
let depth = 0;
|
|
638
|
+
let eofReported = false;
|
|
639
|
+
function report(index, message2) {
|
|
640
|
+
const line = lineAt(starts, index);
|
|
641
|
+
const column = index - starts[line - 1] + 1;
|
|
642
|
+
if (strict) throw new CssError(file, line, column, message2);
|
|
643
|
+
findings.push({ rule: "parse/error", line, column, message: message2 });
|
|
644
|
+
if (findings.length >= MAX_FINDINGS) throw HALT;
|
|
645
|
+
}
|
|
646
|
+
function fatal(index, message2) {
|
|
647
|
+
report(index, message2);
|
|
648
|
+
throw HALT;
|
|
649
|
+
}
|
|
650
|
+
function skipComment(p) {
|
|
651
|
+
const close = text.indexOf("*/", p + 2);
|
|
652
|
+
if (close === -1) {
|
|
653
|
+
report(p, "comment is never closed");
|
|
654
|
+
return length;
|
|
655
|
+
}
|
|
656
|
+
return close + 2;
|
|
657
|
+
}
|
|
658
|
+
function skipString(p) {
|
|
659
|
+
const quote = text.charCodeAt(p);
|
|
660
|
+
for (let i = p + 1; i < length; i++) {
|
|
661
|
+
const c = text.charCodeAt(i);
|
|
662
|
+
if (c === BACKSLASH) {
|
|
663
|
+
if (text.charCodeAt(i + 1) === CR && text.charCodeAt(i + 2) === LF) i += 2;
|
|
664
|
+
else i++;
|
|
665
|
+
} else if (c === quote) return i + 1;
|
|
666
|
+
else if (c === LF || c === CR || c === FF) {
|
|
667
|
+
report(p, "string is never closed");
|
|
668
|
+
return i;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
report(p, "string is never closed");
|
|
672
|
+
return length;
|
|
673
|
+
}
|
|
674
|
+
function identEnd(p) {
|
|
675
|
+
let i = p;
|
|
676
|
+
while (i < length) {
|
|
677
|
+
const c = text.charCodeAt(i);
|
|
678
|
+
if (c === BACKSLASH) {
|
|
679
|
+
const next = text.charCodeAt(i + 1);
|
|
680
|
+
if (Number.isNaN(next) || next === LF || next === CR || next === FF) break;
|
|
681
|
+
i++;
|
|
682
|
+
if (isHex(next)) {
|
|
683
|
+
let n = 1;
|
|
684
|
+
while (n < 6 && isHex(text.charCodeAt(i + 1))) {
|
|
685
|
+
i++;
|
|
686
|
+
n++;
|
|
687
|
+
}
|
|
688
|
+
i++;
|
|
689
|
+
if (text.charCodeAt(i) === CR && text.charCodeAt(i + 1) === LF) i++;
|
|
690
|
+
if (isSpace(text.charCodeAt(i))) i++;
|
|
691
|
+
} else i++;
|
|
692
|
+
} else if (isIdent(c)) i++;
|
|
693
|
+
else break;
|
|
694
|
+
}
|
|
695
|
+
return i;
|
|
696
|
+
}
|
|
697
|
+
const identText = (a, b) => {
|
|
698
|
+
const raw = text.slice(a, b);
|
|
699
|
+
return raw.includes("\\") ? unescapeCss(raw) : raw;
|
|
700
|
+
};
|
|
701
|
+
function isRawUrl(p) {
|
|
702
|
+
if (p < 3) return false;
|
|
703
|
+
if ((text.charCodeAt(p - 1) | 32) !== 108 || (text.charCodeAt(p - 2) | 32) !== 114 || (text.charCodeAt(p - 3) | 32) !== 117) return false;
|
|
704
|
+
if (p > 3 && isIdent(text.charCodeAt(p - 4))) return false;
|
|
705
|
+
let i = p + 1;
|
|
706
|
+
while (i < length && isSpace(text.charCodeAt(i))) i++;
|
|
707
|
+
const c = text.charCodeAt(i);
|
|
708
|
+
return c !== QUOTE1 && c !== QUOTE2;
|
|
709
|
+
}
|
|
710
|
+
function skipRawUrl(p) {
|
|
711
|
+
for (let i = p + 1; i < length; i++) {
|
|
712
|
+
const c = text.charCodeAt(i);
|
|
713
|
+
if (c === BACKSLASH) i++;
|
|
714
|
+
else if (c === CLOSE_PAREN) return i + 1;
|
|
715
|
+
}
|
|
716
|
+
report(p, "parenthesis is never closed");
|
|
717
|
+
return length;
|
|
718
|
+
}
|
|
719
|
+
function scanUntil(from, braces) {
|
|
720
|
+
const closers = [];
|
|
721
|
+
const opened = [];
|
|
722
|
+
const open = (closer, at2) => {
|
|
723
|
+
if (closers.length >= MAX_DEPTH2) fatal(at2, "brackets are nested too deep");
|
|
724
|
+
closers.push(closer);
|
|
725
|
+
opened.push(at2);
|
|
726
|
+
};
|
|
727
|
+
const unclosed = () => {
|
|
728
|
+
const closer = closers[closers.length - 1];
|
|
729
|
+
const what = closer === CLOSE_PAREN ? "parenthesis" : closer === CLOSE_BRACKET ? "bracket" : "brace";
|
|
730
|
+
report(opened[opened.length - 1], `${what} is never closed`);
|
|
731
|
+
closers.length = 0;
|
|
732
|
+
opened.length = 0;
|
|
733
|
+
};
|
|
734
|
+
let p = from;
|
|
735
|
+
while (p < length) {
|
|
736
|
+
const c = text.charCodeAt(p);
|
|
737
|
+
switch (c) {
|
|
738
|
+
case SLASH:
|
|
739
|
+
p = text.charCodeAt(p + 1) === STAR ? skipComment(p) : p + 1;
|
|
740
|
+
break;
|
|
741
|
+
case QUOTE1:
|
|
742
|
+
case QUOTE2:
|
|
743
|
+
p = skipString(p);
|
|
744
|
+
break;
|
|
745
|
+
case BACKSLASH:
|
|
746
|
+
p += 2;
|
|
747
|
+
break;
|
|
748
|
+
case OPEN_PAREN:
|
|
749
|
+
if (isRawUrl(p)) p = skipRawUrl(p);
|
|
750
|
+
else {
|
|
751
|
+
open(CLOSE_PAREN, p);
|
|
752
|
+
p++;
|
|
753
|
+
}
|
|
754
|
+
break;
|
|
755
|
+
case OPEN_BRACKET:
|
|
756
|
+
open(CLOSE_BRACKET, p);
|
|
757
|
+
p++;
|
|
758
|
+
break;
|
|
759
|
+
case CLOSE_PAREN:
|
|
760
|
+
case CLOSE_BRACKET:
|
|
761
|
+
if (closers.length && closers[closers.length - 1] === c) {
|
|
762
|
+
closers.pop();
|
|
763
|
+
opened.pop();
|
|
764
|
+
} else report(p, c === CLOSE_PAREN ? "unmatched closing parenthesis" : "unmatched closing bracket");
|
|
765
|
+
p++;
|
|
766
|
+
break;
|
|
767
|
+
case OPEN_BRACE:
|
|
768
|
+
if (braces) {
|
|
769
|
+
open(CLOSE_BRACE, p);
|
|
770
|
+
p++;
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
if (closers.length) unclosed();
|
|
774
|
+
pos = p;
|
|
775
|
+
return c;
|
|
776
|
+
case CLOSE_BRACE:
|
|
777
|
+
if (braces && closers.length && closers[closers.length - 1] === CLOSE_BRACE) {
|
|
778
|
+
closers.pop();
|
|
779
|
+
opened.pop();
|
|
780
|
+
p++;
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
if (closers.length) unclosed();
|
|
784
|
+
pos = p;
|
|
785
|
+
return c;
|
|
786
|
+
case SEMI:
|
|
787
|
+
if (closers.length === 0) {
|
|
788
|
+
pos = p;
|
|
789
|
+
return c;
|
|
790
|
+
}
|
|
791
|
+
p++;
|
|
792
|
+
break;
|
|
793
|
+
default:
|
|
794
|
+
p++;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (closers.length) unclosed();
|
|
798
|
+
pos = length;
|
|
799
|
+
return -1;
|
|
800
|
+
}
|
|
801
|
+
function readList(from, to, inParen) {
|
|
802
|
+
const list = [];
|
|
803
|
+
let p = from;
|
|
804
|
+
for (; ; ) {
|
|
805
|
+
const complex = readComplex(p, to, inParen);
|
|
806
|
+
list.push(complex.value);
|
|
807
|
+
p = complex.next;
|
|
808
|
+
if (p < to && text.charCodeAt(p) === COMMA) {
|
|
809
|
+
p++;
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
return { list, next: p };
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
function readComplex(from, to, inParen) {
|
|
816
|
+
const items = [];
|
|
817
|
+
let compounds = 0;
|
|
818
|
+
let inCompound = false;
|
|
819
|
+
let amp = false;
|
|
820
|
+
let mark = false;
|
|
821
|
+
let plain = true;
|
|
822
|
+
let p = from;
|
|
823
|
+
while (p < to) {
|
|
824
|
+
const c = text.charCodeAt(p);
|
|
825
|
+
if (c === COMMA) break;
|
|
826
|
+
if (c === CLOSE_PAREN) {
|
|
827
|
+
if (inParen) break;
|
|
828
|
+
p++;
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (isSpace(c)) {
|
|
832
|
+
inCompound = false;
|
|
833
|
+
p++;
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
if (c === GT || c === PLUS || c === TILDE) {
|
|
837
|
+
inCompound = false;
|
|
838
|
+
p++;
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
if (c === PIPE && text.charCodeAt(p + 1) === PIPE) {
|
|
842
|
+
inCompound = false;
|
|
843
|
+
p += 2;
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
if (c === SLASH && text.charCodeAt(p + 1) === STAR) {
|
|
847
|
+
const close = text.indexOf("*/", p + 2);
|
|
848
|
+
p = close === -1 || close + 2 > to ? to : close + 2;
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (!inCompound) {
|
|
852
|
+
compounds++;
|
|
853
|
+
inCompound = true;
|
|
854
|
+
}
|
|
855
|
+
if (c === DOT) {
|
|
856
|
+
const end = identEnd(p + 1);
|
|
857
|
+
if (end > p + 1) items.push({ name: identText(p + 1, end), index: p, ok: true });
|
|
858
|
+
plain = false;
|
|
859
|
+
p = Math.max(end, p + 1);
|
|
860
|
+
} else if (c === HASH) {
|
|
861
|
+
plain = false;
|
|
862
|
+
p = Math.max(identEnd(p + 1), p + 1);
|
|
863
|
+
} else if (c === OPEN_BRACKET) {
|
|
864
|
+
const end = identEnd(p + 1);
|
|
865
|
+
if (text.startsWith("data-theme", p + 1) && end - (p + 1) >= 10) mark = true;
|
|
866
|
+
p = skipBracket(p, to);
|
|
867
|
+
} else if (c === COLON) {
|
|
868
|
+
p++;
|
|
869
|
+
if (text.charCodeAt(p) === COLON) p++;
|
|
870
|
+
const end = identEnd(p);
|
|
871
|
+
const name = text.slice(p, end).toLowerCase().replace(/^-(?:webkit|moz)-/, "");
|
|
872
|
+
p = Math.max(end, p);
|
|
873
|
+
if (text.charCodeAt(p) === OPEN_PAREN && p < to) {
|
|
874
|
+
const inner = readList(p + 1, to, true);
|
|
875
|
+
p = inner.next;
|
|
876
|
+
if (text.charCodeAt(p) === CLOSE_PAREN) p++;
|
|
877
|
+
const through = TRANSPARENT.has(name);
|
|
878
|
+
for (const complex of inner.list) {
|
|
879
|
+
for (const item of complex.items) items.push({ name: item.name, index: item.index, ok: through && item.ok && complex.compounds <= 1 });
|
|
880
|
+
}
|
|
881
|
+
} else if (name === "root") mark = true;
|
|
882
|
+
} else if (c === OPEN_PAREN) {
|
|
883
|
+
const inner = readList(p + 1, to, true);
|
|
884
|
+
p = inner.next;
|
|
885
|
+
if (text.charCodeAt(p) === CLOSE_PAREN) p++;
|
|
886
|
+
for (const complex of inner.list) for (const item of complex.items) items.push({ name: item.name, index: item.index, ok: false });
|
|
887
|
+
} else if (c === AMP) {
|
|
888
|
+
amp = true;
|
|
889
|
+
p++;
|
|
890
|
+
} else if (c === QUOTE1 || c === QUOTE2) {
|
|
891
|
+
p = skipQuoted(p, to);
|
|
892
|
+
} else if (isIdent(c) || c === BACKSLASH) {
|
|
893
|
+
const end = identEnd(p);
|
|
894
|
+
if (end > p && text.slice(p, end).toLowerCase() === "html") mark = true;
|
|
895
|
+
p = Math.max(end, p + 1);
|
|
896
|
+
} else p++;
|
|
897
|
+
}
|
|
898
|
+
return { value: { compounds, amp, root: mark && plain && compounds === 1, items }, next: p };
|
|
899
|
+
}
|
|
900
|
+
function skipBracket(p, to) {
|
|
901
|
+
for (let i = p + 1; i < to; i++) {
|
|
902
|
+
const c = text.charCodeAt(i);
|
|
903
|
+
if (c === BACKSLASH) i++;
|
|
904
|
+
else if (c === QUOTE1 || c === QUOTE2) i = skipQuoted(i, to) - 1;
|
|
905
|
+
else if (c === CLOSE_BRACKET) return i + 1;
|
|
906
|
+
}
|
|
907
|
+
return to;
|
|
908
|
+
}
|
|
909
|
+
function skipQuoted(p, to) {
|
|
910
|
+
const quote = text.charCodeAt(p);
|
|
911
|
+
for (let i = p + 1; i < to; i++) {
|
|
912
|
+
const c = text.charCodeAt(i);
|
|
913
|
+
if (c === BACKSLASH) i++;
|
|
914
|
+
else if (c === quote) return i + 1;
|
|
915
|
+
}
|
|
916
|
+
return to;
|
|
917
|
+
}
|
|
918
|
+
function addClass(name, index, defined, layer) {
|
|
919
|
+
const known = classes.get(name);
|
|
920
|
+
if (known === void 0) {
|
|
921
|
+
const line = lineAt(starts, index);
|
|
922
|
+
classes.set(name, { name, defined, layer, line, column: index - starts[line - 1] + 1 });
|
|
923
|
+
} else if (defined && !known.defined) {
|
|
924
|
+
known.defined = true;
|
|
925
|
+
known.layer = layer;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
function addProperty(name, index, ctx) {
|
|
929
|
+
const scope = ctx.sel !== null && ctx.sel.root ? "root" : "component";
|
|
930
|
+
const known = properties.get(name);
|
|
931
|
+
if (known === void 0 || scope === "root" && known.scope !== "root") {
|
|
932
|
+
const line = lineAt(starts, index);
|
|
933
|
+
properties.set(name, { name, scope, layer: ctx.layer, line, column: index - starts[line - 1] + 1 });
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
function styleRule(ctx, from, to) {
|
|
937
|
+
const { list } = readList(from, to, false);
|
|
938
|
+
const nested = ctx.kind === "style";
|
|
939
|
+
let single = false;
|
|
940
|
+
let root = false;
|
|
941
|
+
for (const complex of list) {
|
|
942
|
+
const own = nested ? complex.compounds === 1 && complex.amp && ctx.sel.single : complex.compounds === 1;
|
|
943
|
+
if (own) single = true;
|
|
944
|
+
if (complex.root || nested && complex.amp && complex.compounds === 1 && ctx.sel.root) root = true;
|
|
945
|
+
for (const item of complex.items) addClass(item.name, item.index, own && item.ok, ctx.layer);
|
|
946
|
+
}
|
|
947
|
+
return { kind: "style", layer: ctx.layer, sel: { single, root } };
|
|
948
|
+
}
|
|
949
|
+
function skipTrivia() {
|
|
950
|
+
while (pos < length) {
|
|
951
|
+
const c = text.charCodeAt(pos);
|
|
952
|
+
if (isSpace(c) || c === 65279) pos++;
|
|
953
|
+
else if (c === SLASH && text.charCodeAt(pos + 1) === STAR) pos = skipComment(pos);
|
|
954
|
+
else break;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
function enterBlock(braceAt, ctx) {
|
|
958
|
+
if (depth >= MAX_DEPTH2) fatal(braceAt, "blocks are nested too deep");
|
|
959
|
+
depth++;
|
|
960
|
+
const closed = readItems(ctx);
|
|
961
|
+
depth--;
|
|
962
|
+
if (!closed && !eofReported) {
|
|
963
|
+
eofReported = true;
|
|
964
|
+
report(braceAt, "block is never closed");
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
function startsCustomProperty() {
|
|
968
|
+
if (text.charCodeAt(pos) !== MINUS || text.charCodeAt(pos + 1) !== MINUS) return false;
|
|
969
|
+
let i = identEnd(pos);
|
|
970
|
+
while (i < length && isSpace(text.charCodeAt(i))) i++;
|
|
971
|
+
return text.charCodeAt(i) === COLON;
|
|
972
|
+
}
|
|
973
|
+
function declare(from, to) {
|
|
974
|
+
for (let i = from; i < to; i++) {
|
|
975
|
+
if (text.charCodeAt(i) === COLON) {
|
|
976
|
+
declarations.push(from, i, to);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function customProperty(ctx) {
|
|
982
|
+
const start = pos;
|
|
983
|
+
const end = identEnd(pos);
|
|
984
|
+
addProperty(identText(start, end), start, ctx);
|
|
985
|
+
pos = end;
|
|
986
|
+
const term = scanUntil(end, true);
|
|
987
|
+
declare(start, pos);
|
|
988
|
+
if (term === SEMI) pos++;
|
|
989
|
+
}
|
|
990
|
+
function atRule(ctx) {
|
|
991
|
+
const at2 = pos;
|
|
992
|
+
const nameEnd = identEnd(pos + 1);
|
|
993
|
+
const name = text.slice(pos + 1, nameEnd).toLowerCase().replace(/^-[a-z]+-/, "");
|
|
994
|
+
const preludeStart = nameEnd;
|
|
995
|
+
const term = scanUntil(nameEnd, false);
|
|
996
|
+
if (term !== OPEN_BRACE) {
|
|
997
|
+
if (term === SEMI) pos++;
|
|
998
|
+
if (ctx.kind !== "decls" && !STATEMENTS.has(name) && !CONDITIONAL.has(name) && !DECLARATIONS.has(name)) report(at2, "unknown at-rule");
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const braceAt = pos;
|
|
1002
|
+
pos++;
|
|
1003
|
+
let child = DECLS;
|
|
1004
|
+
if (ctx.kind === "decls") child = DECLS;
|
|
1005
|
+
else if (name === "layer" || CONDITIONAL.has(name)) {
|
|
1006
|
+
const kind = ctx.kind === "style" ? "style" : "rules";
|
|
1007
|
+
let layer = ctx.layer;
|
|
1008
|
+
if (name === "layer") {
|
|
1009
|
+
const written = text.slice(preludeStart, braceAt).replace(/\/\*[\s\S]*?\*\//g, "").trim();
|
|
1010
|
+
if (/^[\w.-]+$/.test(written)) layer = ctx.layer ? `${ctx.layer}.${written}` : written;
|
|
1011
|
+
} else if (name === "scope") {
|
|
1012
|
+
for (const complex of readList(preludeStart, braceAt, false).list) for (const item of complex.items) addClass(item.name, item.index, false, ctx.layer);
|
|
1013
|
+
}
|
|
1014
|
+
child = { kind, layer, sel: ctx.sel };
|
|
1015
|
+
} else if (name === "keyframes") child = { kind: "keyframes", layer: ctx.layer, sel: null };
|
|
1016
|
+
else if (!DECLARATIONS.has(name) && !MARGIN_BOX.test(name)) report(at2, "unknown at-rule");
|
|
1017
|
+
enterBlock(braceAt, child);
|
|
1018
|
+
}
|
|
1019
|
+
function statement(ctx) {
|
|
1020
|
+
const start = pos;
|
|
1021
|
+
const term = scanUntil(pos, false);
|
|
1022
|
+
if (term !== OPEN_BRACE) {
|
|
1023
|
+
if (ctx.kind !== "rules") declare(start, pos);
|
|
1024
|
+
if (term === SEMI) pos++;
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
const braceAt = pos;
|
|
1028
|
+
pos++;
|
|
1029
|
+
enterBlock(braceAt, ctx.kind === "rules" || ctx.kind === "style" ? styleRule(ctx, start, braceAt) : DECLS);
|
|
1030
|
+
}
|
|
1031
|
+
function readItems(ctx) {
|
|
1032
|
+
for (; ; ) {
|
|
1033
|
+
skipTrivia();
|
|
1034
|
+
if (pos >= length) return false;
|
|
1035
|
+
const c = text.charCodeAt(pos);
|
|
1036
|
+
if (c === CLOSE_BRACE) {
|
|
1037
|
+
pos++;
|
|
1038
|
+
if (depth === 0) {
|
|
1039
|
+
report(pos - 1, "unmatched closing brace");
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
return true;
|
|
1043
|
+
}
|
|
1044
|
+
if (c === SEMI) pos++;
|
|
1045
|
+
else if (c === AT) atRule(ctx);
|
|
1046
|
+
else if (ctx.kind === "style" && startsCustomProperty()) customProperty(ctx);
|
|
1047
|
+
else statement(ctx);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
readItems({ kind: "rules", layer: null, sel: null });
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
if (error !== HALT) throw error;
|
|
1054
|
+
}
|
|
1055
|
+
return { classes: [...classes.values()], properties: [...properties.values()], declarations, findings };
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// packages/check/src/manifests.js
|
|
1059
|
+
import { join, dirname } from "node:path";
|
|
1060
|
+
|
|
1061
|
+
// packages/check/src/walk.js
|
|
1062
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, opendirSync, readSync } from "node:fs";
|
|
1063
|
+
import { relative, resolve, sep } from "node:path";
|
|
1064
|
+
var MIB = 1024 * 1024;
|
|
1065
|
+
var LIMITS = Object.freeze({ fileBytes: MIB, totalBytes: 128 * MIB, entries: 5e3, depth: 64 });
|
|
1066
|
+
var SKIP_REASONS = Object.freeze(["symlink", "not-a-file", "too-large", "unreadable", "read-limit", "too-many-files", "too-deep"]);
|
|
1067
|
+
var DEFAULT_EXT = [
|
|
1068
|
+
[".html", "html"],
|
|
1069
|
+
[".htm", "html"],
|
|
1070
|
+
[".css", "css"]
|
|
1071
|
+
];
|
|
1072
|
+
var NEVER_ENTERED = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
1073
|
+
var CHUNK = 64 * 1024;
|
|
1074
|
+
var byText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
1075
|
+
function matchSegment(pattern, text) {
|
|
1076
|
+
if (!pattern.includes("*") && !pattern.includes("?")) return pattern === text;
|
|
1077
|
+
let p = 0;
|
|
1078
|
+
let t = 0;
|
|
1079
|
+
let star = -1;
|
|
1080
|
+
let mark = 0;
|
|
1081
|
+
while (t < text.length) {
|
|
1082
|
+
if (p < pattern.length && pattern[p] === "*") {
|
|
1083
|
+
star = p++;
|
|
1084
|
+
mark = t;
|
|
1085
|
+
} else if (p < pattern.length && (pattern[p] === "?" || pattern[p] === text[t])) {
|
|
1086
|
+
p++;
|
|
1087
|
+
t++;
|
|
1088
|
+
} else if (star !== -1) {
|
|
1089
|
+
p = star + 1;
|
|
1090
|
+
t = ++mark;
|
|
1091
|
+
} else return false;
|
|
1092
|
+
}
|
|
1093
|
+
while (p < pattern.length && pattern[p] === "*") p++;
|
|
1094
|
+
return p === pattern.length;
|
|
1095
|
+
}
|
|
1096
|
+
var segmentsOf = (text) => text.split("/").filter((segment, i, all) => segment !== "" || i === all.length - 1);
|
|
1097
|
+
var compile = (pattern) => segmentsOf(pattern).map((segment) => segment === "**" ? null : segment);
|
|
1098
|
+
function matchSegments(pattern, parts) {
|
|
1099
|
+
const cols = parts.length;
|
|
1100
|
+
let next = new Array(cols + 1).fill(false);
|
|
1101
|
+
next[cols] = true;
|
|
1102
|
+
for (let i = pattern.length - 1; i >= 0; i--) {
|
|
1103
|
+
const row = new Array(cols + 1).fill(false);
|
|
1104
|
+
if (pattern[i] === null) {
|
|
1105
|
+
row[cols] = next[cols];
|
|
1106
|
+
for (let j = cols - 1; j >= 0; j--) row[j] = next[j] || row[j + 1];
|
|
1107
|
+
} else {
|
|
1108
|
+
for (let j = cols - 1; j >= 0; j--) row[j] = next[j + 1] && matchSegment(pattern[i], parts[j]);
|
|
1109
|
+
}
|
|
1110
|
+
next = row;
|
|
1111
|
+
}
|
|
1112
|
+
return next[0];
|
|
1113
|
+
}
|
|
1114
|
+
var hasWildcard = (text) => text.includes("*") || text.includes("?");
|
|
1115
|
+
function readLimited(absolute, max) {
|
|
1116
|
+
let fd = null;
|
|
1117
|
+
try {
|
|
1118
|
+
fd = openSync(absolute, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
1119
|
+
if (!fstatSync(fd).isFile()) return { reason: "not-a-file" };
|
|
1120
|
+
const scratch = Buffer.allocUnsafe(CHUNK);
|
|
1121
|
+
const chunks = [];
|
|
1122
|
+
let length = 0;
|
|
1123
|
+
while (length <= max) {
|
|
1124
|
+
const read = readSync(fd, scratch, 0, CHUNK, null);
|
|
1125
|
+
if (read === 0) break;
|
|
1126
|
+
chunks.push(Buffer.from(scratch.subarray(0, read)));
|
|
1127
|
+
length += read;
|
|
1128
|
+
}
|
|
1129
|
+
if (length > max) return { reason: "too-large" };
|
|
1130
|
+
return { text: Buffer.concat(chunks, length).toString("utf-8"), bytes: length };
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
return { reason: error && error.code === "ELOOP" ? "symlink" : "unreadable" };
|
|
1133
|
+
} finally {
|
|
1134
|
+
if (fd !== null) {
|
|
1135
|
+
try {
|
|
1136
|
+
closeSync(fd);
|
|
1137
|
+
} catch {
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
function collect(inputs, { cwd, ext = [], ignore = [], limits = LIMITS } = {}) {
|
|
1143
|
+
const base = resolve(cwd);
|
|
1144
|
+
const kinds = new Map(DEFAULT_EXT);
|
|
1145
|
+
for (const extension of ext) kinds.set(String(extension).toLowerCase(), "template");
|
|
1146
|
+
const kindOf = (path) => {
|
|
1147
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
1148
|
+
const dot = name.lastIndexOf(".");
|
|
1149
|
+
return dot <= 0 ? null : kinds.get(name.slice(dot).toLowerCase()) ?? null;
|
|
1150
|
+
};
|
|
1151
|
+
const patterns = ignore.map(compile);
|
|
1152
|
+
const folders = ignore.filter((pattern) => pattern.endsWith("/**")).map((pattern) => compile(pattern.slice(0, -3)));
|
|
1153
|
+
const isIgnored = (path) => {
|
|
1154
|
+
const parts = segmentsOf(path);
|
|
1155
|
+
return patterns.some((pattern) => matchSegments(pattern, parts));
|
|
1156
|
+
};
|
|
1157
|
+
const isPruned = (path) => {
|
|
1158
|
+
const parts = segmentsOf(path);
|
|
1159
|
+
return folders.some((pattern) => matchSegments(pattern, parts)) || patterns.some((pattern) => matchSegments(pattern, parts));
|
|
1160
|
+
};
|
|
1161
|
+
const found = /* @__PURE__ */ new Map();
|
|
1162
|
+
const skipped = /* @__PURE__ */ new Map();
|
|
1163
|
+
const missing = [];
|
|
1164
|
+
let ignored = 0;
|
|
1165
|
+
let visited = 0;
|
|
1166
|
+
let total = 0;
|
|
1167
|
+
let stopped = false;
|
|
1168
|
+
const rel = (absolute) => {
|
|
1169
|
+
const path = relative(base, absolute);
|
|
1170
|
+
return (sep === "/" ? path : path.split(sep).join("/")) || ".";
|
|
1171
|
+
};
|
|
1172
|
+
const skip = (path, reason) => {
|
|
1173
|
+
if (!skipped.has(path)) skipped.set(path, reason);
|
|
1174
|
+
};
|
|
1175
|
+
function take(absolute, path, kind) {
|
|
1176
|
+
if (found.has(path) || skipped.has(path)) return;
|
|
1177
|
+
const result = readLimited(absolute, limits.fileBytes);
|
|
1178
|
+
if (result.reason) return skip(path, result.reason);
|
|
1179
|
+
if (total + result.bytes > limits.totalBytes) return skip(path, "read-limit");
|
|
1180
|
+
total += result.bytes;
|
|
1181
|
+
found.set(path, { path, kind, text: result.text });
|
|
1182
|
+
}
|
|
1183
|
+
function visit(absolute, depth, match, maxParts) {
|
|
1184
|
+
const path = rel(absolute);
|
|
1185
|
+
let stat;
|
|
1186
|
+
try {
|
|
1187
|
+
stat = lstatSync(absolute);
|
|
1188
|
+
} catch {
|
|
1189
|
+
return skip(path, "unreadable");
|
|
1190
|
+
}
|
|
1191
|
+
if (stat.isSymbolicLink()) {
|
|
1192
|
+
if (match && !match(path)) return;
|
|
1193
|
+
if (isIgnored(path)) ignored++;
|
|
1194
|
+
else skip(path, "symlink");
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
if (stat.isDirectory()) {
|
|
1198
|
+
if (isPruned(path)) {
|
|
1199
|
+
ignored++;
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
if (depth > limits.depth) return skip(path, "too-deep");
|
|
1203
|
+
if (maxParts !== null && segmentsOf(path).length >= maxParts) return;
|
|
1204
|
+
walk(absolute, depth, match, maxParts);
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
const kind = kindOf(path);
|
|
1208
|
+
if (kind === null || match && !match(path)) return;
|
|
1209
|
+
if (isIgnored(path)) {
|
|
1210
|
+
ignored++;
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (!stat.isFile()) return skip(path, "not-a-file");
|
|
1214
|
+
take(absolute, path, kind);
|
|
1215
|
+
}
|
|
1216
|
+
function walk(directory, depth, match, maxParts) {
|
|
1217
|
+
let handle;
|
|
1218
|
+
try {
|
|
1219
|
+
handle = opendirSync(directory);
|
|
1220
|
+
} catch {
|
|
1221
|
+
return skip(rel(directory), "unreadable");
|
|
1222
|
+
}
|
|
1223
|
+
const room = limits.entries - visited;
|
|
1224
|
+
const names = [];
|
|
1225
|
+
let more = false;
|
|
1226
|
+
try {
|
|
1227
|
+
for (let entry = handle.readSync(); entry !== null; entry = handle.readSync()) {
|
|
1228
|
+
if (names.length >= room) {
|
|
1229
|
+
more = true;
|
|
1230
|
+
break;
|
|
1231
|
+
}
|
|
1232
|
+
names.push(entry.name);
|
|
1233
|
+
}
|
|
1234
|
+
} catch {
|
|
1235
|
+
skip(rel(directory), "unreadable");
|
|
1236
|
+
} finally {
|
|
1237
|
+
try {
|
|
1238
|
+
handle.closeSync();
|
|
1239
|
+
} catch {
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
visited += names.length;
|
|
1243
|
+
for (const name of names.sort(byText)) {
|
|
1244
|
+
if (stopped) return;
|
|
1245
|
+
if (NEVER_ENTERED.has(name)) continue;
|
|
1246
|
+
visit(resolve(directory, name), depth + 1, match, maxParts);
|
|
1247
|
+
}
|
|
1248
|
+
if (more && !stopped) {
|
|
1249
|
+
stopped = true;
|
|
1250
|
+
skip(rel(directory), "too-many-files");
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
for (const raw of inputs) {
|
|
1254
|
+
if (stopped) break;
|
|
1255
|
+
const input = String(raw);
|
|
1256
|
+
if (hasWildcard(input)) {
|
|
1257
|
+
const segments = input.split("/");
|
|
1258
|
+
const first = segments.findIndex(hasWildcard);
|
|
1259
|
+
const prefix = segments.slice(0, first).join("/");
|
|
1260
|
+
const start = resolve(base, prefix === "" && input.startsWith("/") ? "/" : prefix);
|
|
1261
|
+
const startPath = rel(start);
|
|
1262
|
+
const pattern = compile([...startPath === "." ? [] : [startPath], ...segments.slice(first)].join("/"));
|
|
1263
|
+
let hits = 0;
|
|
1264
|
+
const match = (path2) => {
|
|
1265
|
+
const matches2 = matchSegments(pattern, segmentsOf(path2));
|
|
1266
|
+
if (matches2) hits++;
|
|
1267
|
+
return matches2;
|
|
1268
|
+
};
|
|
1269
|
+
let isFolder = false;
|
|
1270
|
+
try {
|
|
1271
|
+
isFolder = lstatSync(start).isDirectory();
|
|
1272
|
+
} catch {
|
|
1273
|
+
}
|
|
1274
|
+
if (isFolder) walk(start, 0, match, pattern.includes(null) ? null : pattern.length);
|
|
1275
|
+
if (hits === 0) missing.push(input);
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
const absolute = resolve(base, input);
|
|
1279
|
+
let stat;
|
|
1280
|
+
try {
|
|
1281
|
+
stat = lstatSync(absolute);
|
|
1282
|
+
} catch {
|
|
1283
|
+
missing.push(input);
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
const path = rel(absolute);
|
|
1287
|
+
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
1288
|
+
if (path !== "." && isPruned(path)) ignored++;
|
|
1289
|
+
else walk(absolute, 0, null, null);
|
|
1290
|
+
} else if (stat.isSymbolicLink() || kindOf(path) !== null) {
|
|
1291
|
+
visit(absolute, 0, null, null);
|
|
1292
|
+
} else missing.push(input);
|
|
1293
|
+
}
|
|
1294
|
+
return {
|
|
1295
|
+
files: [...found.values()].sort((a, b) => byText(a.path, b.path)),
|
|
1296
|
+
skipped: [...skipped].map(([file, reason]) => ({ file, reason })).sort((a, b) => byText(a.file, b.file)),
|
|
1297
|
+
missing,
|
|
1298
|
+
ignored
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// packages/check/src/manifests.js
|
|
1303
|
+
var PACKAGES = ["tokens", "core", "site", "landing", "cms", "app", "docs"];
|
|
1304
|
+
var MAX_MANIFEST_BYTES = 8 * 1024 * 1024;
|
|
1305
|
+
var MAX_PACKAGE_JSON_BYTES = 256 * 1024;
|
|
1306
|
+
var MAX_CLASSES = 5e4;
|
|
1307
|
+
var MAX_ELEMENTS = 2e3;
|
|
1308
|
+
var MAX_ATTRIBUTES = 1e3;
|
|
1309
|
+
var MAX_HOOKS = 2e3;
|
|
1310
|
+
var MAX_PROPERTIES = 2e4;
|
|
1311
|
+
var MAX_TOKENS_BYTES = 1024 * 1024;
|
|
1312
|
+
var NAME = /^[A-Za-z0-9_:.\-/]{1,64}$/;
|
|
1313
|
+
var TAG = /^[a-z][a-z0-9-]{0,63}$/;
|
|
1314
|
+
var HOOK = /^data-luz-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1315
|
+
var HOST = /^[a-z][a-z0-9]{0,31}(?:\[[a-z][a-z-]{0,31}(?:=[A-Za-z0-9_-]{1,32})?\])?$/;
|
|
1316
|
+
var MAX_HOSTS = 16;
|
|
1317
|
+
var VERSION = /^[0-9A-Za-z.+_-]{1,40}$/;
|
|
1318
|
+
var FORBIDDEN = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1319
|
+
var isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1320
|
+
var isName = (value) => typeof value === "string" && NAME.test(value) && !FORBIDDEN.has(value);
|
|
1321
|
+
function readJson(path, max) {
|
|
1322
|
+
const read = readLimited(path, max);
|
|
1323
|
+
if (read.reason !== void 0) return null;
|
|
1324
|
+
try {
|
|
1325
|
+
return JSON.parse(read.text);
|
|
1326
|
+
} catch {
|
|
1327
|
+
return null;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function parseClasses(data, pkg) {
|
|
1331
|
+
if (!isObject(data) || data.schema !== 1 || data.package !== `@luz-ds/${pkg}`) return null;
|
|
1332
|
+
if (typeof data.version !== "string" || !VERSION.test(data.version)) return null;
|
|
1333
|
+
if (!Array.isArray(data.classes) || data.classes.length > MAX_CLASSES) return null;
|
|
1334
|
+
const classes = [];
|
|
1335
|
+
for (const entry of data.classes) {
|
|
1336
|
+
if (!isObject(entry) || !isName(entry.name)) return null;
|
|
1337
|
+
for (const key of ["block", "modifier", "part"]) if (entry[key] !== void 0 && !isName(entry[key])) return null;
|
|
1338
|
+
const { hosts } = entry;
|
|
1339
|
+
if (hosts !== void 0 && !(Array.isArray(hosts) && hosts.length > 0 && hosts.length <= MAX_HOSTS && hosts.every((host) => typeof host === "string" && HOST.test(host)))) return null;
|
|
1340
|
+
classes.push({ name: entry.name, block: entry.block, kind: entry.kind, defined: entry.defined, modifier: entry.modifier, part: entry.part, hosts });
|
|
1341
|
+
}
|
|
1342
|
+
if (data.properties !== void 0 && !Array.isArray(data.properties)) return null;
|
|
1343
|
+
const properties = (data.properties ?? []).slice(0, MAX_PROPERTIES).filter((entry) => isObject(entry) && isName(entry.name) && (entry.scope === "root" || entry.scope === "component"));
|
|
1344
|
+
let hooks = [];
|
|
1345
|
+
if (data.hooks !== void 0) {
|
|
1346
|
+
if (!Array.isArray(data.hooks) || data.hooks.length > MAX_HOOKS || !data.hooks.every((hook) => typeof hook === "string" && HOOK.test(hook))) return null;
|
|
1347
|
+
hooks = data.hooks;
|
|
1348
|
+
}
|
|
1349
|
+
return { version: data.version, classes, properties, hooks };
|
|
1350
|
+
}
|
|
1351
|
+
function parseElements(data) {
|
|
1352
|
+
if (!isObject(data) || !Array.isArray(data.modules) || data.modules.length > MAX_ELEMENTS) return null;
|
|
1353
|
+
const elements = [];
|
|
1354
|
+
for (const module of data.modules) {
|
|
1355
|
+
if (!isObject(module)) return null;
|
|
1356
|
+
if (module.declarations === void 0) continue;
|
|
1357
|
+
if (!Array.isArray(module.declarations)) return null;
|
|
1358
|
+
for (const declaration of module.declarations) {
|
|
1359
|
+
if (!isObject(declaration)) return null;
|
|
1360
|
+
if (declaration.tagName === void 0) continue;
|
|
1361
|
+
if (typeof declaration.tagName !== "string" || !TAG.test(declaration.tagName)) return null;
|
|
1362
|
+
const attributes = [];
|
|
1363
|
+
if (declaration.attributes !== void 0) {
|
|
1364
|
+
if (!Array.isArray(declaration.attributes) || declaration.attributes.length > MAX_ATTRIBUTES) return null;
|
|
1365
|
+
for (const attribute of declaration.attributes) {
|
|
1366
|
+
if (!isObject(attribute) || !isName(attribute.name)) return null;
|
|
1367
|
+
attributes.push(attribute.name);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
elements.push({ tag: declaration.tagName, attributes });
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
return elements;
|
|
1374
|
+
}
|
|
1375
|
+
function readPackage(classesPath, elementsPath, pkg) {
|
|
1376
|
+
const data = readJson(classesPath, MAX_MANIFEST_BYTES);
|
|
1377
|
+
if (data === null) return null;
|
|
1378
|
+
const classes = parseClasses(data, pkg);
|
|
1379
|
+
if (classes === null) return null;
|
|
1380
|
+
let elements = [];
|
|
1381
|
+
const present = readLimited(elementsPath, MAX_MANIFEST_BYTES);
|
|
1382
|
+
if (present.reason === "too-large" || present.reason === "symlink" || present.reason === "not-a-file") return null;
|
|
1383
|
+
if (present.reason === void 0) {
|
|
1384
|
+
let parsed;
|
|
1385
|
+
try {
|
|
1386
|
+
parsed = JSON.parse(present.text);
|
|
1387
|
+
} catch {
|
|
1388
|
+
return null;
|
|
1389
|
+
}
|
|
1390
|
+
elements = parseElements(parsed);
|
|
1391
|
+
if (elements === null) return null;
|
|
1392
|
+
}
|
|
1393
|
+
return { ...classes, elements };
|
|
1394
|
+
}
|
|
1395
|
+
function readText(path, max) {
|
|
1396
|
+
const read = readLimited(path, max);
|
|
1397
|
+
return read.reason === void 0 ? read.text : null;
|
|
1398
|
+
}
|
|
1399
|
+
function buildWorld(parts) {
|
|
1400
|
+
const classes = /* @__PURE__ */ new Set();
|
|
1401
|
+
const blocks = /* @__PURE__ */ new Set();
|
|
1402
|
+
const tags = /* @__PURE__ */ new Map();
|
|
1403
|
+
const hooks = /* @__PURE__ */ new Set();
|
|
1404
|
+
const versions = /* @__PURE__ */ new Map();
|
|
1405
|
+
const packages = [];
|
|
1406
|
+
const names = { luz: [], o: [], u: [] };
|
|
1407
|
+
const info = /* @__PURE__ */ new Map();
|
|
1408
|
+
const properties = /* @__PURE__ */ new Map();
|
|
1409
|
+
for (const part of parts) {
|
|
1410
|
+
packages.push({ package: `@luz-ds/${part.name}`, version: part.version, source: part.source });
|
|
1411
|
+
versions.set(part.name, { version: part.version, source: part.source });
|
|
1412
|
+
for (const { name, block, ...rest } of part.classes) {
|
|
1413
|
+
if (!classes.has(name)) {
|
|
1414
|
+
classes.add(name);
|
|
1415
|
+
info.set(name, { block, ...rest, package: part.name });
|
|
1416
|
+
if (name.startsWith("luz-")) names.luz.push(name);
|
|
1417
|
+
else if (name.startsWith("o-")) names.o.push(name);
|
|
1418
|
+
else if (name.startsWith("u-")) names.u.push(name);
|
|
1419
|
+
}
|
|
1420
|
+
if (block !== void 0) blocks.add(block);
|
|
1421
|
+
}
|
|
1422
|
+
for (const hook of part.hooks) hooks.add(hook);
|
|
1423
|
+
for (const { name, scope } of part.properties) if (scope === "root" || !properties.has(name)) properties.set(name, scope);
|
|
1424
|
+
for (const { tag, attributes } of part.elements) {
|
|
1425
|
+
let known = tags.get(tag);
|
|
1426
|
+
if (known === void 0) {
|
|
1427
|
+
known = { attributes: /* @__PURE__ */ new Set(), package: part.name };
|
|
1428
|
+
tags.set(tag, known);
|
|
1429
|
+
}
|
|
1430
|
+
for (const attribute of attributes) {
|
|
1431
|
+
known.attributes.add(attribute);
|
|
1432
|
+
if (attribute.startsWith("data-luz-")) hooks.add(attribute);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
for (const list of Object.values(names)) list.sort();
|
|
1437
|
+
const publicNames = [...properties].filter(([, scope]) => scope === "root").map(([name]) => name).sort();
|
|
1438
|
+
return { packages, classes, blocks, tags, tagNames: [...tags.keys()].sort(), hooks, versions, byPrefix: names, info, properties, publicNames };
|
|
1439
|
+
}
|
|
1440
|
+
function createFacts({ data }) {
|
|
1441
|
+
const bundled = /* @__PURE__ */ new Map();
|
|
1442
|
+
const installed = /* @__PURE__ */ new Map();
|
|
1443
|
+
const worlds = /* @__PURE__ */ new Map();
|
|
1444
|
+
const shared = /* @__PURE__ */ new Map();
|
|
1445
|
+
const ids = /* @__PURE__ */ new Map();
|
|
1446
|
+
const fromSnapshot = (pkg) => {
|
|
1447
|
+
if (bundled.has(pkg)) return bundled.get(pkg);
|
|
1448
|
+
let part = null;
|
|
1449
|
+
if (typeof data === "string" && data !== "") {
|
|
1450
|
+
const folder = join(data, pkg);
|
|
1451
|
+
const read = readPackage(join(folder, "classes.json"), join(folder, "custom-elements.json"), pkg);
|
|
1452
|
+
if (read !== null) part = { name: pkg, source: "bundled", ...read };
|
|
1453
|
+
}
|
|
1454
|
+
bundled.set(pkg, part);
|
|
1455
|
+
return part;
|
|
1456
|
+
};
|
|
1457
|
+
const fromFolder = (folder, pkg) => {
|
|
1458
|
+
if (installed.has(folder)) return installed.get(folder);
|
|
1459
|
+
let part = null;
|
|
1460
|
+
const named = readJson(join(folder, "package.json"), MAX_PACKAGE_JSON_BYTES);
|
|
1461
|
+
if (isObject(named) && named.name === `@luz-ds/${pkg}`) {
|
|
1462
|
+
const read = readPackage(join(folder, "dist", "classes.json"), join(folder, "dist", "custom-elements.json"), pkg);
|
|
1463
|
+
if (read !== null) part = { name: pkg, source: "node_modules", ...read };
|
|
1464
|
+
}
|
|
1465
|
+
installed.set(folder, part);
|
|
1466
|
+
return part;
|
|
1467
|
+
};
|
|
1468
|
+
function forDirectory(directory) {
|
|
1469
|
+
const cached = worlds.get(directory);
|
|
1470
|
+
if (cached !== void 0) return cached;
|
|
1471
|
+
const parts = [];
|
|
1472
|
+
for (const pkg of PACKAGES) {
|
|
1473
|
+
let part = null;
|
|
1474
|
+
for (let current = directory; ; ) {
|
|
1475
|
+
part = fromFolder(join(current, "node_modules", "@luz-ds", pkg), pkg);
|
|
1476
|
+
if (part !== null) break;
|
|
1477
|
+
const parent = dirname(current);
|
|
1478
|
+
if (parent === current) break;
|
|
1479
|
+
current = parent;
|
|
1480
|
+
}
|
|
1481
|
+
part ??= fromSnapshot(pkg);
|
|
1482
|
+
if (part !== null) parts.push(part);
|
|
1483
|
+
}
|
|
1484
|
+
const key = parts.map((part) => ids.get(part) ?? ids.set(part, ids.size).get(part)).join(",");
|
|
1485
|
+
let world = shared.get(key);
|
|
1486
|
+
if (world === void 0) {
|
|
1487
|
+
world = buildWorld(parts);
|
|
1488
|
+
shared.set(key, world);
|
|
1489
|
+
}
|
|
1490
|
+
worlds.set(directory, world);
|
|
1491
|
+
return world;
|
|
1492
|
+
}
|
|
1493
|
+
const tokenFiles = /* @__PURE__ */ new Map();
|
|
1494
|
+
function tokensFor(directory, bundled2 = false) {
|
|
1495
|
+
for (let current = directory; !bundled2; ) {
|
|
1496
|
+
const folder = join(current, "node_modules", "@luz-ds", "tokens");
|
|
1497
|
+
if (!tokenFiles.has(folder)) {
|
|
1498
|
+
const named = readJson(join(folder, "package.json"), MAX_PACKAGE_JSON_BYTES);
|
|
1499
|
+
tokenFiles.set(folder, isObject(named) && named.name === "@luz-ds/tokens" ? readText(join(folder, "dist", "tokens.css"), MAX_TOKENS_BYTES) : null);
|
|
1500
|
+
}
|
|
1501
|
+
const text2 = tokenFiles.get(folder);
|
|
1502
|
+
if (text2 !== null) return { text: text2, source: "node_modules" };
|
|
1503
|
+
const parent = dirname(current);
|
|
1504
|
+
if (parent === current) break;
|
|
1505
|
+
current = parent;
|
|
1506
|
+
}
|
|
1507
|
+
const text = typeof data === "string" && data !== "" ? readText(join(data, "tokens", "tokens.css"), MAX_TOKENS_BYTES) : null;
|
|
1508
|
+
return text === null ? null : { text, source: "bundled" };
|
|
1509
|
+
}
|
|
1510
|
+
return { forDirectory, tokensFor };
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// packages/check/src/report.js
|
|
1514
|
+
var MAX_FINDINGS_PER_FILE = 200;
|
|
1515
|
+
var MAX_FINDINGS2 = 5e3;
|
|
1516
|
+
var byText2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
1517
|
+
var at = (value) => typeof value === "number" ? value : Infinity;
|
|
1518
|
+
function finding(key, { file, line = null, column = null, vars = {}, suggestion = null, details } = {}) {
|
|
1519
|
+
const text = message(key, vars);
|
|
1520
|
+
const rule = key.split(":")[0];
|
|
1521
|
+
const found = {
|
|
1522
|
+
rule,
|
|
1523
|
+
severity: MESSAGES[key].severity,
|
|
1524
|
+
file,
|
|
1525
|
+
line: line ?? null,
|
|
1526
|
+
column: column ?? null,
|
|
1527
|
+
message: text,
|
|
1528
|
+
suggestion: suggestion ?? null,
|
|
1529
|
+
docs: docsUrl(rule)
|
|
1530
|
+
};
|
|
1531
|
+
if (details !== void 0) found.details = details;
|
|
1532
|
+
return found;
|
|
1533
|
+
}
|
|
1534
|
+
var order = (a, b) => byText2(a.file, b.file) || at(a.line) - at(b.line) || at(a.column) - at(b.column) || byText2(a.rule, b.rule);
|
|
1535
|
+
function capped(all) {
|
|
1536
|
+
const ranked = all.slice().sort((a, b) => a.severity === b.severity ? order(a, b) : a.severity === "block" ? -1 : 1);
|
|
1537
|
+
const perFile = /* @__PURE__ */ new Map();
|
|
1538
|
+
const kept = [];
|
|
1539
|
+
for (const found of ranked) {
|
|
1540
|
+
const count = perFile.get(found.file) ?? 0;
|
|
1541
|
+
if (count >= MAX_FINDINGS_PER_FILE || kept.length >= MAX_FINDINGS2) continue;
|
|
1542
|
+
perFile.set(found.file, count + 1);
|
|
1543
|
+
kept.push(found);
|
|
1544
|
+
}
|
|
1545
|
+
return { kept: kept.sort(order), truncated: kept.length < all.length };
|
|
1546
|
+
}
|
|
1547
|
+
function buildReport({ version: version2, files = [], manifests = [], findings = [], skipped = [], config = {}, ignored = 0, strict = false }) {
|
|
1548
|
+
const escaped = /* @__PURE__ */ new Map();
|
|
1549
|
+
const shown2 = (path) => {
|
|
1550
|
+
let text = escaped.get(path);
|
|
1551
|
+
if (text === void 0) escaped.set(path, text = displayPath(path));
|
|
1552
|
+
return text;
|
|
1553
|
+
};
|
|
1554
|
+
const skippedList = skipped.map(({ file, reason }) => ({ file: shown2(file), reason })).sort((a, b) => byText2(a.file, b.file) || byText2(a.reason, b.reason));
|
|
1555
|
+
const all = [
|
|
1556
|
+
...findings.map((found) => shown2(found.file) === found.file ? found : { ...found, file: shown2(found.file) }),
|
|
1557
|
+
...skippedList.map(({ file, reason }) => finding("file/skipped", { file, vars: { reason } }))
|
|
1558
|
+
];
|
|
1559
|
+
const blocking = all.filter((found) => found.severity === "block").length;
|
|
1560
|
+
const warnings = all.length - blocking;
|
|
1561
|
+
const { kept, truncated } = capped(all);
|
|
1562
|
+
const off = [...config.off ?? []].sort(byText2);
|
|
1563
|
+
return {
|
|
1564
|
+
tool: "@luz-ds/check",
|
|
1565
|
+
version: cleanText(version2, 40),
|
|
1566
|
+
ok: blocking === 0 && !(strict && warnings > 0),
|
|
1567
|
+
files: files.map(shown2).sort(byText2),
|
|
1568
|
+
manifests: manifests.map(({ package: name, version: has2, source }) => ({ package: cleanText(name, 64), version: cleanText(has2, 40), source: cleanText(source, 20) })),
|
|
1569
|
+
summary: { files: files.length, blocking, warnings, skipped: skippedList.length, ignored, off: off.length, truncated },
|
|
1570
|
+
config: { file: config.file === null || config.file === void 0 ? null : shown2(config.file), off },
|
|
1571
|
+
findings: kept,
|
|
1572
|
+
skipped: skippedList
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
var MARK = { block: "\u2717", warn: "!" };
|
|
1576
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1577
|
+
function formatText(report) {
|
|
1578
|
+
const lines2 = report.findings.flatMap((found) => {
|
|
1579
|
+
const position = found.line === null ? "" : found.column === null ? `:${found.line}` : `:${found.line}:${found.column}`;
|
|
1580
|
+
const head = `${MARK[found.severity]} ${found.file}${position} ${found.rule} ${found.message}`;
|
|
1581
|
+
const places = (found.details?.overrides ?? []).map((o) => ` ${o.token}${o.value === void 0 || o.value === null ? "" : `: ${o.value}`} is set in ${o.file}:${o.line}`);
|
|
1582
|
+
return [head, ...places];
|
|
1583
|
+
});
|
|
1584
|
+
const { summary } = report;
|
|
1585
|
+
const parts = [`${report.ok ? "\u2713" : "\u2717"} ${summary.blocking} blocking and ${plural(summary.warnings, "warning")} in ${plural(summary.files, "file")}`];
|
|
1586
|
+
if (summary.ignored > 0) parts.push(`${summary.ignored} ignored`);
|
|
1587
|
+
if (summary.off > 0) parts.push(`${summary.off} off`);
|
|
1588
|
+
if (summary.truncated) parts.push("output cut, the totals above are complete");
|
|
1589
|
+
let line = parts.join(", ");
|
|
1590
|
+
if ((summary.ignored > 0 || summary.off > 0) && report.config.file !== null) line += ` (${report.config.file})`;
|
|
1591
|
+
if (!report.ok && summary.blocking === 0) line += " (--strict: a warning fails)";
|
|
1592
|
+
if (report.manifests.length > 0) line += ` (manifests: ${report.manifests.map((m) => `${m.package} ${m.version} from ${m.source}`).join(", ")})`;
|
|
1593
|
+
lines2.push(line);
|
|
1594
|
+
return `${lines2.join("\n")}
|
|
1595
|
+
`;
|
|
1596
|
+
}
|
|
1597
|
+
function formatJson(report) {
|
|
1598
|
+
return `${JSON.stringify(report, null, 2)}
|
|
1599
|
+
`;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// packages/check/src/rules/load.js
|
|
1603
|
+
import { posix } from "node:path";
|
|
1604
|
+
|
|
1605
|
+
// packages/tokens/lib/contrast.js
|
|
1606
|
+
var NAMED = { transparent: [0, 0, 0, 0], white: [255, 255, 255, 1], black: [0, 0, 0, 1] };
|
|
1607
|
+
var clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
1608
|
+
function hslToRgb(h, s, l) {
|
|
1609
|
+
const k = (n) => (n + h / 30) % 12;
|
|
1610
|
+
const a = s * Math.min(l, 1 - l);
|
|
1611
|
+
const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
1612
|
+
return [f(0) * 255, f(8) * 255, f(4) * 255];
|
|
1613
|
+
}
|
|
1614
|
+
function parseColor(value) {
|
|
1615
|
+
const text = String(value).trim().toLowerCase();
|
|
1616
|
+
if (text in NAMED) {
|
|
1617
|
+
const [r, g, b, a] = NAMED[text];
|
|
1618
|
+
return { r, g, b, a };
|
|
1619
|
+
}
|
|
1620
|
+
const hex = text.match(/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/)?.[1];
|
|
1621
|
+
if (hex) {
|
|
1622
|
+
const full = hex.length <= 4 ? [...hex].map((c) => c + c).join("") : hex;
|
|
1623
|
+
const byte = (i) => parseInt(full.slice(i, i + 2), 16);
|
|
1624
|
+
return { r: byte(0), g: byte(2), b: byte(4), a: full.length === 8 ? byte(6) / 255 : 1 };
|
|
1625
|
+
}
|
|
1626
|
+
const fn = text.match(/^(rgba?|hsla?)\(([^)]+)\)$/);
|
|
1627
|
+
if (fn) {
|
|
1628
|
+
const parts = fn[2].split(/[\s,/]+/).filter(Boolean);
|
|
1629
|
+
const number = (p, scale = 1) => p.endsWith("%") ? parseFloat(p) / 100 * scale : parseFloat(p);
|
|
1630
|
+
if (parts.length >= 3 && parts.length <= 4 && parts.every((p) => !Number.isNaN(parseFloat(p)))) {
|
|
1631
|
+
const a = parts[3] === void 0 ? 1 : clamp(number(parts[3], 1), 0, 1);
|
|
1632
|
+
if (fn[1].startsWith("rgb")) {
|
|
1633
|
+
const [r2, g2, b2] = parts.slice(0, 3).map((p) => clamp(number(p, 255), 0, 255));
|
|
1634
|
+
return { r: r2, g: g2, b: b2, a };
|
|
1635
|
+
}
|
|
1636
|
+
const [r, g, b] = hslToRgb((parseFloat(parts[0]) % 360 + 360) % 360, clamp(number(parts[1], 1), 0, 1), clamp(number(parts[2], 1), 0, 1));
|
|
1637
|
+
return { r, g, b, a };
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
throw new Error(`cannot read the colour "${value}": write it as #rrggbb, rgb(), rgba(), hsl() or hsla()`);
|
|
1641
|
+
}
|
|
1642
|
+
var over = (top, bottom) => ({
|
|
1643
|
+
r: top.r * top.a + bottom.r * (1 - top.a),
|
|
1644
|
+
g: top.g * top.a + bottom.g * (1 - top.a),
|
|
1645
|
+
b: top.b * top.a + bottom.b * (1 - top.a),
|
|
1646
|
+
a: 1
|
|
1647
|
+
});
|
|
1648
|
+
var luminance = ({ r, g, b }) => {
|
|
1649
|
+
const c = (v) => (v /= 255) <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
|
1650
|
+
return 0.2126 * c(r) + 0.7152 * c(g) + 0.0722 * c(b);
|
|
1651
|
+
};
|
|
1652
|
+
var ratio = (x, y) => {
|
|
1653
|
+
const [a, b] = [luminance(x), luminance(y)];
|
|
1654
|
+
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
|
|
1655
|
+
};
|
|
1656
|
+
var formatRatio = (value) => `${(Math.floor(value * 10 + 1e-9) / 10).toFixed(1)}:1`;
|
|
1657
|
+
var SURFACES = ["surface-page", "surface", "surface-hover", "surface-muted", "input-bg"];
|
|
1658
|
+
var STATUS = ["success", "danger", "warning", "info", "teal", "purple"];
|
|
1659
|
+
var CODE = ["comment", "keyword", "name", "property", "string", "literal"].map((t) => `code-${t}`);
|
|
1660
|
+
var PAIRS = [
|
|
1661
|
+
// Text and links on the surfaces they sit on
|
|
1662
|
+
{ fg: ["text-primary", "text-secondary", "brand-primary", "brand-hover"], bg: SURFACES, min: 4.5, kind: "text" },
|
|
1663
|
+
{ fg: ["text-primary", "text-secondary"], bg: ["surface-strong", "surface-pressed"], min: 4.5, kind: "text" },
|
|
1664
|
+
{ fg: ["text-muted"], bg: ["input-bg", "surface-page", "surface"], min: 4.5, kind: "text" },
|
|
1665
|
+
{ fg: ["text-disabled"], bg: ["surface-page", "surface", "input-bg", "surface-muted", "surface-strong"], min: 3, kind: "disabled-text" },
|
|
1666
|
+
// Text on solid fills
|
|
1667
|
+
{ fg: ["text-on-fill"], bg: ["brand-fill", "brand-fill-hover", "brand-fill-active", "color-danger-fill"], min: 4.5, kind: "text" },
|
|
1668
|
+
// Sidebar
|
|
1669
|
+
{ fg: ["sidebar-text", "sidebar-label", "sidebar-text-hover"], bg: ["sidebar-bg"], min: 4.5, kind: "text" },
|
|
1670
|
+
{ fg: ["sidebar-text-hover"], bg: ["sidebar-hover"], min: 4.5, kind: "text" },
|
|
1671
|
+
{ fg: ["sidebar-active-text"], bg: ["sidebar-active-bg"], min: 4.5, kind: "text" },
|
|
1672
|
+
// Status text on its tint (alerts, tags), also on a hovered row
|
|
1673
|
+
...STATUS.map((s) => ({
|
|
1674
|
+
fg: [`color-${s}`],
|
|
1675
|
+
bg: [`color-${s}-bg > surface-page`, `color-${s}-bg > surface`, `color-${s}-bg > surface-hover`],
|
|
1676
|
+
min: 4.5,
|
|
1677
|
+
kind: "text"
|
|
1678
|
+
})),
|
|
1679
|
+
{ fg: ["color-code"], bg: ["surface-page", "surface", "surface-muted"], min: 4.5, kind: "text" },
|
|
1680
|
+
// Non-text: focus ring, field boundaries, outlines (WCAG 1.4.11)
|
|
1681
|
+
{ fg: ["focus-ring"], bg: [...SURFACES, "surface-strong"], min: 3, kind: "ui" },
|
|
1682
|
+
{ fg: ["input-border"], bg: ["input-bg"], min: 3, kind: "ui" },
|
|
1683
|
+
{ fg: ["outline"], bg: ["surface-page", "surface"], min: 3, kind: "ui" },
|
|
1684
|
+
// Progress and player seek bars: the fill on the track (surface-muted) and the
|
|
1685
|
+
// played part on the buffered part (surface-pressed)
|
|
1686
|
+
{ fg: ["brand-primary"], bg: ["surface-muted", "surface-pressed"], min: 3, kind: "ui" },
|
|
1687
|
+
// Also on the strong surface (tooltip, toast, table header) and as plain text
|
|
1688
|
+
// on every surface: status and code colours and their tints keep 4.5:1 there
|
|
1689
|
+
...STATUS.map((s) => ({
|
|
1690
|
+
fg: [`color-${s}`],
|
|
1691
|
+
bg: [`color-${s}-bg > surface-strong`, "surface-page", "surface", "surface-hover", "surface-muted", "surface-strong"],
|
|
1692
|
+
min: 4.5,
|
|
1693
|
+
kind: "text"
|
|
1694
|
+
})),
|
|
1695
|
+
{ fg: ["color-code"], bg: ["surface-strong"], min: 4.5, kind: "text" },
|
|
1696
|
+
// Syntax colours on the code block (.luz-code-block sits on surface-muted)
|
|
1697
|
+
{ fg: CODE, bg: ["surface-muted"], min: 4.5, kind: "text" },
|
|
1698
|
+
// The field border on every surface a field can sit on (WCAG 1.4.11)
|
|
1699
|
+
{ fg: ["input-border"], bg: [...SURFACES, "surface-strong"], min: 3, kind: "ui" },
|
|
1700
|
+
// .luz-badge--gray: white text on the outline colour
|
|
1701
|
+
{ fg: ["text-on-fill"], bg: ["outline"], min: 4.5, kind: "text" },
|
|
1702
|
+
// The error mark of a form field, on the field itself (select, input)
|
|
1703
|
+
{ fg: ["color-danger"], bg: ["input-bg"], min: 3, kind: "ui" },
|
|
1704
|
+
// What the component pages print and no row above covers: the body text of
|
|
1705
|
+
// an alert on its tint, a link or an avatar on the brand tint, the checked
|
|
1706
|
+
// box and the active sidebar entry as shapes, text on a video
|
|
1707
|
+
{ fg: ["text-primary", "text-secondary"], bg: STATUS.map((s) => `color-${s}-bg > surface-page`), min: 4.5, kind: "text" },
|
|
1708
|
+
{ fg: ["text-primary", "text-secondary", "brand-primary"], bg: ["brand-subtle > surface-page"], min: 4.5, kind: "text" },
|
|
1709
|
+
// A primary button and a focus ring on the brand tint (a band, a hovered
|
|
1710
|
+
// outline button): 1.4.11, strict. In dark mode the fill is the thinnest
|
|
1711
|
+
// pair of the system, so a change to the tint or the fill fails here first
|
|
1712
|
+
{ fg: ["brand-fill", "focus-ring"], bg: ["brand-subtle > surface-page"], min: 3, kind: "ui" },
|
|
1713
|
+
// A checked box and a switch track are told apart by their mark and their
|
|
1714
|
+
// border, so their fill on the page is a stricter aim than WCAG 1.4.11
|
|
1715
|
+
// needs: reported, never failing
|
|
1716
|
+
{ fg: ["brand-fill"], bg: ["surface-page", "surface"], min: 3, kind: "ui", advisory: true },
|
|
1717
|
+
{ fg: ["brand-primary"], bg: ["surface-strong"], min: 3, kind: "ui" },
|
|
1718
|
+
{ fg: ["sidebar-active-bg"], bg: ["sidebar-bg"], min: 3, kind: "ui" },
|
|
1719
|
+
{ fg: ["text-on-fill"], bg: ["surface-media"], min: 4.5, kind: "text" },
|
|
1720
|
+
// Site's bands (notice, cookie banner): a link and a focus ring on the
|
|
1721
|
+
// warning notice, a primary button's edge on the muted band, and the edge
|
|
1722
|
+
// of a checkbox on its field surface
|
|
1723
|
+
{ fg: ["brand-primary"], bg: ["color-warning-bg > surface-page"], min: 4.5, kind: "text" },
|
|
1724
|
+
{ fg: ["focus-ring"], bg: ["color-warning-bg > surface-page"], min: 3, kind: "ui" },
|
|
1725
|
+
{ fg: ["brand-fill"], bg: ["surface-muted"], min: 3, kind: "ui" },
|
|
1726
|
+
{ fg: ["outline"], bg: ["input-bg"], min: 3, kind: "ui" }
|
|
1727
|
+
];
|
|
1728
|
+
var HIGH_CONTRAST_PAIRS = [{ fg: CODE, bg: ["surface-muted"], min: 7, kind: "text" }];
|
|
1729
|
+
var pairsFor = (mode) => mode === "high-contrast" ? [...PAIRS, ...HIGH_CONTRAST_PAIRS] : PAIRS;
|
|
1730
|
+
function expandPairs(pairs) {
|
|
1731
|
+
const out = [];
|
|
1732
|
+
for (const { fg, bg, min, kind, advisory = false } of pairs) for (const f of fg) for (const b of bg) out.push({ fg: f, bg: b, min, kind, advisory });
|
|
1733
|
+
return out;
|
|
1734
|
+
}
|
|
1735
|
+
var VAR = /^var\(\s*(--[\w-]+)\s*(?:,\s*([^)]+))?\)$/;
|
|
1736
|
+
function createResolver(values) {
|
|
1737
|
+
function resolve3(name, chain = []) {
|
|
1738
|
+
if (chain.includes(name)) throw new Error(`${name} refers to itself`);
|
|
1739
|
+
const raw = values[name];
|
|
1740
|
+
if (raw === void 0) throw new Error(`${name} is not defined`);
|
|
1741
|
+
const ref = raw.match(VAR);
|
|
1742
|
+
if (!ref) return { value: raw, chain: [...chain, name] };
|
|
1743
|
+
if (values[ref[1]] === void 0 && ref[2]) return { value: ref[2].trim(), chain: [...chain, name] };
|
|
1744
|
+
return resolve3(ref[1], [...chain, name]);
|
|
1745
|
+
}
|
|
1746
|
+
return resolve3;
|
|
1747
|
+
}
|
|
1748
|
+
function measure(resolve3, pairs) {
|
|
1749
|
+
const results = [];
|
|
1750
|
+
const unreadable = [];
|
|
1751
|
+
const colour = (name) => {
|
|
1752
|
+
const { value, chain } = resolve3(`--luz-${name}`);
|
|
1753
|
+
try {
|
|
1754
|
+
return { colour: parseColor(value), chain };
|
|
1755
|
+
} catch (error) {
|
|
1756
|
+
error.tokens = chain;
|
|
1757
|
+
throw error;
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
const surface = (spec) => {
|
|
1761
|
+
const layers = spec.split(">").map((s) => s.trim());
|
|
1762
|
+
let result = colour("surface-page").colour;
|
|
1763
|
+
const chain = [];
|
|
1764
|
+
for (const layer of layers.reverse()) {
|
|
1765
|
+
const c = colour(layer);
|
|
1766
|
+
chain.push(...c.chain);
|
|
1767
|
+
result = over(c.colour, result);
|
|
1768
|
+
}
|
|
1769
|
+
return { colour: result, chain };
|
|
1770
|
+
};
|
|
1771
|
+
for (const { fg, bg, min, kind, advisory } of expandPairs(pairs)) {
|
|
1772
|
+
try {
|
|
1773
|
+
const back = surface(bg);
|
|
1774
|
+
const front = colour(fg);
|
|
1775
|
+
const value = ratio(over(front.colour, back.colour), back.colour);
|
|
1776
|
+
results.push({ foreground: `--luz-${fg}`, background: bg, ratio: value, minimum: min, kind, advisory, ok: value + 1e-9 >= min, tokens: [...front.chain, ...back.chain] });
|
|
1777
|
+
} catch (error) {
|
|
1778
|
+
unreadable.push({ foreground: `--luz-${fg}`, background: bg, reason: error.message, tokens: error.tokens ?? [] });
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return { results, unreadable };
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// packages/check/src/rules/shared.js
|
|
1785
|
+
var SUGGESTIONS_PER_FILE = 200;
|
|
1786
|
+
var SUGGESTIONS_IN_ALL = 5e3;
|
|
1787
|
+
var PACKAGE_PREFIXES = [
|
|
1788
|
+
["luz-landing-", "landing"],
|
|
1789
|
+
["luz-cms-", "cms"],
|
|
1790
|
+
["luz-app-", "app"],
|
|
1791
|
+
["luz-docs-", "docs"],
|
|
1792
|
+
["luz-site-", "site"]
|
|
1793
|
+
];
|
|
1794
|
+
function packageOfName(name) {
|
|
1795
|
+
for (const [prefix, pkg] of PACKAGE_PREFIXES) if (name.startsWith(prefix)) return pkg;
|
|
1796
|
+
return "core";
|
|
1797
|
+
}
|
|
1798
|
+
var isEcho = (name) => echoName(name) === name;
|
|
1799
|
+
var TOKEN_SKIP = /[{}$:@[]/;
|
|
1800
|
+
var VALUE_SKIP = /[{}$<%@(]/;
|
|
1801
|
+
var SPACE2 = /[ \t\n\r\f]+/;
|
|
1802
|
+
var classAttribute = (record, template) => record.attrs.get("class") ?? (template ? record.attrs.get("classname") : void 0);
|
|
1803
|
+
function classTokens(value, template) {
|
|
1804
|
+
if (template && VALUE_SKIP.test(value)) return [];
|
|
1805
|
+
return value.split(SPACE2).filter((token) => token.length >= 3 && !TOKEN_SKIP.test(token));
|
|
1806
|
+
}
|
|
1807
|
+
function createBudget() {
|
|
1808
|
+
const total = /* @__PURE__ */ new Map();
|
|
1809
|
+
return () => {
|
|
1810
|
+
const own = /* @__PURE__ */ new Map();
|
|
1811
|
+
return (rule) => {
|
|
1812
|
+
const here = own.get(rule) ?? 0;
|
|
1813
|
+
const all = total.get(rule) ?? 0;
|
|
1814
|
+
if (here >= SUGGESTIONS_PER_FILE || all >= SUGGESTIONS_IN_ALL) return false;
|
|
1815
|
+
own.set(rule, here + 1);
|
|
1816
|
+
total.set(rule, all + 1);
|
|
1817
|
+
return true;
|
|
1818
|
+
};
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
function toHex(value) {
|
|
1822
|
+
try {
|
|
1823
|
+
const { r, g, b, a } = parseColor(value);
|
|
1824
|
+
return a === 1 ? `#${[r, g, b].map((n) => Math.round(n).toString(16).padStart(2, "0")).join("")}` : null;
|
|
1825
|
+
} catch {
|
|
1826
|
+
return null;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// packages/check/src/rules/load.js
|
|
1831
|
+
var TEMPLATE = /\{\{|<%|\$\{/;
|
|
1832
|
+
var IN_URL = /@luz-ds\/(tokens|core|site|landing|cms|app|docs)(?![\w-])/;
|
|
1833
|
+
var IN_DEV = /\/packages\/(tokens|core|site|landing|cms|app|docs)\//;
|
|
1834
|
+
var FILE = /^(?:luz(-tokens)?|(site|landing|cms|app|docs))(?:\.min)?\.css$/;
|
|
1835
|
+
var IMPORT = /@import\s+(?:url\(\s*)?["']?([^"')\s;]+)/g;
|
|
1836
|
+
var LAYER = /@layer\s+([^;{}@]{0,512})([;{}])/g;
|
|
1837
|
+
var STYLESHEET = /(?:^|\s)stylesheet(?:\s|$)/i;
|
|
1838
|
+
var REMOTE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
1839
|
+
var ORDER = ["tokens", "core", "site"];
|
|
1840
|
+
function classify(url) {
|
|
1841
|
+
if (TEMPLATE.test(url)) return "template";
|
|
1842
|
+
const named = IN_URL.exec(url) ?? IN_DEV.exec(url);
|
|
1843
|
+
if (named !== null) return { pkg: named[1], copy: false };
|
|
1844
|
+
const file = FILE.exec(url.split(/[?#]/)[0].split("/").pop().toLowerCase());
|
|
1845
|
+
if (file === null) return null;
|
|
1846
|
+
return { pkg: file[2] ?? (file[1] ? "tokens" : "core"), copy: file[2] !== void 0 };
|
|
1847
|
+
}
|
|
1848
|
+
function withoutComments(text) {
|
|
1849
|
+
let out = "";
|
|
1850
|
+
for (let from = 0; ; ) {
|
|
1851
|
+
const open = text.indexOf("/*", from);
|
|
1852
|
+
const close = open === -1 ? -1 : text.indexOf("*/", open + 2);
|
|
1853
|
+
if (close === -1) return out + (open === -1 ? text.slice(from) : "");
|
|
1854
|
+
out += text.slice(from, open);
|
|
1855
|
+
from = close + 2;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
function importsOf(css) {
|
|
1859
|
+
return css.includes("@import") ? [...withoutComments(css).matchAll(IMPORT)].map((m) => m[1]) : [];
|
|
1860
|
+
}
|
|
1861
|
+
function runPath(from, url) {
|
|
1862
|
+
const target = url.split(/[?#]/)[0];
|
|
1863
|
+
if (target === "" || REMOTE.test(target)) return null;
|
|
1864
|
+
return posix.normalize(target.startsWith("/") ? target.slice(1) : posix.join(posix.dirname(from), target));
|
|
1865
|
+
}
|
|
1866
|
+
function follow(url, from, imports, visit) {
|
|
1867
|
+
const seen = /* @__PURE__ */ new Set([from]);
|
|
1868
|
+
const work = [[url, from]];
|
|
1869
|
+
while (work.length > 0) {
|
|
1870
|
+
const [current, base] = work.pop();
|
|
1871
|
+
if (visit(current) !== false) continue;
|
|
1872
|
+
const next = runPath(base, current);
|
|
1873
|
+
if (next === null || seen.has(next)) continue;
|
|
1874
|
+
seen.add(next);
|
|
1875
|
+
const list = imports.get(next) ?? [];
|
|
1876
|
+
for (let i = list.length - 1; i >= 0; i--) work.push([list[i], next]);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
function reachesLuz(path, imports) {
|
|
1880
|
+
let reached = false;
|
|
1881
|
+
for (const url of imports.get(path) ?? []) {
|
|
1882
|
+
follow(url, path, imports, (current) => {
|
|
1883
|
+
const known = classify(current);
|
|
1884
|
+
if (known !== null) reached = true;
|
|
1885
|
+
return known !== null;
|
|
1886
|
+
});
|
|
1887
|
+
if (reached) return true;
|
|
1888
|
+
}
|
|
1889
|
+
return false;
|
|
1890
|
+
}
|
|
1891
|
+
function documentOf(records, path, imports) {
|
|
1892
|
+
const doc = { entries: [], styles: [], dynamic: false, bundled: false, inline: false, page: false };
|
|
1893
|
+
const add = (url, record, n) => follow(url, path, imports, (current) => {
|
|
1894
|
+
const known = classify(current);
|
|
1895
|
+
if (known === null) return false;
|
|
1896
|
+
if (known === "template") doc.dynamic = true;
|
|
1897
|
+
else doc.entries.push({ pkg: known.pkg, copy: known.copy, record, n });
|
|
1898
|
+
return true;
|
|
1899
|
+
});
|
|
1900
|
+
records.forEach((record, n) => {
|
|
1901
|
+
const { name, attrs } = record;
|
|
1902
|
+
if (name === "link") {
|
|
1903
|
+
const rel = attrs.get("rel");
|
|
1904
|
+
const href = attrs.get("href");
|
|
1905
|
+
if (rel !== void 0 && href !== void 0 && STYLESHEET.test(rel.value)) add(href.value, record, n);
|
|
1906
|
+
} else if (name === "style" && record.text) {
|
|
1907
|
+
for (const url of importsOf(record.text)) add(url, record, n);
|
|
1908
|
+
if (!record.text.includes("@layer")) return;
|
|
1909
|
+
for (const [, prelude, end] of withoutComments(record.text).matchAll(LAYER)) {
|
|
1910
|
+
const layers = prelude.split(",").map((layer) => layer.trim());
|
|
1911
|
+
if (layers.some((layer) => layer === "luz" || layer.startsWith("luz."))) doc.inline = true;
|
|
1912
|
+
if (end === ";" && layers.includes("luz")) doc.styles.push({ record, n });
|
|
1913
|
+
}
|
|
1914
|
+
} else if (name === "html" || name === "head") doc.page = true;
|
|
1915
|
+
else if (name === "script" && attrs.has("src") && (attrs.get("type")?.value.trim().toLowerCase() ?? "") === "module") doc.bundled = true;
|
|
1916
|
+
});
|
|
1917
|
+
return doc;
|
|
1918
|
+
}
|
|
1919
|
+
function loadRules(ctx) {
|
|
1920
|
+
const { file, records, world, doc, template } = ctx;
|
|
1921
|
+
const { entries } = doc;
|
|
1922
|
+
const found = [];
|
|
1923
|
+
const at2 = (record) => ({ file, line: record.line, column: record.column });
|
|
1924
|
+
if (!doc.dynamic && entries.length > 0) {
|
|
1925
|
+
if (!entries.some((entry) => entry.pkg === "tokens")) {
|
|
1926
|
+
const first2 = entries.find((entry) => !entry.copy);
|
|
1927
|
+
if (first2 !== void 0) found.push(finding("load/tokens-missing", { ...at2(first2.record), vars: { package: first2.pkg } }));
|
|
1928
|
+
}
|
|
1929
|
+
const loaded = new Set(entries.map((entry) => entry.pkg));
|
|
1930
|
+
const missing = /* @__PURE__ */ new Set();
|
|
1931
|
+
for (const record of records) {
|
|
1932
|
+
const owner = record.name.startsWith("luz-") ? world.tags.get(record.name)?.package : void 0;
|
|
1933
|
+
if (owner !== void 0 && !loaded.has(owner) && !missing.has(owner)) {
|
|
1934
|
+
missing.add(owner);
|
|
1935
|
+
found.push(finding("load/package-missing:tag", { ...at2(record), vars: { tag: record.name, package: owner } }));
|
|
1936
|
+
}
|
|
1937
|
+
const attribute = classAttribute(record, template);
|
|
1938
|
+
if (attribute === void 0) continue;
|
|
1939
|
+
for (const token of classTokens(attribute.value, template)) {
|
|
1940
|
+
const entry = world.info.get(token);
|
|
1941
|
+
if (entry === void 0 || entry.kind === "state" || entry.defined === false || token.startsWith("is-")) continue;
|
|
1942
|
+
if (loaded.has(entry.package) || missing.has(entry.package)) continue;
|
|
1943
|
+
missing.add(entry.package);
|
|
1944
|
+
found.push(finding("load/package-missing", { ...at2(attribute), vars: { name: token, package: entry.package } }));
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
const first = entries.find((entry) => !entry.copy);
|
|
1949
|
+
if (first !== void 0 && !doc.styles.some((style) => style.n < first.n)) {
|
|
1950
|
+
for (const style of doc.styles) if (style.n > first.n) found.push(finding("load/layer-statement", { ...at2(style.record), vars: { tag: "style" } }));
|
|
1951
|
+
}
|
|
1952
|
+
let top = null;
|
|
1953
|
+
for (const entry of entries) {
|
|
1954
|
+
if (entry.copy) continue;
|
|
1955
|
+
const rank = ORDER.includes(entry.pkg) ? ORDER.indexOf(entry.pkg) : 3;
|
|
1956
|
+
if (top !== null && rank < top.rank) {
|
|
1957
|
+
found.push(finding("load/order", { file, vars: { first: top.pkg, second: entry.pkg } }));
|
|
1958
|
+
break;
|
|
1959
|
+
}
|
|
1960
|
+
if (top === null || rank > top.rank) top = { rank, pkg: entry.pkg };
|
|
1961
|
+
}
|
|
1962
|
+
return found;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// packages/check/src/suggest.js
|
|
1966
|
+
var MAX_NAME = 64;
|
|
1967
|
+
var ROWS = [new Int32Array(MAX_NAME + 1), new Int32Array(MAX_NAME + 1), new Int32Array(MAX_NAME + 1)];
|
|
1968
|
+
function bounded(a, b, limit) {
|
|
1969
|
+
const n = a.length;
|
|
1970
|
+
const m = b.length;
|
|
1971
|
+
if (Math.abs(n - m) > limit) return limit + 1;
|
|
1972
|
+
const rows = m <= MAX_NAME ? ROWS : [new Int32Array(m + 1), new Int32Array(m + 1), new Int32Array(m + 1)];
|
|
1973
|
+
let [previous, previous2, row] = rows;
|
|
1974
|
+
for (let j = 0; j <= m; j++) previous[j] = j;
|
|
1975
|
+
for (let i = 1; i <= n; i++) {
|
|
1976
|
+
row[0] = i;
|
|
1977
|
+
let least = row[0];
|
|
1978
|
+
for (let j = 1; j <= m; j++) {
|
|
1979
|
+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
1980
|
+
let value = Math.min(previous[j] + 1, row[j - 1] + 1, previous[j - 1] + cost);
|
|
1981
|
+
if (i > 1 && j > 1 && a.charCodeAt(i - 1) === b.charCodeAt(j - 2) && a.charCodeAt(i - 2) === b.charCodeAt(j - 1)) {
|
|
1982
|
+
value = Math.min(value, previous2[j - 2] + 1);
|
|
1983
|
+
}
|
|
1984
|
+
row[j] = value;
|
|
1985
|
+
if (value < least) least = value;
|
|
1986
|
+
}
|
|
1987
|
+
if (least > limit) return limit + 1;
|
|
1988
|
+
const spare = previous2;
|
|
1989
|
+
previous2 = previous;
|
|
1990
|
+
previous = row;
|
|
1991
|
+
row = spare;
|
|
1992
|
+
}
|
|
1993
|
+
return Math.min(previous[m], limit + 1);
|
|
1994
|
+
}
|
|
1995
|
+
var limitOf = (length) => Math.min(Math.max(2, Math.ceil(length / 4)), Math.floor(0.4 * length));
|
|
1996
|
+
function closest(name, candidates, ceiling = Infinity) {
|
|
1997
|
+
const text = String(name);
|
|
1998
|
+
if (text.length === 0 || text.length > MAX_NAME) return null;
|
|
1999
|
+
let limit = Math.min(limitOf(text.length), ceiling);
|
|
2000
|
+
if (limit < 1) return null;
|
|
2001
|
+
let best = null;
|
|
2002
|
+
for (const candidate of candidates) {
|
|
2003
|
+
if (typeof candidate !== "string" || Math.abs(candidate.length - text.length) > limit) continue;
|
|
2004
|
+
const found = bounded(text, candidate, limit);
|
|
2005
|
+
if (found > limit) continue;
|
|
2006
|
+
if (best === null || found < limit || candidate < best) {
|
|
2007
|
+
best = candidate;
|
|
2008
|
+
limit = found;
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
return best;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// packages/check/src/rules/css.js
|
|
2015
|
+
var NOISE = /"(?:\\[\s\S]|[^"\\\n])*"|'(?:\\[\s\S]|[^'\\\n])*'|url\([^)]*\)/gi;
|
|
2016
|
+
var COLOUR2 = /#[0-9a-f]{3,8}(?![\w-])|(?<![\w-])(?:(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color(?:-mix)?)\(|(?:white|black)(?![\w-]))/i;
|
|
2017
|
+
var SIZE_PROPERTY = /^(?:font-size|(?:row-|column-)?gap|border-radius|(?:margin|padding)(?:-[a-z-]+)?)$/;
|
|
2018
|
+
var LENGTH = /(?<![\w.-])[-+]?(?:\d+\.?\d*|\.\d+)(?:px|rem)(?![\w-])/gi;
|
|
2019
|
+
var PIXELS = /(?<![\w.-])(?:\d+\.?\d*|\.\d+)px(?![\w-])/i;
|
|
2020
|
+
var IMPORTANT = /!\s*important(?![\w-])/i;
|
|
2021
|
+
var READ2 = /var\(\s*(--luz-[\w-]*)/g;
|
|
2022
|
+
var KNOBS = /* @__PURE__ */ new Set(["--luz-type-base", "--luz-space-unit"]);
|
|
2023
|
+
var textual = /^(?:color|fill|stroke|caret-color|text-decoration-color)$/;
|
|
2024
|
+
var lines = /^(?:border|outline)/;
|
|
2025
|
+
var tokenFor = (property) => textual.test(property) ? "--luz-text-primary" : lines.test(property) ? "--luz-outline" : property.startsWith("box-shadow") ? "--luz-shadow-sm" : "--luz-surface";
|
|
2026
|
+
function functionAt(value, index) {
|
|
2027
|
+
let depth = 0;
|
|
2028
|
+
for (let i = value.indexOf("(", index); i !== -1 && i < value.length; i++) {
|
|
2029
|
+
const c = value.charCodeAt(i);
|
|
2030
|
+
if (c === 40) depth++;
|
|
2031
|
+
else if (c === 41 && --depth === 0) return value.slice(index, i + 1);
|
|
2032
|
+
}
|
|
2033
|
+
return null;
|
|
2034
|
+
}
|
|
2035
|
+
function cssRules({ file, css, text, world, take }) {
|
|
2036
|
+
const { declarations } = css;
|
|
2037
|
+
const found = [];
|
|
2038
|
+
if (declarations.length === 0) return found;
|
|
2039
|
+
const starts = lineStarts(text);
|
|
2040
|
+
const hexes = /* @__PURE__ */ new Map();
|
|
2041
|
+
let facts;
|
|
2042
|
+
for (let at2 = 0; at2 < declarations.length; at2 += 3) {
|
|
2043
|
+
const start = declarations[at2];
|
|
2044
|
+
const name = text.slice(start, declarations[at2 + 1]).trim();
|
|
2045
|
+
const custom = name.startsWith("--");
|
|
2046
|
+
let value = text.slice(declarations[at2 + 1] + 1, declarations[at2 + 2]);
|
|
2047
|
+
if (value.includes("/*")) value = withoutComments(value);
|
|
2048
|
+
value = value.replace(NOISE, " ");
|
|
2049
|
+
const line = lineAt(starts, start);
|
|
2050
|
+
const where = { file, line, column: start - starts[line - 1] + 1 };
|
|
2051
|
+
const property = custom ? name : name.toLowerCase();
|
|
2052
|
+
if (IMPORTANT.test(value)) found.push(finding("css/important", { ...where, vars: { property } }));
|
|
2053
|
+
if (custom) {
|
|
2054
|
+
if (KNOBS.has(property) && PIXELS.test(value)) found.push(finding("css/knob-in-px", { ...where, vars: { property } }));
|
|
2055
|
+
} else {
|
|
2056
|
+
const colour = property.startsWith("font") ? null : COLOUR2.exec(value);
|
|
2057
|
+
if (colour !== null) {
|
|
2058
|
+
const literal = colour[0].endsWith("(") ? functionAt(value, colour.index) : colour[0];
|
|
2059
|
+
let hex = literal === null ? null : hexes.get(literal);
|
|
2060
|
+
if (hex === void 0) hexes.set(literal, hex = toHex(literal));
|
|
2061
|
+
const vars = { property, token: tokenFor(property) };
|
|
2062
|
+
found.push(hex === null ? finding("css/literal-colour:none", { ...where, vars }) : finding("css/literal-colour", { ...where, vars: { ...vars, value: hex } }));
|
|
2063
|
+
}
|
|
2064
|
+
if (SIZE_PROPERTY.test(property)) {
|
|
2065
|
+
for (const match of value.matchAll(LENGTH)) {
|
|
2066
|
+
if (Number.parseFloat(match[0]) === 0) continue;
|
|
2067
|
+
found.push(finding("css/literal-size", { ...where, vars: { property, value: match[0] } }));
|
|
2068
|
+
break;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
if (!value.includes("--luz-") && !property.startsWith("--luz-")) continue;
|
|
2073
|
+
const names = new Set(property.startsWith("--luz-") ? [property] : []);
|
|
2074
|
+
for (const match of value.matchAll(READ2)) names.add(match[1]);
|
|
2075
|
+
for (const token of names) {
|
|
2076
|
+
facts ??= world();
|
|
2077
|
+
if (facts === null) return found;
|
|
2078
|
+
const scope = facts.properties.get(token);
|
|
2079
|
+
if (scope === "root") continue;
|
|
2080
|
+
if (scope === "component") {
|
|
2081
|
+
found.push(finding("css/private-token", { ...where, vars: { token } }));
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
const near = isEcho(token) && take("css/unknown-token") ? closest(token, facts.publicNames) : null;
|
|
2085
|
+
found.push(near === null ? finding("css/unknown-token:none", { ...where, vars: { token } }) : finding("css/unknown-token", { ...where, vars: { token, closest: near }, suggestion: near }));
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
return found;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
// packages/check/src/rules/element.js
|
|
2092
|
+
var GLOBAL = /* @__PURE__ */ new Set([
|
|
2093
|
+
"accesskey",
|
|
2094
|
+
"anchor",
|
|
2095
|
+
"autocapitalize",
|
|
2096
|
+
"autocorrect",
|
|
2097
|
+
"autofocus",
|
|
2098
|
+
"class",
|
|
2099
|
+
"contenteditable",
|
|
2100
|
+
"dir",
|
|
2101
|
+
"draggable",
|
|
2102
|
+
"enterkeyhint",
|
|
2103
|
+
"exportparts",
|
|
2104
|
+
"hidden",
|
|
2105
|
+
"id",
|
|
2106
|
+
"inert",
|
|
2107
|
+
"inputmode",
|
|
2108
|
+
"is",
|
|
2109
|
+
"itemid",
|
|
2110
|
+
"itemprop",
|
|
2111
|
+
"itemref",
|
|
2112
|
+
"itemscope",
|
|
2113
|
+
"itemtype",
|
|
2114
|
+
"key",
|
|
2115
|
+
"lang",
|
|
2116
|
+
"nonce",
|
|
2117
|
+
"part",
|
|
2118
|
+
"popover",
|
|
2119
|
+
"ref",
|
|
2120
|
+
"role",
|
|
2121
|
+
"slot",
|
|
2122
|
+
"spellcheck",
|
|
2123
|
+
"style",
|
|
2124
|
+
"tabindex",
|
|
2125
|
+
"title",
|
|
2126
|
+
"translate",
|
|
2127
|
+
"virtualkeyboardpolicy",
|
|
2128
|
+
"writingsuggestions"
|
|
2129
|
+
]);
|
|
2130
|
+
var FRAMEWORK_PREFIX = ["v-", "x-", "hx-", "ng-", "on"];
|
|
2131
|
+
var BINDING = "@:*[(#{.";
|
|
2132
|
+
var PLAIN = /^[a-z_][a-z0-9_.-]*$/;
|
|
2133
|
+
function isAccepted(name, declared) {
|
|
2134
|
+
if (declared.has(name) || GLOBAL.has(name)) return true;
|
|
2135
|
+
if (name.startsWith("aria-")) return true;
|
|
2136
|
+
if (name.startsWith("data-")) return !name.startsWith("data-luz-") || name.startsWith("data-luz-label-");
|
|
2137
|
+
if (!PLAIN.test(name)) return true;
|
|
2138
|
+
if (FRAMEWORK_PREFIX.some((prefix) => name.startsWith(prefix))) return true;
|
|
2139
|
+
return BINDING.includes(name[0]);
|
|
2140
|
+
}
|
|
2141
|
+
function elementRules(ctx) {
|
|
2142
|
+
const { file, records, world, take } = ctx;
|
|
2143
|
+
const found = [];
|
|
2144
|
+
for (const record of records) {
|
|
2145
|
+
const tag = record.name;
|
|
2146
|
+
if (tag.endsWith("-")) continue;
|
|
2147
|
+
const declared = tag.startsWith("luz-") ? world.tags.get(tag) : void 0;
|
|
2148
|
+
if (tag.startsWith("luz-") && declared === void 0 && !world.blocks.has(tag)) {
|
|
2149
|
+
const near = isEcho(tag) && take("element/unknown") ? closest(tag, world.tagNames) : null;
|
|
2150
|
+
const where = { file, line: record.line, column: record.column };
|
|
2151
|
+
if (near === null) found.push(finding("element/unknown:none", { ...where, vars: { tag, package: packageOfName(tag) } }));
|
|
2152
|
+
else found.push(finding("element/unknown", { ...where, vars: { tag, closest: near }, suggestion: `<${near}>` }));
|
|
2153
|
+
continue;
|
|
2154
|
+
}
|
|
2155
|
+
for (const [name, attribute] of record.attrs) {
|
|
2156
|
+
const where = { file, line: attribute.line, column: attribute.column };
|
|
2157
|
+
if (declared !== void 0) {
|
|
2158
|
+
if (isAccepted(name, declared.attributes)) continue;
|
|
2159
|
+
const near = isEcho(name) && take("element/unknown-attribute") ? closest(name, declared.attributes) : null;
|
|
2160
|
+
if (near === null) found.push(finding("element/unknown-attribute:none", { ...where, vars: { attribute: name, tag } }));
|
|
2161
|
+
else found.push(finding("element/unknown-attribute", { ...where, vars: { attribute: name, tag, closest: near }, suggestion: near }));
|
|
2162
|
+
} else if (name.startsWith("data-luz-") && !name.startsWith("data-luz-label-") && !world.hooks.has(name)) {
|
|
2163
|
+
found.push(finding("attr/unknown-hook", { ...where, vars: { attribute: name } }));
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
return found;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
// packages/check/src/rules/class.js
|
|
2171
|
+
var LINK = /@luz-ds\/(tokens|core|site|landing|cms|app|docs)@(\d{1,9})\.(\d{1,9})\.(\d{1,9})/g;
|
|
2172
|
+
var SEMVER = /^(\d{1,9})\.(\d{1,9})\.(\d{1,9})/;
|
|
2173
|
+
var nameOf = /* @__PURE__ */ new WeakMap();
|
|
2174
|
+
function newerRelease(records, world) {
|
|
2175
|
+
for (const record of records) {
|
|
2176
|
+
const url = record.name === "link" ? record.attrs.get("href") : record.name === "script" ? record.attrs.get("src") : void 0;
|
|
2177
|
+
if (url === void 0 || !url.value.includes("@luz-ds/")) continue;
|
|
2178
|
+
for (const match of url.value.matchAll(LINK)) {
|
|
2179
|
+
const has2 = world.versions.get(match[1]);
|
|
2180
|
+
if (has2 === void 0 || has2.source !== "bundled") continue;
|
|
2181
|
+
const known = SEMVER.exec(has2.version);
|
|
2182
|
+
if (known === null) continue;
|
|
2183
|
+
const linked = [Number(match[2]), Number(match[3]), Number(match[4])];
|
|
2184
|
+
const older = [Number(known[1]), Number(known[2]), Number(known[3])];
|
|
2185
|
+
for (let i = 0; i < 3; i++) {
|
|
2186
|
+
if (linked[i] === older[i]) continue;
|
|
2187
|
+
if (linked[i] > older[i]) return { package: match[1], linked: linked.join("."), known: older.join(".") };
|
|
2188
|
+
break;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
return null;
|
|
2193
|
+
}
|
|
2194
|
+
function verdict(name, ctx) {
|
|
2195
|
+
const { world, defined } = ctx;
|
|
2196
|
+
if (name.startsWith("luz-")) return world.classes.has(name) ? null : { closest: void 0 };
|
|
2197
|
+
const group = name.startsWith("o-") ? world.byPrefix.o : name.startsWith("u-") ? world.byPrefix.u : null;
|
|
2198
|
+
if (group === null || world.classes.has(name) || defined.has(name)) return null;
|
|
2199
|
+
for (const known of group) if (name.length > known.length && name.startsWith(known)) return null;
|
|
2200
|
+
const near = closest(name, group, 2);
|
|
2201
|
+
return near === null ? null : { closest: near };
|
|
2202
|
+
}
|
|
2203
|
+
function memoOf(world) {
|
|
2204
|
+
let memo = nameOf.get(world);
|
|
2205
|
+
if (memo === void 0) {
|
|
2206
|
+
memo = /* @__PURE__ */ new Map();
|
|
2207
|
+
nameOf.set(world, memo);
|
|
2208
|
+
}
|
|
2209
|
+
return memo;
|
|
2210
|
+
}
|
|
2211
|
+
function unknownClasses(ctx) {
|
|
2212
|
+
const { file, records, template, take, world } = ctx;
|
|
2213
|
+
const found = [];
|
|
2214
|
+
const memo = memoOf(world);
|
|
2215
|
+
let newer;
|
|
2216
|
+
for (const record of records) {
|
|
2217
|
+
const attribute = classAttribute(record, template);
|
|
2218
|
+
if (attribute === void 0) continue;
|
|
2219
|
+
for (const token of classTokens(attribute.value, template)) {
|
|
2220
|
+
if (!token.startsWith("luz-") && !token.startsWith("o-") && !token.startsWith("u-")) continue;
|
|
2221
|
+
let entry = memo.get(token);
|
|
2222
|
+
if (entry === void 0) {
|
|
2223
|
+
entry = verdict(token, ctx);
|
|
2224
|
+
memo.set(token, entry);
|
|
2225
|
+
}
|
|
2226
|
+
if (entry === null) continue;
|
|
2227
|
+
let near = entry.closest;
|
|
2228
|
+
if (near === void 0) {
|
|
2229
|
+
near = null;
|
|
2230
|
+
if (isEcho(token) && take("class/unknown")) {
|
|
2231
|
+
near = closest(token, world.byPrefix.luz);
|
|
2232
|
+
entry.closest = near;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
const where = { file, line: attribute.line, column: attribute.column };
|
|
2236
|
+
const pkg = packageOfName(token);
|
|
2237
|
+
if (token.startsWith("luz-")) {
|
|
2238
|
+
if (newer === void 0) newer = newerRelease(records, world);
|
|
2239
|
+
if (newer !== null) {
|
|
2240
|
+
const details = { package: `@luz-ds/${newer.package}`, linked: newer.linked, known: newer.known };
|
|
2241
|
+
found.push(finding("class/unknown:newer", { ...where, vars: { name: token, known: newer.known, linked: newer.linked, package: pkg }, details }));
|
|
2242
|
+
continue;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
if (near === null) found.push(finding("class/unknown:none", { ...where, vars: { name: token, package: pkg } }));
|
|
2246
|
+
else found.push(finding("class/unknown", { ...where, vars: { name: token, closest: near }, suggestion: `.${near}` }));
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
return found;
|
|
2250
|
+
}
|
|
2251
|
+
function stylesheetClasses(ctx) {
|
|
2252
|
+
const { file, css, world, take } = ctx;
|
|
2253
|
+
const found = [];
|
|
2254
|
+
const memo = memoOf(world);
|
|
2255
|
+
for (const entry of css.classes) {
|
|
2256
|
+
const { name } = entry;
|
|
2257
|
+
if (!name.startsWith("luz-") || world.classes.has(name)) continue;
|
|
2258
|
+
const where = { file, line: entry.line, column: entry.column };
|
|
2259
|
+
if (entry.defined) {
|
|
2260
|
+
found.push(finding("class/reserved-prefix", { ...where, vars: { name } }));
|
|
2261
|
+
continue;
|
|
2262
|
+
}
|
|
2263
|
+
let near = null;
|
|
2264
|
+
if (isEcho(name) && take("class/unknown-in-css")) {
|
|
2265
|
+
const key = `css:${name}`;
|
|
2266
|
+
if (memo.has(key)) near = memo.get(key);
|
|
2267
|
+
else {
|
|
2268
|
+
near = closest(name, world.byPrefix.luz);
|
|
2269
|
+
memo.set(key, near);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
if (near === null) found.push(finding("class/unknown-in-css:none", { ...where, vars: { name, package: packageOfName(name) } }));
|
|
2273
|
+
else found.push(finding("class/unknown-in-css", { ...where, vars: { name, closest: near }, suggestion: `.${near}` }));
|
|
2274
|
+
}
|
|
2275
|
+
return found;
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
// packages/check/src/rules/misuse.js
|
|
2279
|
+
var TEMPLATE2 = /\{\{|<%|\$\{/;
|
|
2280
|
+
var COMPUTED = /[{}$<%@(]/;
|
|
2281
|
+
var CLASS_BINDING = /^(?:[:@]|v-bind:|x-bind:|\[|ng-)?(?:ng)?class(?:[:.\]].*)?$/;
|
|
2282
|
+
var HTML_NAME = /^[a-z][a-z0-9]*$/;
|
|
2283
|
+
var SELECTOR = /^([a-z0-9]+)(?:\[([a-z-]+)(?:=(.+))?\])?$/;
|
|
2284
|
+
var SPACE3 = /\s+/;
|
|
2285
|
+
var ROLES = new Map(
|
|
2286
|
+
["button button", "link a", "checkbox input", "radio input", "textbox input", "switch input", "dialog dialog", "navigation nav", "table table", "heading h1", "list ul", "main main"].map((pair) => pair.split(" "))
|
|
2287
|
+
);
|
|
2288
|
+
var has = (attrs, name) => attrs.has(name) || attrs.has(`:${name}`) || attrs.has(`[${name}]`) || attrs.has(`v-bind:${name}`) || attrs.has(`x-bind:${name}`);
|
|
2289
|
+
function matches(selector, record) {
|
|
2290
|
+
const [, tag, attribute, value] = SELECTOR.exec(selector);
|
|
2291
|
+
if (tag !== record.name) return false;
|
|
2292
|
+
if (attribute === void 0) return true;
|
|
2293
|
+
const own = record.attrs.get(attribute);
|
|
2294
|
+
if (own === void 0) return has(record.attrs, attribute);
|
|
2295
|
+
return value === void 0 || TEMPLATE2.test(own.value) || own.value.toLowerCase() === value;
|
|
2296
|
+
}
|
|
2297
|
+
function inside(record, block, template, memo) {
|
|
2298
|
+
let table = memo.get(block);
|
|
2299
|
+
if (table === void 0) memo.set(block, table = /* @__PURE__ */ new Map());
|
|
2300
|
+
const trail = [];
|
|
2301
|
+
let found = false;
|
|
2302
|
+
for (let ancestor = record.parent; ancestor !== null; ancestor = ancestor.parent) {
|
|
2303
|
+
const known = table.get(ancestor);
|
|
2304
|
+
if (known !== void 0) {
|
|
2305
|
+
found = known;
|
|
2306
|
+
break;
|
|
2307
|
+
}
|
|
2308
|
+
trail.push(ancestor);
|
|
2309
|
+
const attribute = classAttribute(ancestor, template);
|
|
2310
|
+
if (ancestor.name === block || attribute !== void 0 && (COMPUTED.test(attribute.value) || attribute.value.split(SPACE3).includes(block))) {
|
|
2311
|
+
found = true;
|
|
2312
|
+
break;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
for (const ancestor of trail) table.set(ancestor, found);
|
|
2316
|
+
return found;
|
|
2317
|
+
}
|
|
2318
|
+
function misuseRules(ctx) {
|
|
2319
|
+
const { file, records, world, defined, template } = ctx;
|
|
2320
|
+
const found = [];
|
|
2321
|
+
const memo = /* @__PURE__ */ new Map();
|
|
2322
|
+
for (const record of records) {
|
|
2323
|
+
const attribute = classAttribute(record, template);
|
|
2324
|
+
const tokens = attribute === void 0 ? [] : [...new Set(classTokens(attribute.value, template))];
|
|
2325
|
+
const own = attribute === void 0 ? null : { file, line: attribute.line, column: attribute.column };
|
|
2326
|
+
const plain = record.written === record.name && HTML_NAME.test(record.name) && !record.inert;
|
|
2327
|
+
let reported = false;
|
|
2328
|
+
if (plain) {
|
|
2329
|
+
for (const token of tokens) {
|
|
2330
|
+
const hosts = world.info.get(token)?.hosts;
|
|
2331
|
+
if (hosts === void 0 || hosts.some((host) => matches(host, record))) continue;
|
|
2332
|
+
const names = [...new Set(hosts.map((host) => host.split("[")[0]))];
|
|
2333
|
+
found.push(finding("misuse/host-element", { ...own, vars: { name: token, tag: record.name, hosts: names } }));
|
|
2334
|
+
reported = true;
|
|
2335
|
+
break;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
if (tokens.length > 0) {
|
|
2339
|
+
let bound = null;
|
|
2340
|
+
for (const token of tokens) {
|
|
2341
|
+
const entry = world.info.get(token);
|
|
2342
|
+
if (entry === void 0) continue;
|
|
2343
|
+
if (entry.modifier !== void 0) {
|
|
2344
|
+
const block = token.slice(0, token.length - entry.modifier.length - 2);
|
|
2345
|
+
if (record.name !== block && !tokens.includes(block)) {
|
|
2346
|
+
bound ??= COMPUTED.test(attribute.value) || [...record.attrs.keys()].some((name) => name !== "class" && CLASS_BINDING.test(name));
|
|
2347
|
+
if (!bound) found.push(finding("misuse/modifier-alone", { ...own, vars: { modifier: token, block } }));
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
if (entry.part !== void 0 && !inside(record, entry.block, template, memo)) {
|
|
2351
|
+
found.push(finding("misuse/part-orphan", { ...own, vars: { name: token, block: entry.block } }));
|
|
2352
|
+
}
|
|
2353
|
+
if (token.startsWith("is-") && !defined.has(token)) found.push(finding("misuse/state-class", { ...own, vars: { name: token } }));
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
if (!plain) continue;
|
|
2357
|
+
if ((record.name === "div" || record.name === "span") && !reported) {
|
|
2358
|
+
const role = record.attrs.get("role");
|
|
2359
|
+
const word = role === void 0 ? "" : role.value.trim().toLowerCase().split(SPACE3)[0];
|
|
2360
|
+
const onclick = record.attrs.get("onclick");
|
|
2361
|
+
if (ROLES.has(word)) {
|
|
2362
|
+
found.push(finding("a11y/role-on-generic", { file, line: role.line, column: role.column, vars: { role: word, tag: record.name, element: ROLES.get(word) } }));
|
|
2363
|
+
} else if (onclick !== void 0 && !has(record.attrs, "tabindex")) {
|
|
2364
|
+
found.push(finding("a11y/role-on-generic:onclick", { file, line: onclick.line, column: onclick.column, vars: { attribute: "onclick", tag: record.name } }));
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
if (record.name === "a" && !has(record.attrs, "href")) {
|
|
2368
|
+
const name = tokens.find((token) => world.info.get(token)?.hosts?.includes("a"));
|
|
2369
|
+
if (name !== void 0) found.push(finding("a11y/link-no-href", { file, line: record.line, column: record.column, vars: { tag: "a", name } }));
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
return found;
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
// packages/check/src/rules/index.js
|
|
2376
|
+
var LUZ_FILE = /(?:^|[/@])luz[-.a-z]*\.(?:css|js)(?:[?#]|$)/i;
|
|
2377
|
+
var PREFIXED = /(?:^|[ \t\n\r\f])(?:luz|o|u)-/;
|
|
2378
|
+
function namesLuz(parsed) {
|
|
2379
|
+
if (parsed.css !== null) return parsed.css.classes.some((entry) => entry.name.startsWith("luz-"));
|
|
2380
|
+
for (const record of parsed.records) {
|
|
2381
|
+
if (record.name.startsWith("luz-")) return true;
|
|
2382
|
+
for (const [name, attribute] of record.attrs) {
|
|
2383
|
+
if (name.startsWith("data-luz-")) return true;
|
|
2384
|
+
if ((name === "class" || name === "classname") && PREFIXED.test(attribute.value)) return true;
|
|
2385
|
+
if ((name === "href" || name === "src") && (attribute.value.includes("@luz-ds/") || LUZ_FILE.test(attribute.value))) return true;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
return false;
|
|
2389
|
+
}
|
|
2390
|
+
function checkParsed(parsed, ctx) {
|
|
2391
|
+
const base = { file: parsed.path, world: ctx.world, defined: ctx.defined, take: ctx.take };
|
|
2392
|
+
if (parsed.css !== null) return stylesheetClasses({ ...base, css: parsed.css });
|
|
2393
|
+
const markup = { ...base, records: parsed.records, template: parsed.kind === "template", doc: ctx.doc };
|
|
2394
|
+
return [...unknownClasses(markup), ...elementRules(markup), ...loadRules(markup), ...misuseRules(markup)];
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
// packages/tokens/lib/theme.js
|
|
2398
|
+
var BASE_MODES = ["light", "dark", "high-contrast"];
|
|
2399
|
+
var stripComments = (css) => css.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "));
|
|
2400
|
+
function parseRules(css) {
|
|
2401
|
+
const text = stripComments(css);
|
|
2402
|
+
const rules = [];
|
|
2403
|
+
let i = 0;
|
|
2404
|
+
let line = 1;
|
|
2405
|
+
function readPrelude() {
|
|
2406
|
+
let depth = 0;
|
|
2407
|
+
let quote = "";
|
|
2408
|
+
let start = i;
|
|
2409
|
+
let startLine = line;
|
|
2410
|
+
while (i < text.length && /\s/.test(text[i])) {
|
|
2411
|
+
if (text[i] === "\n") line++;
|
|
2412
|
+
i++;
|
|
2413
|
+
}
|
|
2414
|
+
start = i;
|
|
2415
|
+
startLine = line;
|
|
2416
|
+
for (; i < text.length; i++) {
|
|
2417
|
+
const c = text[i];
|
|
2418
|
+
if (c === "\n") line++;
|
|
2419
|
+
if (quote) {
|
|
2420
|
+
if (c === "\\") i++;
|
|
2421
|
+
else if (c === quote) quote = "";
|
|
2422
|
+
continue;
|
|
2423
|
+
}
|
|
2424
|
+
if (c === '"' || c === "'") quote = c;
|
|
2425
|
+
else if (c === "(" || c === "[") depth++;
|
|
2426
|
+
else if (c === ")" || c === "]") depth--;
|
|
2427
|
+
else if (depth <= 0 && (c === "{" || c === ";" || c === "}")) break;
|
|
2428
|
+
}
|
|
2429
|
+
return { prelude: text.slice(start, i).trim(), stop: text[i], line: startLine };
|
|
2430
|
+
}
|
|
2431
|
+
function readBlock(conditions, selectors, atLine) {
|
|
2432
|
+
const declarations = [];
|
|
2433
|
+
while (i < text.length) {
|
|
2434
|
+
const { prelude, stop, line: startLine } = readPrelude();
|
|
2435
|
+
if (stop === "{") {
|
|
2436
|
+
i++;
|
|
2437
|
+
if (prelude.startsWith("@")) {
|
|
2438
|
+
readBlockInto(prelude, conditions, selectors);
|
|
2439
|
+
} else {
|
|
2440
|
+
const inner = readBlock(conditions, prelude.split(",").map((s) => s.trim()).filter(Boolean), startLine);
|
|
2441
|
+
void inner;
|
|
2442
|
+
}
|
|
2443
|
+
} else {
|
|
2444
|
+
const colon = prelude.indexOf(":");
|
|
2445
|
+
if (prelude && !prelude.startsWith("@") && colon > 0) {
|
|
2446
|
+
const name = prelude.slice(0, colon).trim();
|
|
2447
|
+
if (name.startsWith("--luz-")) declarations.push({ name, value: prelude.slice(colon + 1).trim(), line: startLine });
|
|
2448
|
+
}
|
|
2449
|
+
if (stop === "}") {
|
|
2450
|
+
i++;
|
|
2451
|
+
break;
|
|
2452
|
+
}
|
|
2453
|
+
if (stop === ";") i++;
|
|
2454
|
+
else break;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
if (selectors && declarations.length) rules.push({ selectors, conditions, declarations, line: atLine });
|
|
2458
|
+
return declarations;
|
|
2459
|
+
}
|
|
2460
|
+
function readBlockInto(atPrelude, conditions, selectors) {
|
|
2461
|
+
readBlock([...conditions, atPrelude], selectors ?? null, line);
|
|
2462
|
+
}
|
|
2463
|
+
readBlock([], null, 1);
|
|
2464
|
+
return rules;
|
|
2465
|
+
}
|
|
2466
|
+
function readSelector(selector) {
|
|
2467
|
+
const compound = selector.trim().replace(/\s+/g, "");
|
|
2468
|
+
let rest = compound;
|
|
2469
|
+
let theme = null;
|
|
2470
|
+
let mode = null;
|
|
2471
|
+
let attrs = 0;
|
|
2472
|
+
let types = 0;
|
|
2473
|
+
if (/^html/i.test(rest)) {
|
|
2474
|
+
rest = rest.slice(4);
|
|
2475
|
+
types = 1;
|
|
2476
|
+
} else if (rest.startsWith("*")) rest = rest.slice(1);
|
|
2477
|
+
for (; ; ) {
|
|
2478
|
+
if (rest.startsWith(":root")) {
|
|
2479
|
+
rest = rest.slice(5);
|
|
2480
|
+
attrs++;
|
|
2481
|
+
continue;
|
|
2482
|
+
}
|
|
2483
|
+
const attr = rest.match(/^\[(data-theme|data-mode)=(?:"([\w-]+)"|'([\w-]+)'|([\w-]+))\]/);
|
|
2484
|
+
if (attr) {
|
|
2485
|
+
const value = attr[2] ?? attr[3] ?? attr[4];
|
|
2486
|
+
if (attr[1] === "data-theme") theme = value;
|
|
2487
|
+
else mode = value;
|
|
2488
|
+
rest = rest.slice(attr[0].length);
|
|
2489
|
+
attrs++;
|
|
2490
|
+
continue;
|
|
2491
|
+
}
|
|
2492
|
+
break;
|
|
2493
|
+
}
|
|
2494
|
+
if (rest !== "") return null;
|
|
2495
|
+
return { theme, mode, specificity: attrs * 100 + types };
|
|
2496
|
+
}
|
|
2497
|
+
var schemeOf = (prelude) => prelude.match(/^@media\s*\(\s*prefers-color-scheme\s*:\s*(light|dark)\s*\)$/i)?.[1].toLowerCase() ?? null;
|
|
2498
|
+
var transparent = (prelude) => /^@(layer|supports)\b/i.test(prelude);
|
|
2499
|
+
function targetsOf(rules) {
|
|
2500
|
+
const themes = /* @__PURE__ */ new Set();
|
|
2501
|
+
for (const { selectors } of rules) {
|
|
2502
|
+
for (const s of selectors) {
|
|
2503
|
+
const read = readSelector(s);
|
|
2504
|
+
if (read?.theme && !BASE_MODES.includes(read.theme)) themes.add(read.theme);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
const targets = BASE_MODES.map((name) => ({ name, theme: name, mode: null, scheme: name === "high-contrast" ? null : name }));
|
|
2508
|
+
for (const theme of [...themes].sort()) {
|
|
2509
|
+
targets.push({ name: theme, theme, mode: null, scheme: "light", custom: true });
|
|
2510
|
+
for (const mode of BASE_MODES) targets.push({ name: `${theme}/${mode}`, theme, mode, scheme: mode === "high-contrast" ? null : mode, custom: true });
|
|
2511
|
+
}
|
|
2512
|
+
return targets;
|
|
2513
|
+
}
|
|
2514
|
+
function valuesFor(target, sources) {
|
|
2515
|
+
const values = {};
|
|
2516
|
+
const origin = {};
|
|
2517
|
+
const rank = {};
|
|
2518
|
+
const ignored = [];
|
|
2519
|
+
let order2 = 0;
|
|
2520
|
+
for (const { file, rules } of sources) {
|
|
2521
|
+
for (const rule of rules) {
|
|
2522
|
+
order2++;
|
|
2523
|
+
let scheme = "any";
|
|
2524
|
+
let skip = null;
|
|
2525
|
+
for (const condition of rule.conditions) {
|
|
2526
|
+
const wanted = schemeOf(condition);
|
|
2527
|
+
if (wanted) scheme = wanted;
|
|
2528
|
+
else if (!transparent(condition)) skip = condition;
|
|
2529
|
+
}
|
|
2530
|
+
if (skip) {
|
|
2531
|
+
ignored.push({ file, line: rule.line, selector: rule.selectors.join(", "), reason: `${skip} is not evaluated` });
|
|
2532
|
+
continue;
|
|
2533
|
+
}
|
|
2534
|
+
if (scheme !== "any" && scheme !== target.scheme) continue;
|
|
2535
|
+
for (const selector of rule.selectors) {
|
|
2536
|
+
const read = readSelector(selector);
|
|
2537
|
+
if (!read) {
|
|
2538
|
+
if (file !== "tokens") ignored.push({ file, line: rule.line, selector, reason: "only html, :root, [data-theme] and [data-mode] selectors are read" });
|
|
2539
|
+
continue;
|
|
2540
|
+
}
|
|
2541
|
+
const themeOk = read.theme === null || read.theme === target.theme;
|
|
2542
|
+
const modeOk = read.mode === null ? true : read.mode === target.mode;
|
|
2543
|
+
if (!themeOk || !modeOk) continue;
|
|
2544
|
+
for (const { name, value, line } of rule.declarations) {
|
|
2545
|
+
const key = read.specificity * 1e6 + order2;
|
|
2546
|
+
if (rank[name] !== void 0 && rank[name] > key) continue;
|
|
2547
|
+
rank[name] = key;
|
|
2548
|
+
values[name] = value;
|
|
2549
|
+
origin[name] = { file, line };
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
return { values, origin, ignored };
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
// packages/tokens/lib/check.js
|
|
2558
|
+
function analyze(sources, { strict = false } = {}) {
|
|
2559
|
+
const mine = sources.slice(1);
|
|
2560
|
+
const targets = targetsOf(mine.flatMap((s) => s.rules));
|
|
2561
|
+
const failures = [];
|
|
2562
|
+
const warnings = [];
|
|
2563
|
+
const unreadable = /* @__PURE__ */ new Map();
|
|
2564
|
+
const ignored = /* @__PURE__ */ new Map();
|
|
2565
|
+
let checked = 0;
|
|
2566
|
+
const overridesOf = (tokens, values, origin) => [...new Set(tokens)].filter((t) => origin[t] && origin[t].file !== "tokens").map((t) => ({ token: t, value: values[t], file: origin[t].file, line: origin[t].line }));
|
|
2567
|
+
for (const target of targets) {
|
|
2568
|
+
const { values, origin, ignored: left } = valuesFor(target, sources);
|
|
2569
|
+
for (const item of left) ignored.set(`${item.file}:${item.line} ${item.selector}`, item);
|
|
2570
|
+
const { results, unreadable: bad } = measure(createResolver(values), pairsFor(target.name));
|
|
2571
|
+
for (const r of results) {
|
|
2572
|
+
checked++;
|
|
2573
|
+
if (r.ok) continue;
|
|
2574
|
+
(r.advisory && !strict ? warnings : failures).push({
|
|
2575
|
+
mode: target.name,
|
|
2576
|
+
foreground: r.foreground,
|
|
2577
|
+
background: r.background,
|
|
2578
|
+
ratio: Math.floor(r.ratio * 100) / 100,
|
|
2579
|
+
minimum: r.minimum,
|
|
2580
|
+
kind: r.kind,
|
|
2581
|
+
overrides: overridesOf(r.tokens, values, origin)
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
for (const b of bad) {
|
|
2585
|
+
unreadable.set(b.reason, { mode: target.name, foreground: b.foreground, background: b.background, reason: b.reason, overrides: overridesOf(b.tokens, values, origin) });
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
return {
|
|
2589
|
+
ok: failures.length === 0 && unreadable.size === 0,
|
|
2590
|
+
modes: targets.map((t) => t.name),
|
|
2591
|
+
checked,
|
|
2592
|
+
failures,
|
|
2593
|
+
warnings,
|
|
2594
|
+
unreadable: [...unreadable.values()],
|
|
2595
|
+
ignored: [...ignored.values()]
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
// packages/check/src/rules/theme.js
|
|
2600
|
+
var MAX_THEMES = 16;
|
|
2601
|
+
var WIDE = /^@supports\b[\s\S]*(?:^|[\s(:])(?:oklch|oklab|lab|lch|hwb|color|color-mix|light-dark)\(/i;
|
|
2602
|
+
var REFERENCE = /^var\(--luz-[A-Za-z0-9-]{1,56}\)$/;
|
|
2603
|
+
function rulesOf(text) {
|
|
2604
|
+
try {
|
|
2605
|
+
return parseRules(text);
|
|
2606
|
+
} catch {
|
|
2607
|
+
return null;
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
function colourTokens(rules) {
|
|
2611
|
+
const values = {};
|
|
2612
|
+
for (const rule of rules) for (const { name, value } of rule.declarations) values[name] = value;
|
|
2613
|
+
const resolve3 = createResolver(values);
|
|
2614
|
+
const names = /* @__PURE__ */ new Set();
|
|
2615
|
+
for (const name of Object.keys(values)) {
|
|
2616
|
+
try {
|
|
2617
|
+
parseColor(resolve3(name).value);
|
|
2618
|
+
names.add(name);
|
|
2619
|
+
} catch {
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
return names;
|
|
2623
|
+
}
|
|
2624
|
+
function readable(value) {
|
|
2625
|
+
if (/^var\(/i.test(value)) return true;
|
|
2626
|
+
try {
|
|
2627
|
+
parseColor(value);
|
|
2628
|
+
return true;
|
|
2629
|
+
} catch {
|
|
2630
|
+
return false;
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
var echoValue = (value) => toHex(value) ?? (REFERENCE.test(value) ? value : null);
|
|
2634
|
+
var byText3 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
2635
|
+
function themeFindings({ files, named, linked, tokensFor }) {
|
|
2636
|
+
const found = [];
|
|
2637
|
+
const wanted = new Set(named);
|
|
2638
|
+
const candidates = [];
|
|
2639
|
+
for (const { path, text } of files) {
|
|
2640
|
+
if (wanted.has(path) || text.includes("--luz-")) candidates.push({ path, text });
|
|
2641
|
+
}
|
|
2642
|
+
if (candidates.length === 0) return found;
|
|
2643
|
+
let tokens = tokensFor(candidates[0].path);
|
|
2644
|
+
let tokenRules = tokens === null ? null : rulesOf(tokens.text);
|
|
2645
|
+
if ((tokenRules === null || tokenRules.length === 0) && tokens?.source === "node_modules") {
|
|
2646
|
+
tokens = tokensFor(candidates[0].path, true);
|
|
2647
|
+
tokenRules = tokens === null ? null : rulesOf(tokens.text);
|
|
2648
|
+
}
|
|
2649
|
+
if (tokenRules === null || tokenRules.length === 0) return found;
|
|
2650
|
+
const colours = colourTokens(tokenRules);
|
|
2651
|
+
const themes = [];
|
|
2652
|
+
for (const { path, text } of candidates) {
|
|
2653
|
+
const rules = rulesOf(text);
|
|
2654
|
+
if (rules !== null && (wanted.has(path) || rules.some((rule) => rule.declarations.some((declaration) => colours.has(declaration.name))))) themes.push({ path, rules });
|
|
2655
|
+
}
|
|
2656
|
+
if (themes.length === 0) return found;
|
|
2657
|
+
const given = /* @__PURE__ */ new Map();
|
|
2658
|
+
named.forEach((path, index) => given.has(path) || given.set(path, index));
|
|
2659
|
+
const link = /* @__PURE__ */ new Map();
|
|
2660
|
+
linked.forEach((path, index) => link.has(path) || link.set(path, index));
|
|
2661
|
+
const rest = themes.filter((theme) => !given.has(theme.path)).sort((a, b) => (link.get(a.path) ?? Infinity) - (link.get(b.path) ?? Infinity) || byText3(a.path, b.path));
|
|
2662
|
+
const own = themes.filter((theme) => given.has(theme.path)).sort((a, b) => given.get(a.path) - given.get(b.path));
|
|
2663
|
+
const ordered = [...rest, ...own];
|
|
2664
|
+
const unlinked = rest.find((theme) => !link.has(theme.path));
|
|
2665
|
+
if (rest.length > 1 && unlinked !== void 0) found.push(finding("theme/advisory", { file: unlinked.path, vars: { source: "alphabetical" } }));
|
|
2666
|
+
const names = targetsOf(ordered.flatMap((theme) => theme.rules)).filter((target) => target.custom && target.mode === null);
|
|
2667
|
+
const dropped = new Set(names.slice(MAX_THEMES).map((target) => target.theme));
|
|
2668
|
+
let beyond = null;
|
|
2669
|
+
const sources = ordered.map((theme) => {
|
|
2670
|
+
const rules = [];
|
|
2671
|
+
for (const rule of theme.rules) {
|
|
2672
|
+
if (dropped.size > 0 && targetsOf([rule]).some((target) => target.custom && dropped.has(target.theme))) {
|
|
2673
|
+
beyond ??= theme.path;
|
|
2674
|
+
continue;
|
|
2675
|
+
}
|
|
2676
|
+
if (!rule.conditions.some((condition) => WIDE.test(condition))) {
|
|
2677
|
+
rules.push(rule);
|
|
2678
|
+
continue;
|
|
2679
|
+
}
|
|
2680
|
+
const kept = rule.declarations.filter((declaration) => readable(declaration.value));
|
|
2681
|
+
for (const declaration of rule.declarations) {
|
|
2682
|
+
if (!kept.includes(declaration)) found.push(finding("theme/advisory:wide", { file: theme.path, line: declaration.line, vars: { token: declaration.name } }));
|
|
2683
|
+
}
|
|
2684
|
+
if (kept.length > 0) rules.push({ ...rule, declarations: kept });
|
|
2685
|
+
}
|
|
2686
|
+
return { file: theme.path, rules };
|
|
2687
|
+
});
|
|
2688
|
+
if (beyond !== null) found.push(finding("theme/advisory:limit", { file: beyond, vars: { count: String(dropped.size), limit: String(MAX_THEMES) } }));
|
|
2689
|
+
const result = analyze([{ file: "tokens", rules: tokenRules }, ...sources]);
|
|
2690
|
+
const first = ordered[0].path;
|
|
2691
|
+
const places = (list, withValue) => list.map((o) => {
|
|
2692
|
+
const place = { token: echoName(o.token) };
|
|
2693
|
+
if (withValue) place.value = echoValue(o.value);
|
|
2694
|
+
place.file = displayPath(o.file);
|
|
2695
|
+
place.line = o.line;
|
|
2696
|
+
return place;
|
|
2697
|
+
});
|
|
2698
|
+
for (const failure of result.failures) {
|
|
2699
|
+
const { mode, foreground, background, ratio: ratio2, minimum, kind, overrides } = failure;
|
|
2700
|
+
const layers = background.split(/\s*>\s*/);
|
|
2701
|
+
if (layers.length > 1 && layers[layers.length - 1] !== "surface-page") layers.push("surface-page");
|
|
2702
|
+
found.push(
|
|
2703
|
+
finding(layers.length > 1 ? "theme/contrast:layers" : "theme/contrast", {
|
|
2704
|
+
file: overrides[0]?.file ?? first,
|
|
2705
|
+
vars: { mode, foreground: layers.length > 1 ? foreground.slice(6) : foreground, background: layers.join("/"), ratio: formatRatio(ratio2).slice(0, -2), minimum: String(minimum), kind },
|
|
2706
|
+
details: { mode: echoName(mode), foreground, background, ratio: ratio2, minimum, kind, overrides: places(overrides, true) }
|
|
2707
|
+
})
|
|
2708
|
+
);
|
|
2709
|
+
}
|
|
2710
|
+
for (const bad of result.unreadable) {
|
|
2711
|
+
const { mode, foreground, background, overrides } = bad;
|
|
2712
|
+
const last = overrides[overrides.length - 1];
|
|
2713
|
+
found.push(
|
|
2714
|
+
finding("theme/unreadable", {
|
|
2715
|
+
file: last?.file ?? first,
|
|
2716
|
+
vars: { token: last?.token ?? foreground },
|
|
2717
|
+
details: { mode: echoName(mode), foreground, background, overrides: places(overrides, false) }
|
|
2718
|
+
})
|
|
2719
|
+
);
|
|
2720
|
+
}
|
|
2721
|
+
const placed = /* @__PURE__ */ new Map();
|
|
2722
|
+
for (const theme of sources) {
|
|
2723
|
+
const lines2 = /* @__PURE__ */ new Map();
|
|
2724
|
+
for (const rule of theme.rules) lines2.set(rule.line, [...lines2.get(rule.line) ?? [], rule]);
|
|
2725
|
+
placed.set(theme.file, lines2);
|
|
2726
|
+
}
|
|
2727
|
+
for (const item of result.ignored) {
|
|
2728
|
+
for (const rule of placed.get(item.file)?.get(item.line) ?? []) {
|
|
2729
|
+
if (!rule.selectors.includes(item.selector) && rule.selectors.join(", ") !== item.selector) continue;
|
|
2730
|
+
const colour = rule.declarations.find((declaration) => colours.has(declaration.name));
|
|
2731
|
+
if (colour !== void 0) found.push(finding("theme/advisory:unplaced", { file: item.file, line: item.line, vars: { token: colour.name } }));
|
|
2732
|
+
break;
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
return found;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
// packages/check/src/cli.js
|
|
2739
|
+
var BUNDLED_DATA = fileURLToPath(new URL("../dist/data/", import.meta.url));
|
|
2740
|
+
var BUNDLED_AGENT = fileURLToPath(new URL("../dist/agents/luz-accessibility-reviewer.md", import.meta.url));
|
|
2741
|
+
var NO_MANIFEST = "No LUZ manifest to read: the files name LUZ, and neither node_modules/@luz-ds nor the data bundled with luz-check has one; reinstall @luz-ds/check.";
|
|
2742
|
+
var STYLE_TAG = /<style\b/i;
|
|
2743
|
+
var USAGE = `Usage: luz-check [paths\u2026] [--json] [--strict] [--config <file>] [--theme <file.css>] [--ext .astro,.vue]
|
|
2744
|
+
luz-check agent
|
|
2745
|
+
luz-check --version | --help
|
|
2746
|
+
|
|
2747
|
+
Reads the HTML and CSS you wrote and says which LUZ classes, elements, load
|
|
2748
|
+
order and theme colours are wrong. It runs offline and writes nothing. A path
|
|
2749
|
+
is a file, a folder (read recursively: .html, .htm and .css) or a glob such as
|
|
2750
|
+
"src/**/*.html"; with no path it reads the current folder.
|
|
2751
|
+
|
|
2752
|
+
--json print the report as JSON
|
|
2753
|
+
--strict fail on a warning as well
|
|
2754
|
+
--config <file> read this configuration instead of luz-check.json
|
|
2755
|
+
--theme <file> a theme stylesheet (repeatable)
|
|
2756
|
+
--ext <list> also read these extensions as HTML, such as .astro,.vue
|
|
2757
|
+
agent print the accessibility reviewer
|
|
2758
|
+
--version print the version
|
|
2759
|
+
--help show this text
|
|
2760
|
+
|
|
2761
|
+
Limits: 1 MiB a file, 128 MiB, 5 000 entries and 64 folders deep. It never
|
|
2762
|
+
follows a link; what it did not read is listed as skipped.
|
|
2763
|
+
|
|
2764
|
+
Exit code 0 when nothing blocks, 1 when a finding blocks, 2 for a usage error or nothing to read.
|
|
2765
|
+
|
|
2766
|
+
It reads source files only. It does not check names, labels, alt text,
|
|
2767
|
+
headings or the language of a page, and a run with no findings says nothing
|
|
2768
|
+
about them.`;
|
|
2769
|
+
var VALUE_OPTIONS = /* @__PURE__ */ new Set(["--config", "--theme", "--ext"]);
|
|
2770
|
+
var EXTENSION = /^\.?[A-Za-z0-9]{1,16}$/;
|
|
2771
|
+
var OPTION_HINT = "luz-check --help lists the options.";
|
|
2772
|
+
var shown = (value, max = 80) => displayPath([...String(value)].slice(0, max).join(""));
|
|
2773
|
+
var usageError = (text) => ({ code: 2, stdout: "", stderr: `${text}
|
|
2774
|
+
` });
|
|
2775
|
+
function parseArgs(args) {
|
|
2776
|
+
const options = { help: false, version: false, json: false, strict: false, config: null, themes: [], ext: [], paths: [] };
|
|
2777
|
+
let onlyPaths = false;
|
|
2778
|
+
for (let i = 0; i < args.length; i++) {
|
|
2779
|
+
const arg = args[i];
|
|
2780
|
+
if (onlyPaths || !arg.startsWith("-") || arg === "-") {
|
|
2781
|
+
options.paths.push(arg);
|
|
2782
|
+
continue;
|
|
2783
|
+
}
|
|
2784
|
+
if (arg === "--") onlyPaths = true;
|
|
2785
|
+
else if (arg === "--help" || arg === "-h") options.help = true;
|
|
2786
|
+
else if (arg === "--version" || arg === "-v") options.version = true;
|
|
2787
|
+
else if (arg === "--json") options.json = true;
|
|
2788
|
+
else if (arg === "--strict") options.strict = true;
|
|
2789
|
+
else {
|
|
2790
|
+
const equals = arg.indexOf("=");
|
|
2791
|
+
const name = equals === -1 ? arg : arg.slice(0, equals);
|
|
2792
|
+
if (!VALUE_OPTIONS.has(name)) return { error: `Unknown option ${shown(arg, 40)}; ${OPTION_HINT}` };
|
|
2793
|
+
const value = equals === -1 ? args[++i] : arg.slice(equals + 1);
|
|
2794
|
+
if (value === void 0 || value === "" || equals === -1 && value.startsWith("-")) return { error: `${name} needs a value; ${OPTION_HINT}` };
|
|
2795
|
+
if (name === "--config") options.config = value;
|
|
2796
|
+
else if (name === "--theme") options.themes.push(value);
|
|
2797
|
+
else {
|
|
2798
|
+
for (const part of value.split(",")) {
|
|
2799
|
+
const extension = part.trim();
|
|
2800
|
+
if (!EXTENSION.test(extension)) return { error: `--ext takes extensions such as .astro,.vue, not ${shown(extension, 20)}` };
|
|
2801
|
+
options.ext.push(extension.startsWith(".") ? extension : `.${extension}`);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
return options;
|
|
2807
|
+
}
|
|
2808
|
+
function parseFile(file) {
|
|
2809
|
+
if (file.kind === "css") {
|
|
2810
|
+
const css = readCss(file.text, file.path);
|
|
2811
|
+
const errors2 = css.findings.map((problem) => ({ line: problem.line, column: problem.column, kind: "CSS" }));
|
|
2812
|
+
return { path: file.path, kind: file.kind, text: file.text, css, records: [], errors: errors2 };
|
|
2813
|
+
}
|
|
2814
|
+
const records = [];
|
|
2815
|
+
const errors = [];
|
|
2816
|
+
for (const record of scan(file.text, file.path)) {
|
|
2817
|
+
if (record.type === "error") {
|
|
2818
|
+
errors.push({ line: record.line, column: record.column, kind: "HTML" });
|
|
2819
|
+
break;
|
|
2820
|
+
}
|
|
2821
|
+
records.push(record);
|
|
2822
|
+
}
|
|
2823
|
+
return { path: file.path, kind: file.kind, text: file.text, css: null, records, errors };
|
|
2824
|
+
}
|
|
2825
|
+
function definedClasses(files) {
|
|
2826
|
+
let names = null;
|
|
2827
|
+
const build = () => {
|
|
2828
|
+
names = /* @__PURE__ */ new Set();
|
|
2829
|
+
for (const file of files) {
|
|
2830
|
+
if (file.kind === "css") {
|
|
2831
|
+
for (const entry of readCss(file.text, file.path).classes) if (entry.defined) names.add(entry.name);
|
|
2832
|
+
} else if (STYLE_TAG.test(file.text)) {
|
|
2833
|
+
for (const record of scan(file.text, file.path)) {
|
|
2834
|
+
if (record.type !== "element" || record.name !== "style" || !record.text) continue;
|
|
2835
|
+
for (const entry of readCss(record.text, file.path).classes) if (entry.defined) names.add(entry.name);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
};
|
|
2840
|
+
return {
|
|
2841
|
+
has(name) {
|
|
2842
|
+
if (names === null) build();
|
|
2843
|
+
return names.has(name);
|
|
2844
|
+
}
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
function importsOfRun(files) {
|
|
2848
|
+
const imports = /* @__PURE__ */ new Map();
|
|
2849
|
+
for (const file of files) if (file.kind === "css" && file.text.includes("@import")) imports.set(file.path, importsOf(file.text));
|
|
2850
|
+
return imports;
|
|
2851
|
+
}
|
|
2852
|
+
function loadConfig(cwd, option) {
|
|
2853
|
+
const wanted = option === null ? resolve2(cwd, "luz-check.json") : resolve2(cwd, option);
|
|
2854
|
+
const name = displayPath(option === null ? "luz-check.json" : relative2(resolve2(cwd), wanted).split(sep2).join("/") || ".");
|
|
2855
|
+
let stat;
|
|
2856
|
+
try {
|
|
2857
|
+
stat = lstatSync2(wanted);
|
|
2858
|
+
} catch {
|
|
2859
|
+
if (option === null) return { config: parseConfig("{}"), file: null };
|
|
2860
|
+
throw new ConfigError(name, "was not found");
|
|
2861
|
+
}
|
|
2862
|
+
if (stat.isSymbolicLink()) throw new ConfigError(name, "is a link, which luz-check does not follow; name the file itself");
|
|
2863
|
+
if (!stat.isFile()) throw new ConfigError(name, "is not a file");
|
|
2864
|
+
const read = readLimited(wanted, 1 << 20);
|
|
2865
|
+
if (read.reason) throw new ConfigError(name, read.reason === "too-large" ? "is larger than 64 kB" : "could not be read");
|
|
2866
|
+
return { config: parseConfig(read.text, name), file: name };
|
|
2867
|
+
}
|
|
2868
|
+
function execute(argv, env) {
|
|
2869
|
+
if (!Array.isArray(argv) || argv.some((arg) => typeof arg !== "string")) return usageError(`luz-check takes text arguments; ${OPTION_HINT}`);
|
|
2870
|
+
if (argv.includes("--help") || argv.includes("-h")) return { code: 0, stdout: `${USAGE}
|
|
2871
|
+
`, stderr: "" };
|
|
2872
|
+
const version2 = env && typeof env.version === "string" ? env.version : null;
|
|
2873
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
2874
|
+
if (version2 === null) return usageError("luz-check has no version here.");
|
|
2875
|
+
return { code: 0, stdout: `@luz-ds/check ${cleanText(version2, 40)}
|
|
2876
|
+
`, stderr: "" };
|
|
2877
|
+
}
|
|
2878
|
+
const options = parseArgs(argv);
|
|
2879
|
+
if ("error" in options) return usageError(options.error);
|
|
2880
|
+
if (!env || typeof env.cwd !== "string" || version2 === null) return usageError("luz-check needs a working directory and a version.");
|
|
2881
|
+
const { cwd } = env;
|
|
2882
|
+
if (options.paths.length === 1 && options.paths[0] === "agent") {
|
|
2883
|
+
let reviewer;
|
|
2884
|
+
try {
|
|
2885
|
+
reviewer = readFileSync(BUNDLED_AGENT, "utf-8");
|
|
2886
|
+
} catch {
|
|
2887
|
+
return usageError("luz-check agent could not read the reviewer file that ships with this package; reinstall @luz-ds/check.");
|
|
2888
|
+
}
|
|
2889
|
+
return { code: 0, stdout: reviewer, stderr: "" };
|
|
2890
|
+
}
|
|
2891
|
+
const { config, file: configFile } = loadConfig(cwd, options.config);
|
|
2892
|
+
const strict = options.strict || config.strict;
|
|
2893
|
+
const inputs = options.paths.length > 0 ? options.paths : ["."];
|
|
2894
|
+
const extensions = [...new Set(options.ext)];
|
|
2895
|
+
const read = collect(inputs, { cwd, ext: extensions, ignore: config.ignore });
|
|
2896
|
+
if (read.missing.length > 0) {
|
|
2897
|
+
const names = read.missing.map((input) => shown(input)).join(", ");
|
|
2898
|
+
return usageError(`No file matches ${names}; luz-check reads .html, .htm and .css files and the extensions of --ext.`);
|
|
2899
|
+
}
|
|
2900
|
+
const themes = [...config.themes, ...options.themes];
|
|
2901
|
+
for (const theme of themes) {
|
|
2902
|
+
if (!theme.toLowerCase().endsWith(".css")) return usageError(`Theme ${shown(theme)} is not a .css file.`);
|
|
2903
|
+
}
|
|
2904
|
+
const themed = { files: [], skipped: [], missing: [] };
|
|
2905
|
+
for (const theme of themes) {
|
|
2906
|
+
const one = collect([theme], { cwd, ignore: [] });
|
|
2907
|
+
themed.files.push(...one.files);
|
|
2908
|
+
themed.skipped.push(...one.skipped);
|
|
2909
|
+
themed.missing.push(...one.missing);
|
|
2910
|
+
}
|
|
2911
|
+
if (themed.missing.length > 0) return usageError(`No theme file ${themed.missing.map((input) => shown(input)).join(", ")}.`);
|
|
2912
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
2913
|
+
for (const file of [...read.files, ...themed.files]) if (!byPath.has(file.path)) byPath.set(file.path, file);
|
|
2914
|
+
const skipped = [...read.skipped];
|
|
2915
|
+
for (const entry of themed.skipped) if (!skipped.some((s) => s.file === entry.file)) skipped.push(entry);
|
|
2916
|
+
const files = [...byPath.values()];
|
|
2917
|
+
if (files.length === 0) {
|
|
2918
|
+
const left = read.ignored > 0 ? ` (${read.ignored} left out by the ignore list)` : "";
|
|
2919
|
+
return usageError(`No HTML or CSS file to read${left}; luz-check reads .html, .htm and .css files and the extensions of --ext.`);
|
|
2920
|
+
}
|
|
2921
|
+
const data = typeof env.data === "string" && env.data !== "" ? env.data : BUNDLED_DATA;
|
|
2922
|
+
const facts = createFacts({ data });
|
|
2923
|
+
const defined = definedClasses(files);
|
|
2924
|
+
const imports = importsOfRun(files);
|
|
2925
|
+
const startFile = createBudget();
|
|
2926
|
+
const manifests = [];
|
|
2927
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2928
|
+
const findings = [];
|
|
2929
|
+
let missing = false;
|
|
2930
|
+
const worldOf = (directory) => {
|
|
2931
|
+
const world = facts.forDirectory(directory);
|
|
2932
|
+
if (world.packages.length === 0) {
|
|
2933
|
+
missing = true;
|
|
2934
|
+
return null;
|
|
2935
|
+
}
|
|
2936
|
+
for (const entry of world.packages) {
|
|
2937
|
+
const key = `${entry.package} ${entry.version} ${entry.source}`;
|
|
2938
|
+
if (!seen.has(key)) {
|
|
2939
|
+
seen.add(key);
|
|
2940
|
+
manifests.push(entry);
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
return world;
|
|
2944
|
+
};
|
|
2945
|
+
const linked = [];
|
|
2946
|
+
const named = [];
|
|
2947
|
+
let reached = false;
|
|
2948
|
+
for (const file of files) {
|
|
2949
|
+
const parsed = parseFile(file);
|
|
2950
|
+
for (const error of parsed.errors) {
|
|
2951
|
+
const key = file.kind === "template" ? "parse/error:template" : "parse/error";
|
|
2952
|
+
findings.push(finding(key, { file: file.path, line: error.line, column: error.column, vars: { kind: error.kind } }));
|
|
2953
|
+
}
|
|
2954
|
+
const directory = dirname2(resolve2(cwd, file.path));
|
|
2955
|
+
const take = startFile();
|
|
2956
|
+
let doc = null;
|
|
2957
|
+
if (file.kind === "css") {
|
|
2958
|
+
reached = reached || reachesLuz(file.path, imports);
|
|
2959
|
+
for (const found of cssRules({ file: file.path, css: parsed.css, text: file.text, world: () => worldOf(directory), take })) findings.push(found);
|
|
2960
|
+
if (missing) return usageError(NO_MANIFEST);
|
|
2961
|
+
} else {
|
|
2962
|
+
doc = documentOf(parsed.records, file.path, imports);
|
|
2963
|
+
reached = reached || doc.bundled || doc.dynamic || doc.inline || doc.entries.length > 0;
|
|
2964
|
+
for (const record of parsed.records) {
|
|
2965
|
+
const rel = record.name === "link" ? record.attrs.get("rel") : void 0;
|
|
2966
|
+
const href = rel !== void 0 && STYLESHEET.test(rel.value) ? record.attrs.get("href") : void 0;
|
|
2967
|
+
const target = href === void 0 ? null : runPath(file.path, href.value);
|
|
2968
|
+
if (target !== null) linked.push(target);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
if (!namesLuz(parsed)) continue;
|
|
2972
|
+
if (doc !== null && doc.page && file.kind === "html" && parsed.errors.length === 0) named.push(file.path);
|
|
2973
|
+
const world = worldOf(directory);
|
|
2974
|
+
if (world === null) return usageError(NO_MANIFEST);
|
|
2975
|
+
for (const found of checkParsed(parsed, { world, defined, take, doc })) findings.push(found);
|
|
2976
|
+
}
|
|
2977
|
+
const sheets = files.filter((file) => file.kind === "css");
|
|
2978
|
+
if (sheets.length > 0) {
|
|
2979
|
+
const given = [...new Set(themed.files.map((file) => file.path))];
|
|
2980
|
+
for (const found of themeFindings({ files: sheets, named: given, linked, tokensFor: (path, bundled) => facts.tokensFor(dirname2(resolve2(cwd, path)), bundled) })) findings.push(found);
|
|
2981
|
+
}
|
|
2982
|
+
if (named.length > 0 && !reached) findings.push(finding("load/no-stylesheet", { file: named[0], vars: { count: String(named.length) } }));
|
|
2983
|
+
const kept = findings.filter((found) => !(found.severity === "warn" && config.rules[found.rule] === "off"));
|
|
2984
|
+
const order2 = new Map(PACKAGES.map((name, index) => [`@luz-ds/${name}`, index]));
|
|
2985
|
+
manifests.sort((a, b) => order2.get(a.package) - order2.get(b.package));
|
|
2986
|
+
const report = buildReport({
|
|
2987
|
+
version: version2,
|
|
2988
|
+
files: files.map((file) => file.path),
|
|
2989
|
+
manifests,
|
|
2990
|
+
findings: kept,
|
|
2991
|
+
skipped,
|
|
2992
|
+
config: { file: configFile, off: Object.keys(config.rules) },
|
|
2993
|
+
ignored: read.ignored,
|
|
2994
|
+
strict
|
|
2995
|
+
});
|
|
2996
|
+
return { code: report.ok ? 0 : 1, stdout: options.json ? formatJson(report) : formatText(report), stderr: "" };
|
|
2997
|
+
}
|
|
2998
|
+
function run(argv, env) {
|
|
2999
|
+
try {
|
|
3000
|
+
return execute(argv, env);
|
|
3001
|
+
} catch (error) {
|
|
3002
|
+
if (error instanceof ConfigError) return usageError(cleanText(error.message, 200));
|
|
3003
|
+
return usageError("luz-check stopped on an internal error; no file was checked.");
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
// packages/check/src/main.js
|
|
3008
|
+
var manifest = fileURLToPath2(new URL("../package.json", import.meta.url));
|
|
3009
|
+
var { version } = JSON.parse(readFileSync2(manifest, "utf-8"));
|
|
3010
|
+
var { code, stdout, stderr } = run(process.argv.slice(2), { version, cwd: process.cwd() });
|
|
3011
|
+
if (stdout) process.stdout.write(stdout);
|
|
3012
|
+
if (stderr) process.stderr.write(stderr);
|
|
3013
|
+
process.exitCode = code;
|