@usefragments/core 1.5.2 → 1.7.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/dist/chunk-RANPUC6C.js +72 -0
- package/dist/chunk-RANPUC6C.js.map +1 -0
- package/dist/{chunk-BAHCOAVG.js → chunk-WNMWKUYG.js} +447 -201
- package/dist/chunk-WNMWKUYG.js.map +1 -0
- package/dist/codes/index.d.ts +2 -2
- package/dist/codes/index.js +1 -1
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/compiled-types/index.js +8 -0
- package/dist/generate/index.d.ts +1 -1
- package/dist/{governance-pKrfh517.d.ts → governance-D9KtH-vg.d.ts} +617 -22
- package/dist/index.d.ts +800 -179
- package/dist/index.js +1003 -237
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/registry.d.ts +36 -36
- package/dist/schemas/index.d.ts +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +2 -1
- package/src/__tests__/policy-exclude.test.ts +180 -0
- package/src/agent-format.test.ts +13 -0
- package/src/agent-format.ts +9 -3
- package/src/canonical-bridge.ts +69 -1
- package/src/canonical-direction.test.ts +118 -0
- package/src/canonical-direction.ts +43 -2
- package/src/codes/__tests__/codes.test.ts +14 -2
- package/src/codes/codes.ts +41 -2
- package/src/compiled-types/index.ts +81 -0
- package/src/compiled-types/parse.test.ts +47 -0
- package/src/facts/builders.ts +48 -0
- package/src/facts/compile.ts +148 -34
- package/src/facts/fact-index.ts +15 -2
- package/src/facts/facts.test.ts +44 -3
- package/src/facts/index.ts +11 -6
- package/src/facts/types.ts +60 -9
- package/src/governance-integrity.test.ts +272 -4
- package/src/governance-integrity.ts +345 -25
- package/src/governance.ts +87 -1
- package/src/index.ts +46 -1
- package/src/policy-exclude.ts +113 -0
- package/src/rules/a11y-required-accessible-name.ts +175 -28
- package/src/rules/a11y-standard.ts +102 -0
- package/src/rules/a11y-utils.ts +7 -0
- package/src/rules/components-prefer-library.test.ts +75 -28
- package/src/rules/components-prefer-library.ts +35 -15
- package/src/rules/components-shadow-component.test.ts +21 -9
- package/src/rules/emit-gate.test.ts +74 -4
- package/src/rules/emit-gate.ts +24 -9
- package/src/rules/families.test.ts +69 -0
- package/src/rules/families.ts +52 -0
- package/src/rules/fix-availability.ts +1 -0
- package/src/rules/index.ts +18 -2
- package/src/rules/jsx-preferred-import-path.ts +29 -11
- package/src/rules/rules.test.ts +188 -8
- package/src/rules/styles-no-raw-color.ts +13 -4
- package/src/rules/styles-no-raw-dimensions.ts +13 -4
- package/src/rules/styles-no-raw-spacing.ts +12 -4
- package/src/rules/styles-no-raw-typography.ts +13 -4
- package/src/rules/tiers.ts +1 -0
- package/src/rules/utils.ts +39 -0
- package/src/tokens/design-token-parser.test.ts +131 -0
- package/src/tokens/design-token-parser.ts +362 -49
- package/src/types.ts +2 -2
- package/dist/chunk-BAHCOAVG.js.map +0 -1
- package/dist/{index-DbkPE46t.d.ts → index-hZAlYCli.d.ts} +8 -8
|
@@ -175,4 +175,135 @@ describe("parseDesignTokenContent — CSS terminator robustness (#34/#35)", () =
|
|
|
175
175
|
expect(res.tokens.find((token) => token.name === "--sp-1")?.category).toBe("spacing");
|
|
176
176
|
expect(res.tokens.find((token) => token.name === "--fui-sp-2")?.category).toBe("spacing");
|
|
177
177
|
});
|
|
178
|
+
|
|
179
|
+
it("preserves the same custom property across configured theme selectors", () => {
|
|
180
|
+
const content = [
|
|
181
|
+
":root { --surface: #ffffff; --text: #111111; }",
|
|
182
|
+
'[data-theme="dark"] { --surface: #111111; --text: var(--surface); }',
|
|
183
|
+
".high-contrast { --surface: #000000; }",
|
|
184
|
+
].join("\n");
|
|
185
|
+
const res = parseDesignTokenContent(content, {
|
|
186
|
+
filePath: "tokens.css",
|
|
187
|
+
themeSelectors: {
|
|
188
|
+
":root": "default",
|
|
189
|
+
'[data-theme="dark"]': "dark",
|
|
190
|
+
".high-contrast": "high-contrast",
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(
|
|
195
|
+
res.tokens
|
|
196
|
+
.filter((token) => token.name === "--surface")
|
|
197
|
+
.map((token) => [token.theme, token.resolvedValue, token.selector])
|
|
198
|
+
).toEqual([
|
|
199
|
+
["default", "#ffffff", ":root"],
|
|
200
|
+
["dark", "#111111", '[data-theme="dark"]'],
|
|
201
|
+
["high-contrast", "#000000", ".high-contrast"],
|
|
202
|
+
]);
|
|
203
|
+
expect(
|
|
204
|
+
res.tokens.find((token) => token.name === "--text" && token.theme === "dark")?.resolvedValue
|
|
205
|
+
).toBe("#111111");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("preserves selector-specific values in compact single-line theme CSS", () => {
|
|
209
|
+
const content =
|
|
210
|
+
':root{--surface:#ffffff;--text:#111111}[data-theme="dark"]{--surface:#111111;--text:var(--surface)}.high-contrast{--surface:#000000}';
|
|
211
|
+
const res = parseDesignTokenContent(content, {
|
|
212
|
+
filePath: "tokens.min.css",
|
|
213
|
+
themeSelectors: {
|
|
214
|
+
":root": "default",
|
|
215
|
+
'[data-theme="dark"]': "dark",
|
|
216
|
+
".high-contrast": "high-contrast",
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(
|
|
221
|
+
res.tokens
|
|
222
|
+
.filter((token) => token.name === "--surface")
|
|
223
|
+
.map((token) => [token.theme, token.resolvedValue, token.selector])
|
|
224
|
+
).toEqual([
|
|
225
|
+
["default", "#ffffff", ":root"],
|
|
226
|
+
["dark", "#111111", '[data-theme="dark"]'],
|
|
227
|
+
["high-contrast", "#000000", ".high-contrast"],
|
|
228
|
+
]);
|
|
229
|
+
expect(
|
|
230
|
+
res.tokens.find((token) => token.name === "--text" && token.theme === "dark")?.resolvedValue
|
|
231
|
+
).toBe("#111111");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("uses the nearest selector through compact at-rules and ignores structural text in comments", () => {
|
|
235
|
+
const content =
|
|
236
|
+
':root{/* fake } [data-theme="dark"] { */--surface:#fff}@media(prefers-color-scheme:dark){[data-theme="dark"]{--surface:#111}}';
|
|
237
|
+
const res = parseDesignTokenContent(content, {
|
|
238
|
+
filePath: "tokens.min.css",
|
|
239
|
+
themeSelectors: {
|
|
240
|
+
":root": "default",
|
|
241
|
+
'[data-theme="dark"]': "dark",
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(
|
|
246
|
+
res.tokens
|
|
247
|
+
.filter((token) => token.name === "--surface")
|
|
248
|
+
.map((token) => [token.theme, token.resolvedValue, token.selector])
|
|
249
|
+
).toEqual([
|
|
250
|
+
["default", "#fff", ":root"],
|
|
251
|
+
["dark", "#111", '[data-theme="dark"]'],
|
|
252
|
+
]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("ignores declaration-shaped text in comments and strings while preserving quoted delimiters", () => {
|
|
256
|
+
const content =
|
|
257
|
+
':root{/* --comment-token:red; } */--label:"literal; { value }";--image:url("data:image/svg+xml;utf8,<svg>{}</svg>");--raw-image:url(data:image/svg+xml;utf8,<svg>{}</svg>);--real:#fff;content:"--string-token:blue;"}';
|
|
258
|
+
const res = parseDesignTokenContent(content, { filePath: "tokens.min.css" });
|
|
259
|
+
|
|
260
|
+
expect(res.tokens.map((token) => [token.name, token.rawValue])).toEqual([
|
|
261
|
+
["--label", '"literal; { value }"'],
|
|
262
|
+
["--image", 'url("data:image/svg+xml;utf8,<svg>{}</svg>")'],
|
|
263
|
+
["--raw-image", "url(data:image/svg+xml;utf8,<svg>{}</svg>)"],
|
|
264
|
+
["--real", "#fff"],
|
|
265
|
+
]);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("keeps the nearest theme selector through multiple nested compact at-rules", () => {
|
|
269
|
+
const content =
|
|
270
|
+
'@layer theme{@supports(--feature-probe:#fff){:root{--surface:#fff}@media(prefers-color-scheme:dark){[data-theme="dark"]{--surface:#111;--text:var(--surface)}}}}';
|
|
271
|
+
const res = parseDesignTokenContent(content, {
|
|
272
|
+
filePath: "tokens.min.css",
|
|
273
|
+
themeSelectors: {
|
|
274
|
+
":root": "default",
|
|
275
|
+
'[data-theme="dark"]': "dark",
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(
|
|
280
|
+
res.tokens.map((token) => [token.name, token.theme, token.selector, token.resolvedValue])
|
|
281
|
+
).toEqual([
|
|
282
|
+
["--surface", "default", ":root", "#fff"],
|
|
283
|
+
["--surface", "dark", '[data-theme="dark"]', "#111"],
|
|
284
|
+
["--text", "dark", '[data-theme="dark"]', "#111"],
|
|
285
|
+
]);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("handles thousands of compact declarations without losing selector context", () => {
|
|
289
|
+
const declarationCount = 4_000;
|
|
290
|
+
const declarations = Array.from(
|
|
291
|
+
{ length: declarationCount },
|
|
292
|
+
(_, index) => `--token-${index}:${index}px`
|
|
293
|
+
).join(";");
|
|
294
|
+
const res = parseDesignTokenContent(
|
|
295
|
+
`@layer theme{@media(min-width:0){[data-theme="dense"]{${declarations}}}}`,
|
|
296
|
+
{
|
|
297
|
+
filePath: "tokens.min.css",
|
|
298
|
+
themeSelectors: { '[data-theme="dense"]': "dense" },
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
expect(res.tokens).toHaveLength(declarationCount);
|
|
303
|
+
expect(res.tokens.at(-1)).toMatchObject({
|
|
304
|
+
name: `--token-${declarationCount - 1}`,
|
|
305
|
+
theme: "dense",
|
|
306
|
+
selector: '[data-theme="dense"]',
|
|
307
|
+
});
|
|
308
|
+
});
|
|
178
309
|
});
|
|
@@ -10,12 +10,7 @@ import { parseScssTokens } from "./scss.js";
|
|
|
10
10
|
import { containsTailwindV4Theme, parseTailwindV4Theme } from "./tailwind-v4.js";
|
|
11
11
|
import type { DesignTokenParseOptions, ParsedToken, TokenParseOutput } from "./types.js";
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
// so the final declaration in a block (no trailing `;` — valid CSS, #34) and a
|
|
15
|
-
// value that wraps across newlines (#35) are both captured without swallowing a
|
|
16
|
-
// following block. Matched over the FULL content string (not per-line) so the
|
|
17
|
-
// `[^;{}]+?` class can span `\n`. Terminates on `;`, the next `}`, or EOF.
|
|
18
|
-
const TOKEN_DECLARATION_PATTERN = /--([a-zA-Z0-9_-]+)\s*:\s*([^;{}]+?)\s*(?:;|(?=\})|$)/g;
|
|
13
|
+
const CSS_CUSTOM_PROPERTY_NAME_CHARACTER = /[a-zA-Z0-9_-]/u;
|
|
19
14
|
const VAR_REFERENCE_PATTERN = /var\(\s*--([a-zA-Z0-9_-]+)(?:\s*,\s*([^)]+))?\s*\)/;
|
|
20
15
|
|
|
21
16
|
export function parseDesignTokenContent(
|
|
@@ -98,25 +93,64 @@ export function parseDesignTokenContent(
|
|
|
98
93
|
}
|
|
99
94
|
}
|
|
100
95
|
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
96
|
+
const declarationsByThemeAndName = new Map<
|
|
97
|
+
string,
|
|
98
|
+
{
|
|
99
|
+
name: string;
|
|
100
|
+
rawValue: string;
|
|
101
|
+
line?: number;
|
|
102
|
+
selector: string;
|
|
103
|
+
theme: string;
|
|
104
|
+
}
|
|
105
|
+
>();
|
|
106
|
+
|
|
107
|
+
// Scan once over the full source. Besides resolving compact single-line
|
|
108
|
+
// selectors, the scanner distinguishes declarations from declaration-shaped
|
|
109
|
+
// text in comments/strings and respects delimiters inside CSS values.
|
|
110
|
+
const declarationMatches = scanCssCustomPropertyDeclarations(content);
|
|
111
|
+
for (const match of declarationMatches) {
|
|
112
|
+
const fullName = match.name;
|
|
113
|
+
const lineNumber = match.line;
|
|
114
|
+
const selector = match.selector;
|
|
115
|
+
const theme = themeSelectors[selector] || "default";
|
|
116
|
+
declarationsByThemeAndName.set(`${theme}\0${fullName}`, {
|
|
117
|
+
name: fullName,
|
|
118
|
+
rawValue: normalizeCssCustomPropertyValue(match.rawValue),
|
|
113
119
|
line: lineNumber,
|
|
120
|
+
selector,
|
|
121
|
+
theme,
|
|
114
122
|
});
|
|
115
123
|
}
|
|
116
124
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
const declarations = [...declarationsByThemeAndName.values()];
|
|
126
|
+
const defaults = new Map<string, { rawValue: string; line?: number }>();
|
|
127
|
+
const declarationsByTheme = new Map<string, Map<string, { rawValue: string; line?: number }>>();
|
|
128
|
+
for (const declaration of declarations) {
|
|
129
|
+
if (declaration.theme === "default") defaults.set(declaration.name, declaration);
|
|
130
|
+
let themeDeclarations = declarationsByTheme.get(declaration.theme);
|
|
131
|
+
if (!themeDeclarations) {
|
|
132
|
+
themeDeclarations = new Map();
|
|
133
|
+
declarationsByTheme.set(declaration.theme, themeDeclarations);
|
|
134
|
+
}
|
|
135
|
+
themeDeclarations.set(declaration.name, declaration);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const tokensByTheme = new Map<string, Map<string, { rawValue: string; line?: number }>>();
|
|
139
|
+
for (const [theme, themeDeclarations] of declarationsByTheme) {
|
|
140
|
+
if (theme === "default") {
|
|
141
|
+
tokensByTheme.set(theme, defaults);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const tokensByName = new Map(defaults);
|
|
145
|
+
for (const [name, declaration] of themeDeclarations) {
|
|
146
|
+
tokensByName.set(name, declaration);
|
|
147
|
+
}
|
|
148
|
+
tokensByTheme.set(theme, tokensByName);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const contentLines = content.split("\n");
|
|
152
|
+
for (const { name, rawValue, line, selector, theme } of declarations) {
|
|
153
|
+
const tokensByName = tokensByTheme.get(theme) ?? defaults;
|
|
120
154
|
const { resolvedValue, chain, hasCircular, unresolvedRef } = resolveDesignTokenValue(
|
|
121
155
|
rawValue,
|
|
122
156
|
tokensByName
|
|
@@ -141,7 +175,7 @@ export function parseDesignTokenContent(
|
|
|
141
175
|
lineNumber: line,
|
|
142
176
|
theme,
|
|
143
177
|
selector,
|
|
144
|
-
description: extractDescription(
|
|
178
|
+
description: extractDescription(contentLines, line ?? 1),
|
|
145
179
|
});
|
|
146
180
|
}
|
|
147
181
|
|
|
@@ -162,15 +196,6 @@ export function parseDesignTokenContent(
|
|
|
162
196
|
};
|
|
163
197
|
}
|
|
164
198
|
|
|
165
|
-
/** Count the `\n` characters in `content` up to (not including) `index`. */
|
|
166
|
-
function countNewlines(content: string, index: number): number {
|
|
167
|
-
let count = 0;
|
|
168
|
-
for (let i = 0; i < index && i < content.length; i++) {
|
|
169
|
-
if (content.charCodeAt(i) === 10) count++;
|
|
170
|
-
}
|
|
171
|
-
return count;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
199
|
/**
|
|
175
200
|
* Flatten a DTCG/JSON `TokenParseOutput` into `DesignToken[]`. Names pass through
|
|
176
201
|
* verbatim (parseDtcgTokens already emits `--kebab` CSS names via
|
|
@@ -384,34 +409,322 @@ function inferTokenLevel(name: string, rawValue: string, referenceChain: string[
|
|
|
384
409
|
return 2;
|
|
385
410
|
}
|
|
386
411
|
|
|
387
|
-
|
|
388
|
-
|
|
412
|
+
interface ScannedCssCustomProperty {
|
|
413
|
+
name: string;
|
|
414
|
+
rawValue: string;
|
|
415
|
+
line: number;
|
|
416
|
+
selector: string;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Extract custom-property declarations and their nearest non-at-rule selector
|
|
421
|
+
* in one pass. Comments and strings are lexical, not structural; declaration
|
|
422
|
+
* values may contain quoted or nested `;`, `{`, and `}` delimiters.
|
|
423
|
+
*/
|
|
424
|
+
function scanCssCustomPropertyDeclarations(content: string): ScannedCssCustomProperty[] {
|
|
425
|
+
const declarations: ScannedCssCustomProperty[] = [];
|
|
426
|
+
const parentSelectors: string[] = [];
|
|
389
427
|
let currentSelector = ":root";
|
|
390
|
-
let
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
428
|
+
let boundary = 0;
|
|
429
|
+
let statementHasContent = false;
|
|
430
|
+
let line = 1;
|
|
431
|
+
let quote: "'" | '"' | null = null;
|
|
432
|
+
let escaped = false;
|
|
433
|
+
let inComment = false;
|
|
434
|
+
let parentheses = 0;
|
|
435
|
+
let brackets = 0;
|
|
436
|
+
|
|
437
|
+
for (let index = 0; index < content.length; index++) {
|
|
438
|
+
const character = content[index];
|
|
439
|
+
const next = content[index + 1];
|
|
440
|
+
if (character === "\n") line++;
|
|
441
|
+
|
|
442
|
+
if (inComment) {
|
|
443
|
+
if (character === "*" && next === "/") {
|
|
444
|
+
inComment = false;
|
|
445
|
+
index++;
|
|
399
446
|
}
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (quote) {
|
|
450
|
+
if (escaped) {
|
|
451
|
+
escaped = false;
|
|
452
|
+
} else if (character === "\\") {
|
|
453
|
+
escaped = true;
|
|
454
|
+
} else if (character === quote) {
|
|
455
|
+
quote = null;
|
|
456
|
+
}
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (character === "/" && next === "*") {
|
|
460
|
+
inComment = true;
|
|
461
|
+
index++;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (character === "'" || character === '"') {
|
|
465
|
+
quote = character;
|
|
466
|
+
statementHasContent = true;
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (character === "\\") {
|
|
470
|
+
statementHasContent = true;
|
|
471
|
+
if (next === "\n") line++;
|
|
472
|
+
index++;
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (character === "(") {
|
|
476
|
+
parentheses++;
|
|
477
|
+
statementHasContent = true;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (character === ")") {
|
|
481
|
+
parentheses = Math.max(0, parentheses - 1);
|
|
482
|
+
statementHasContent = true;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (character === "[") {
|
|
486
|
+
brackets++;
|
|
487
|
+
statementHasContent = true;
|
|
488
|
+
continue;
|
|
400
489
|
}
|
|
490
|
+
if (character === "]") {
|
|
491
|
+
brackets = Math.max(0, brackets - 1);
|
|
492
|
+
statementHasContent = true;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (parentheses > 0 || brackets > 0) {
|
|
496
|
+
if (!/\s/u.test(character)) statementHasContent = true;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (character === "{") {
|
|
501
|
+
const header = stripCssComments(content.slice(boundary, index)).trim();
|
|
502
|
+
parentSelectors.push(currentSelector);
|
|
503
|
+
if (header && !header.startsWith("@")) currentSelector = header;
|
|
504
|
+
boundary = index + 1;
|
|
505
|
+
statementHasContent = false;
|
|
506
|
+
} else if (character === "}") {
|
|
507
|
+
currentSelector = parentSelectors.pop() ?? ":root";
|
|
508
|
+
boundary = index + 1;
|
|
509
|
+
statementHasContent = false;
|
|
510
|
+
} else if (character === ";") {
|
|
511
|
+
boundary = index + 1;
|
|
512
|
+
statementHasContent = false;
|
|
513
|
+
} else if (character === "-" && next === "-" && !statementHasContent) {
|
|
514
|
+
const declaration = scanCssCustomPropertyAt(content, index);
|
|
515
|
+
if (!declaration) {
|
|
516
|
+
statementHasContent = true;
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const rawValue = content.slice(declaration.valueStart, declaration.end).trim();
|
|
521
|
+
if (rawValue) {
|
|
522
|
+
declarations.push({
|
|
523
|
+
name: declaration.name,
|
|
524
|
+
rawValue,
|
|
525
|
+
line,
|
|
526
|
+
selector: currentSelector,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
line += countNewlinesInRange(content, index + 1, declaration.end);
|
|
530
|
+
statementHasContent = true;
|
|
531
|
+
index = declaration.end - 1;
|
|
532
|
+
} else if (!/\s/u.test(character)) {
|
|
533
|
+
statementHasContent = true;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
401
536
|
|
|
402
|
-
|
|
403
|
-
|
|
537
|
+
return declarations;
|
|
538
|
+
}
|
|
404
539
|
|
|
405
|
-
|
|
406
|
-
|
|
540
|
+
function scanCssCustomPropertyAt(
|
|
541
|
+
content: string,
|
|
542
|
+
start: number
|
|
543
|
+
): { name: string; valueStart: number; end: number } | null {
|
|
544
|
+
let cursor = start + 2;
|
|
545
|
+
while (
|
|
546
|
+
cursor < content.length &&
|
|
547
|
+
CSS_CUSTOM_PROPERTY_NAME_CHARACTER.test(content[cursor] ?? "")
|
|
548
|
+
) {
|
|
549
|
+
cursor++;
|
|
550
|
+
}
|
|
551
|
+
if (cursor === start + 2) return null;
|
|
552
|
+
|
|
553
|
+
const nameEnd = cursor;
|
|
554
|
+
cursor = skipCssWhitespaceAndComments(content, cursor);
|
|
555
|
+
if (content[cursor] !== ":") return null;
|
|
556
|
+
|
|
557
|
+
const valueStart = cursor + 1;
|
|
558
|
+
return {
|
|
559
|
+
name: content.slice(start, nameEnd),
|
|
560
|
+
valueStart,
|
|
561
|
+
end: findCssCustomPropertyValueEnd(content, valueStart),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function findCssCustomPropertyValueEnd(content: string, start: number): number {
|
|
566
|
+
let quote: "'" | '"' | null = null;
|
|
567
|
+
let escaped = false;
|
|
568
|
+
let inComment = false;
|
|
569
|
+
let parentheses = 0;
|
|
570
|
+
let brackets = 0;
|
|
571
|
+
let braces = 0;
|
|
572
|
+
|
|
573
|
+
for (let index = start; index < content.length; index++) {
|
|
574
|
+
const character = content[index];
|
|
575
|
+
const next = content[index + 1];
|
|
576
|
+
if (inComment) {
|
|
577
|
+
if (character === "*" && next === "/") {
|
|
578
|
+
inComment = false;
|
|
579
|
+
index++;
|
|
580
|
+
}
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
if (quote) {
|
|
584
|
+
if (escaped) {
|
|
585
|
+
escaped = false;
|
|
586
|
+
} else if (character === "\\") {
|
|
587
|
+
escaped = true;
|
|
588
|
+
} else if (character === quote) {
|
|
589
|
+
quote = null;
|
|
590
|
+
}
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (character === "/" && next === "*") {
|
|
594
|
+
inComment = true;
|
|
595
|
+
index++;
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (character === "'" || character === '"') {
|
|
599
|
+
quote = character;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (character === "\\") {
|
|
603
|
+
index++;
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (character === "(") {
|
|
607
|
+
parentheses++;
|
|
608
|
+
} else if (character === ")") {
|
|
609
|
+
parentheses = Math.max(0, parentheses - 1);
|
|
610
|
+
} else if (character === "[") {
|
|
611
|
+
brackets++;
|
|
612
|
+
} else if (character === "]") {
|
|
613
|
+
brackets = Math.max(0, brackets - 1);
|
|
614
|
+
} else if (character === "{") {
|
|
615
|
+
braces++;
|
|
616
|
+
} else if (character === "}") {
|
|
617
|
+
if (braces > 0) {
|
|
618
|
+
braces--;
|
|
619
|
+
} else if (parentheses === 0 && brackets === 0) {
|
|
620
|
+
return index;
|
|
621
|
+
}
|
|
622
|
+
} else if (character === ";" && parentheses === 0 && brackets === 0 && braces === 0) {
|
|
623
|
+
return index;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return content.length;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function skipCssWhitespaceAndComments(content: string, start: number): number {
|
|
631
|
+
let cursor = start;
|
|
632
|
+
while (cursor < content.length) {
|
|
633
|
+
if (/\s/u.test(content[cursor] ?? "")) {
|
|
634
|
+
cursor++;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (content[cursor] !== "/" || content[cursor + 1] !== "*") break;
|
|
638
|
+
const commentEnd = content.indexOf("*/", cursor + 2);
|
|
639
|
+
if (commentEnd < 0) return content.length;
|
|
640
|
+
cursor = commentEnd + 2;
|
|
641
|
+
}
|
|
642
|
+
return cursor;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function stripCssComments(content: string): string {
|
|
646
|
+
let output = "";
|
|
647
|
+
let quote: "'" | '"' | null = null;
|
|
648
|
+
let escaped = false;
|
|
649
|
+
let inComment = false;
|
|
650
|
+
|
|
651
|
+
for (let index = 0; index < content.length; index++) {
|
|
652
|
+
const character = content[index];
|
|
653
|
+
const next = content[index + 1];
|
|
654
|
+
if (inComment) {
|
|
655
|
+
if (character === "*" && next === "/") {
|
|
656
|
+
inComment = false;
|
|
657
|
+
index++;
|
|
658
|
+
}
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (quote) {
|
|
662
|
+
output += character;
|
|
663
|
+
if (escaped) {
|
|
664
|
+
escaped = false;
|
|
665
|
+
} else if (character === "\\") {
|
|
666
|
+
escaped = true;
|
|
667
|
+
} else if (character === quote) {
|
|
668
|
+
quote = null;
|
|
669
|
+
}
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (character === "/" && next === "*") {
|
|
673
|
+
inComment = true;
|
|
674
|
+
index++;
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
output += character;
|
|
678
|
+
if (character === "'" || character === '"') quote = character;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return output;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function countNewlinesInRange(content: string, start: number, end: number): number {
|
|
685
|
+
let count = 0;
|
|
686
|
+
for (let index = start; index < end; index++) {
|
|
687
|
+
if (content.charCodeAt(index) === 10) count++;
|
|
688
|
+
}
|
|
689
|
+
return count;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function normalizeCssCustomPropertyValue(value: string): string {
|
|
693
|
+
let output = "";
|
|
694
|
+
let quote: "'" | '"' | null = null;
|
|
695
|
+
let escaped = false;
|
|
696
|
+
let pendingWhitespace = false;
|
|
697
|
+
|
|
698
|
+
for (const character of value.trim()) {
|
|
699
|
+
if (quote) {
|
|
700
|
+
output += character;
|
|
701
|
+
if (escaped) {
|
|
702
|
+
escaped = false;
|
|
703
|
+
} else if (character === "\\") {
|
|
704
|
+
escaped = true;
|
|
705
|
+
} else if (character === quote) {
|
|
706
|
+
quote = null;
|
|
707
|
+
}
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
if (character === "'" || character === '"') {
|
|
711
|
+
if (pendingWhitespace && output) output += " ";
|
|
712
|
+
pendingWhitespace = false;
|
|
713
|
+
quote = character;
|
|
714
|
+
output += character;
|
|
715
|
+
} else if (/\s/u.test(character)) {
|
|
716
|
+
pendingWhitespace = true;
|
|
717
|
+
} else {
|
|
718
|
+
if (pendingWhitespace && output) output += " ";
|
|
719
|
+
pendingWhitespace = false;
|
|
720
|
+
output += character;
|
|
407
721
|
}
|
|
408
722
|
}
|
|
409
723
|
|
|
410
|
-
return
|
|
724
|
+
return output;
|
|
411
725
|
}
|
|
412
726
|
|
|
413
|
-
function extractDescription(
|
|
414
|
-
const lines = content.split("\n");
|
|
727
|
+
function extractDescription(lines: readonly string[], line: number): string | undefined {
|
|
415
728
|
if (line <= 1) return undefined;
|
|
416
729
|
|
|
417
730
|
const prevLine = lines[line - 2]?.trim();
|
package/src/types.ts
CHANGED
|
@@ -892,7 +892,7 @@ export type Theme = "light" | "dark";
|
|
|
892
892
|
*/
|
|
893
893
|
export interface Screenshot {
|
|
894
894
|
/** PNG image data */
|
|
895
|
-
data:
|
|
895
|
+
data: Uint8Array;
|
|
896
896
|
|
|
897
897
|
/** SHA-256 hash of image data (for change detection) */
|
|
898
898
|
hash: string;
|
|
@@ -944,7 +944,7 @@ export interface DiffResult {
|
|
|
944
944
|
totalPixels: number;
|
|
945
945
|
|
|
946
946
|
/** PNG image highlighting differences */
|
|
947
|
-
diffImage?:
|
|
947
|
+
diffImage?: Uint8Array;
|
|
948
948
|
|
|
949
949
|
/** Bounding boxes of changed regions */
|
|
950
950
|
changedRegions: BoundingBox[];
|