@miaskiewicz/turbo-dom 0.5.8 → 0.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.
@@ -0,0 +1,2803 @@
1
+ // CSSOM shorthand properties as Chrome 156 implements them (miaskiewicz/turbo-dom#90): the
2
+ // shorthand table, setting a shorthand (each longhand set — omitted ones to their initial value,
3
+ // or pending when the value holds var()), serializing a shorthand from its longhands
4
+ // (getPropertyValue) and a declaration block (cssText).
5
+ //
6
+ // The npm runtime's lockstep port of turbo-dom's Rust `rtdom::css_shorthand` (crates/turbo-dom/
7
+ // src/rtdom/css_shorthand/): same table, grammars and serializers, checked against the same
8
+ // Chrome 156 control (crates/turbo-dom/tests/fixtures/css_shorthand_chrome156.json, by
9
+ // test/css-shorthand-chrome.test.mjs). Change both together.
10
+ //
11
+ // PERF: only element.style reads/writes reach this module (test/app time).
12
+ import { canonicalizeColor } from './color.mjs';
13
+ import { SHORTHAND_TABLE, LONGHAND_ALIASES } from './css-shorthand-table.mjs';
14
+
15
+ // ---------------------------------------------------------------- numbers ----
16
+ const f32 = Math.fround;
17
+ /** The largest finite f32. */
18
+ const F32_MAX = 3.4028234663852886e38;
19
+ /** Blink's CSS number serialization: six significant digits (printf `%g`). */
20
+ export function cssNumber(value) {
21
+ const v = f32(value);
22
+ if (v === 0 || !Number.isFinite(v)) return '0';
23
+ const exponent = Math.floor(Math.log10(Math.abs(v)));
24
+ const trim = (text) => (text.includes('.') ? text.replace(/0+$/, '').replace(/\.$/, '') : text);
25
+ if (exponent < -5 || exponent >= 6) {
26
+ const [mantissa, power] = v.toExponential(5).split('e');
27
+ const p = Number(power);
28
+ return `${trim(mantissa)}e${p < 0 ? '-' : '+'}${String(Math.abs(p)).padStart(2, '0')}`;
29
+ }
30
+ return trim(v.toFixed(5 - exponent));
31
+ }
32
+
33
+ // --------------------------------------------------------------- tokenizer ----
34
+ // The CSS Syntax tokens the grammars read: ident, function, number / percentage / dimension,
35
+ // hash, string, url, delim, comma, (, ), [, ], whitespace. `null` for bad strings / urls.
36
+ const isNameStart = (c) => /[A-Za-z_\u0080-￿]/.test(c);
37
+ const isName = (c) => /[A-Za-z0-9_\-\u0080-￿]/.test(c);
38
+ function tokenize(text) {
39
+ const out = [];
40
+ let i = 0;
41
+ const n = text.length;
42
+ // CSS Syntax "consume an escaped code point": zero, a surrogate or past U+10FFFF is U+FFFD.
43
+ const escaped = (hex) => {
44
+ const code = parseInt(hex, 16);
45
+ return String.fromCodePoint(code === 0 || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff) ? 0xfffd : code);
46
+ };
47
+ const startsNumber = (at) => {
48
+ const c = text[at], d = text[at + 1], e = text[at + 2];
49
+ if (c === '+' || c === '-') return /[0-9]/.test(d || '') || (d === '.' && /[0-9]/.test(e || ''));
50
+ if (c === '.') return /[0-9]/.test(d || '');
51
+ return /[0-9]/.test(c || '');
52
+ };
53
+ const startsIdent = (at) => {
54
+ const c = text[at], d = text[at + 1];
55
+ // A backslash starts an escape unless a newline follows it.
56
+ const escape = (index) => text[index] === '\\' && text[index + 1] !== '\n';
57
+ if (c === '-') return isNameStart(d || '') || d === '-' || escape(at + 1);
58
+ if (c === '\\') return escape(at);
59
+ return isNameStart(c || '');
60
+ };
61
+ const readName = () => {
62
+ let name = '';
63
+ while (i < n) {
64
+ const c = text[i];
65
+ if (isName(c)) { name += c; i++; }
66
+ else if (c === '\\' && i + 1 >= n) { name += '\ufffd'; i++; }
67
+ else if (c === '\\' && text[i + 1] !== '\n') {
68
+ i++;
69
+ const hex = /^[0-9a-fA-F]{1,6}/.exec(text.slice(i));
70
+ if (hex) { name += escaped(hex[0]); i += hex[0].length; if (/\s/.test(text[i] || '')) i++; }
71
+ else { name += text[i]; i++; }
72
+ } else break;
73
+ }
74
+ return name;
75
+ };
76
+ while (i < n) {
77
+ const c = text[i];
78
+ if (c === '/' && text[i + 1] === '*') { const end = text.indexOf('*/', i + 2); i = end < 0 ? n : end + 2; continue; }
79
+ if (/\s/.test(c)) { while (i < n && /\s/.test(text[i])) i++; out.push({ type: 'ws' }); continue; }
80
+ if (c === '"' || c === "'") {
81
+ let value = ''; i++;
82
+ let closed = false;
83
+ while (i < n) {
84
+ const d = text[i];
85
+ if (d === c) { i++; closed = true; break; }
86
+ if (d === '\n') return null;
87
+ if (d === '\\') {
88
+ if (i + 1 >= n) { i++; continue; }
89
+ const e = text[i + 1];
90
+ if (e === '\n') { i += 2; continue; }
91
+ i++;
92
+ const hex = /^[0-9a-fA-F]{1,6}/.exec(text.slice(i));
93
+ if (hex) { value += escaped(hex[0]); i += hex[0].length; if (/\s/.test(text[i] || '')) i++; }
94
+ else { value += e; i++; }
95
+ continue;
96
+ }
97
+ value += d; i++;
98
+ }
99
+ void closed;
100
+ out.push({ type: 'string', value });
101
+ continue;
102
+ }
103
+ if (startsNumber(i)) {
104
+ const m = /^[+-]?(\d*\.\d+|\d+)([eE][+-]?\d+)?/.exec(text.slice(i));
105
+ const raw = m[0];
106
+ i += raw.length;
107
+ const value = Number(raw);
108
+ const integer = !/[.eE]/.test(raw);
109
+ if (text[i] === '%') { i++; out.push({ type: 'percentage', value, raw }); continue; }
110
+ if (startsIdent(i)) { out.push({ type: 'dimension', value, unit: readName() }); continue; }
111
+ out.push({ type: 'number', value, integer });
112
+ continue;
113
+ }
114
+ if (startsIdent(i)) {
115
+ const name = readName();
116
+ if (text[i] === '(') {
117
+ i++;
118
+ if (name.toLowerCase() === 'url') {
119
+ let j = i;
120
+ while (j < n && /\s/.test(text[j])) j++;
121
+ if (text[j] !== '"' && text[j] !== "'") {
122
+ let value = '';
123
+ i = j;
124
+ while (i < n && text[i] !== ')') {
125
+ const d = text[i];
126
+ if (/\s/.test(d)) { while (i < n && /\s/.test(text[i])) i++; if (i < n && text[i] !== ')') return null; break; }
127
+ if (d === '"' || d === "'" || d === '(') return null;
128
+ if (d === '\\') {
129
+ // An escaped newline makes a bad url.
130
+ if (text[i + 1] === '\n') return null;
131
+ i++;
132
+ const hex = /^[0-9a-fA-F]{1,6}/.exec(text.slice(i));
133
+ if (hex) { value += escaped(hex[0]); i += hex[0].length; if (/\s/.test(text[i] || '')) i++; }
134
+ else if (i < n) { value += text[i]; i++; }
135
+ else value += '\ufffd';
136
+ continue;
137
+ }
138
+ value += d; i++;
139
+ }
140
+ // The end of the input closes the url.
141
+ if (i < n) i++;
142
+ out.push({ type: 'url', value });
143
+ continue;
144
+ }
145
+ }
146
+ out.push({ type: 'function', name });
147
+ continue;
148
+ }
149
+ out.push({ type: 'ident', value: name });
150
+ continue;
151
+ }
152
+ if (c === '#' && i + 1 < n && (isName(text[i + 1]) || text[i + 1] === '\\')) { i++; out.push({ type: 'hash', value: readName() }); continue; }
153
+ i++;
154
+ if (c === ',') out.push({ type: 'comma' });
155
+ else if (c === '(' || c === '[' || c === '{') out.push({ type: 'open', value: c });
156
+ else if (c === ')' || c === ']' || c === '}') out.push({ type: 'close', value: c });
157
+ else if (c === ';') out.push({ type: 'semicolon' });
158
+ else out.push({ type: 'delim', value: c });
159
+ }
160
+ return out;
161
+ }
162
+
163
+ // ------------------------------------------------------------- components ----
164
+ const LENGTH_UNITS = new Set(['px', 'em', 'rem', 'ex', 'rex', 'ch', 'rch', 'ic', 'ric', 'cap', 'rcap', 'lh', 'rlh', 'vw', 'vh', 'vi', 'vb', 'vmin', 'vmax', 'svw', 'svh', 'svi', 'svb', 'svmin', 'svmax', 'lvw', 'lvh', 'lvi', 'lvb', 'lvmin', 'lvmax', 'dvw', 'dvh', 'dvi', 'dvb', 'dvmin', 'dvmax', 'cqw', 'cqh', 'cqi', 'cqb', 'cqmin', 'cqmax', 'cm', 'mm', 'q', 'in', 'pt', 'pc']);
165
+ const MATH_FUNCTIONS = new Set(['calc', 'min', 'max', 'clamp', 'round', 'mod', 'rem', 'abs', 'sign', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2', 'pow', 'sqrt', 'hypot', 'log', 'exp', 'calc-size']);
166
+ const IMAGE_FUNCTIONS = new Set(['linear-gradient', 'radial-gradient', 'conic-gradient', 'repeating-linear-gradient', 'repeating-radial-gradient', 'repeating-conic-gradient', '-webkit-linear-gradient', '-webkit-radial-gradient', '-webkit-repeating-linear-gradient', '-webkit-repeating-radial-gradient', '-webkit-gradient', 'image-set', '-webkit-image-set', 'cross-fade', '-webkit-cross-fade', 'paint', 'image']);
167
+ const COLOR_FUNCTIONS = new Set(['rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'lab', 'lch', 'oklab', 'oklch', 'color', 'color-mix', 'light-dark', 'contrast-color', '-webkit-device-cmyk']);
168
+ const SYSTEM_COLORS = new Set(['currentcolor', 'transparent', 'accentcolor', 'accentcolortext', 'activetext', 'buttonborder', 'buttonface', 'buttontext', 'canvas', 'canvastext', 'field', 'fieldtext', 'graytext', 'highlight', 'highlighttext', 'linktext', 'mark', 'marktext', 'selecteditem', 'selecteditemtext', 'visitedtext', 'activeborder', 'activecaption', 'appworkspace', 'background', 'buttonhighlight', 'buttonshadow', 'captiontext', 'inactiveborder', 'inactivecaption', 'inactivecaptiontext', 'infobackground', 'infotext', 'menu', 'menutext', 'scrollbar', 'threeddarkshadow', 'threedface', 'threedhighlight', 'threedlightshadow', 'threedshadow', 'window', 'windowframe', 'windowtext', '-webkit-link', '-webkit-focus-ring-color', '-internal-quirk-inherit']);
169
+ const CSS_WIDE = new Set(['initial', 'inherit', 'unset', 'revert', 'revert-layer']);
170
+
171
+ export const quoted = (text) => {
172
+ let out = '"';
173
+ for (const c of text) out += c === '"' ? '\\"' : c === '\\' ? '\\\\' : c === '\n' ? '\\a ' : c;
174
+ return `${out}"`;
175
+ };
176
+
177
+ /** Join components with single spaces, commas as `, `. */
178
+ function join(comps) {
179
+ let out = '';
180
+ comps.forEach((comp, index) => {
181
+ if (comp.kind === 'comma') { out += ','; return; }
182
+ if (index > 0) out += ' ';
183
+ out += comp.text;
184
+ });
185
+ return out;
186
+ }
187
+
188
+ /**
189
+ * The like-unit terms of a sum of products (operands: literals, the constants, nested
190
+ * parentheses / `calc()`, one-unit `min()` / `max()` / `clamp()` already simplified to
191
+ * `calc(...)`), absolute lengths combined in px; `null` when Chrome keeps it unsimplified.
192
+ */
193
+ function sumTerms(inner) {
194
+ // Operands alternate with operators; products bind first.
195
+ if (inner.length % 2 === 0) return null;
196
+ const CONSTANTS = { e: Math.E, pi: Math.PI, infinity: Infinity, '-infinity': -Infinity, nan: NaN };
197
+ const operand = (comp) => {
198
+ if (comp.kind === 'number') return [['', comp.value]];
199
+ if (comp.kind === 'percentage') return [['%', comp.value]];
200
+ if (comp.kind === 'dimension') return [[comp.unit, comp.value]];
201
+ if (comp.kind === 'ident') { const name = comp.value.toLowerCase(); return name in CONSTANTS ? [['', CONSTANTS[name]]] : null; }
202
+ if (comp.kind === 'function' && comp.name === '(') { const nested = components(comp.text.slice(1, -1)); return nested && sumTerms(nested); }
203
+ // A nested math function Chrome simplified already spells `calc(...)`.
204
+ if (isMath(comp) && comp.text.startsWith('calc(')) {
205
+ const nested = components(comp.text.slice(5, -1));
206
+ return nested && sumTerms(nested);
207
+ }
208
+ return null;
209
+ };
210
+ const scalar = (terms) => (terms.length === 1 && terms[0][0] === '' ? terms[0][1] : null);
211
+ const scale = (terms, factor) => terms.map(([unit, value]) => [unit, value * factor]);
212
+ const terms = [];
213
+ const push = (product, sign) => {
214
+ for (const [unit, value] of product) { const existing = terms.find((t) => t[0] === unit); if (existing) existing[1] += sign * value; else terms.push([unit, sign * value]); }
215
+ };
216
+ let sign = 1;
217
+ let current = operand(inner[0]);
218
+ if (!current) return null;
219
+ for (let index = 1; index < inner.length; index += 2) {
220
+ const next = operand(inner[index + 1]);
221
+ if (!next) return null;
222
+ const op = inner[index];
223
+ if (op.kind === 'delim' && op.value === '*') {
224
+ // calcType admitted at most one dimension: when the right side is not the scalar, the left is.
225
+ const b = scalar(next);
226
+ current = b !== null ? scale(current, b) : scale(next, scalar(current));
227
+ } else if (op.kind === 'slash' || (op.kind === 'delim' && op.value === '/')) {
228
+ const b = scalar(next);
229
+ if (b === null) return null;
230
+ current = scale(current, 1 / b);
231
+ } else if (op.kind === 'delim' && (op.value === '+' || op.value === '-')) {
232
+ push(current, sign);
233
+ sign = op.value === '+' ? 1 : -1;
234
+ current = next;
235
+ } else return null;
236
+ }
237
+ push(current, sign);
238
+ // Absolute lengths combine in px.
239
+ const ABSOLUTE = { in: 96, cm: 96 / 2.54, mm: 96 / 25.4, q: 96 / 101.6, pt: 96 / 72, pc: 16 };
240
+ if (terms.some(([unit]) => unit in ABSOLUTE)) {
241
+ let px = 0;
242
+ const rest = [];
243
+ for (const [unit, value] of terms) {
244
+ if (unit in ABSOLUTE) px += value * ABSOLUTE[unit];
245
+ else if (unit === 'px') px += value;
246
+ else rest.push([unit, value]);
247
+ }
248
+ rest.push(['px', px]);
249
+ terms.length = 0;
250
+ terms.push(...rest);
251
+ }
252
+ // Times, angles and frequencies resolve to their canonical unit (`calc(1ms)` is `calc(0.001s)`).
253
+ for (const [canonical, factors] of [['s', { s: 1, ms: 0.001 }], ['deg', { deg: 1, grad: 0.9, rad: 180 / Math.PI, turn: 360 }], ['hz', { hz: 1, khz: 1000 }]]) {
254
+ if (!terms.some(([unit]) => unit in factors)) continue;
255
+ let total = 0;
256
+ for (let index = terms.length - 1; index >= 0; index--) if (terms[index][0] in factors) { total += terms[index][1] * factors[terms[index][0]]; terms.splice(index, 1); }
257
+ terms.push([canonical, total]);
258
+ }
259
+ return terms;
260
+ }
261
+ function calcSum(inner) {
262
+ const terms = sumTerms(inner);
263
+ return terms && calcText(terms);
264
+ }
265
+ const unitCategory = (unit) => (LENGTH_UNITS.has(unit) ? 0 : unit === 's' || unit === 'ms' ? 1 : ['deg', 'grad', 'rad', 'turn'].includes(unit) ? 2 : unit === 'hz' || unit === 'khz' ? 3 : ['dpi', 'dpcm', 'dppx', 'x'].includes(unit) ? 4 : null);
266
+ /**
267
+ * The type of a `calc()` sum (`number`, `percent`, a unit category, `unknown` for a nested
268
+ * math function), `null` when Chrome rejects it: a sum mixes numbers and dimensions or two
269
+ * dimension categories, a product multiplies two dimensions, or a divisor has a unit.
270
+ */
271
+ function calcType(inner) {
272
+ const operand = (comp) => {
273
+ if (comp.kind === 'number') return 'number';
274
+ if (comp.kind === 'percentage') return 'percent';
275
+ if (comp.kind === 'dimension') { const category = unitCategory(comp.unit); return category === null ? null : `unit${category}`; }
276
+ if (comp.kind === 'function' && comp.name === '(') { const nested = components(comp.text.slice(1, -1)); return nested && calcType(nested); }
277
+ if (isMath(comp)) return mathType(comp);
278
+ if (comp.kind === 'ident' && ['e', 'pi', 'infinity', '-infinity', 'nan'].includes(comp.value.toLowerCase())) return 'number';
279
+ return null;
280
+ };
281
+ const unit = unitType, sum = sumType;
282
+ if (!inner.length) return null;
283
+ let total = null;
284
+ let product = operand(inner[0]);
285
+ if (product === null) return null;
286
+ for (let index = 1; index < inner.length; index += 2) {
287
+ const operator = inner[index], next = inner[index + 1] && operand(inner[index + 1]);
288
+ if (!next) return null;
289
+ if (operator.kind === 'delim' && operator.value === '*') {
290
+ if (unit(product) && unit(next)) return null;
291
+ if (product === 'number' || product === 'unknown') product = next;
292
+ } else if (operator.kind === 'slash' || (operator.kind === 'delim' && operator.value === '/')) {
293
+ if (unit(next)) return null;
294
+ } else if (operator.kind === 'delim' && (operator.value === '+' || operator.value === '-')) {
295
+ total = total === null ? product : sum(total, product);
296
+ if (total === null) return null;
297
+ product = next;
298
+ } else return null;
299
+ }
300
+ return total === null ? product : sum(total, product);
301
+ }
302
+ const unitType = (type) => type === 'percent' || type.startsWith('unit');
303
+ /** The type of a sum of two types (`null`: Chrome rejects adding them). */
304
+ function sumType(a, b) {
305
+ if (a === 'unknown') return b;
306
+ if (b === 'unknown') return a;
307
+ if (a === 'number' && b === 'number') return 'number';
308
+ if (a === 'percent' && unitType(b)) return b;
309
+ if (b === 'percent' && unitType(a)) return a;
310
+ return a.startsWith('unit') && a === b ? a : null;
311
+ }
312
+ const isMath = (c) => c.kind === 'function' && MATH_FUNCTIONS.has(c.name);
313
+ /**
314
+ * A math function's type: a simplified one's `calc()` sum, `min()` / `max()` / `clamp()` /
315
+ * `abs()` their arguments' common type, `sign()` a number, the others unknown.
316
+ */
317
+ function mathType(comp) {
318
+ const inner = innerOf(comp);
319
+ if (comp.text.startsWith('calc(')) return calcType(inner);
320
+ if (comp.name === 'sign') return 'number';
321
+ return ['min', 'max', 'clamp', 'abs'].includes(comp.name) ? argumentsType(inner) : 'unknown';
322
+ }
323
+ /** A function's comma-separated arguments. */
324
+ const argumentsOf = (inner) => split(inner, isComma);
325
+ /**
326
+ * A `calc()` sum's type when Chrome accepts it: operands alternate with operators (a literal
327
+ * product has at most one unit) and the types add up.
328
+ */
329
+ function checkedSum(inner) {
330
+ if (!inner.length || inner.length % 2 === 0) return null;
331
+ const unit = (c) => c.kind === 'dimension' || c.kind === 'percentage';
332
+ const alternates = inner.every((comp, index) => {
333
+ if (index % 2 === 1) {
334
+ const product = comp.kind === 'delim' && comp.value === '*';
335
+ const operator = product || (comp.kind === 'delim' && ['+', '-', '/'].includes(comp.value)) || comp.kind === 'slash';
336
+ return operator && !(product && unit(inner[index - 1]) && unit(inner[index + 1]));
337
+ }
338
+ return comp.kind !== 'comma' && comp.kind !== 'slash' && comp.kind !== 'delim';
339
+ });
340
+ const type = calcType(inner);
341
+ return alternates ? type : null;
342
+ }
343
+ const isNone = (argument) => argument.length === 1 && isIdent(argument[0], ['none']);
344
+ /**
345
+ * The common type of a math function's comma-separated `calc()` sums (`clamp()`'s `none`
346
+ * bounds left out); `null` when an argument is not a sum or the types do not add up.
347
+ */
348
+ function argumentsType(inner) {
349
+ let total = 'unknown';
350
+ for (const argument of argumentsOf(inner)) {
351
+ if (isNone(argument)) continue;
352
+ const type = checkedSum(argument);
353
+ total = type === null ? null : sumType(total, type);
354
+ if (total === null) return null;
355
+ }
356
+ return total;
357
+ }
358
+ /**
359
+ * Structural checks Chrome makes before keeping a function: a `calc()` sum alternates operands
360
+ * and operators and type-checks, `min()` / `max()` / `clamp()` have their arguments, `rgb()` /
361
+ * `hsl()` have three channels (and an alpha) in one syntax, `linear()` stops have at most two
362
+ * positions, `steps()` counts and `cubic-bezier()` x coordinates are in range.
363
+ */
364
+ function functionValid(name, inner) {
365
+ if (name === 'calc') return checkedSum(inner) !== null;
366
+ if (name === 'min' || name === 'max') return !argumentsOf(inner).some(isNone) && argumentsType(inner) !== null;
367
+ if (name === 'clamp') { const args = argumentsOf(inner); return args.length === 3 && !isNone(args[1]) && argumentsType(inner) !== null; }
368
+ if (name === 'abs' || name === 'sign') return !inner.some(isComma) && checkedSum(inner) !== null;
369
+ if (['rgb', 'rgba', 'hsl', 'hsla'].includes(name)) {
370
+ const commasCount = inner.filter((c) => c.kind === 'comma').length;
371
+ // Relative color syntax: `from <color>`, three channels, then `/ alpha`.
372
+ if (inner.length && isIdent(inner[0], ['from'])) return commasCount === 0 && (inner.length === 5 || (inner.length === 7 && inner[5].kind === 'slash'));
373
+ // A hue: a number, an angle, `none` (modern syntax) or a math function of those.
374
+ const hue = (c) => c.kind === 'number' || (c.kind === 'dimension' && unitCategory(c.unit) === 2) || (commasCount === 0 && isIdent(c, ['none'])) || (isMath(c) && ['number', 'unit2', 'unknown'].includes(mathType(c)));
375
+ if (name.startsWith('hsl') && inner.length && !hue(inner[0])) return false;
376
+ if (commasCount) return (commasCount === 2 || commasCount === 3) && inner.length === 2 * commasCount + 1 && inner.every((c, index) => (c.kind === 'comma') === (index % 2 === 1));
377
+ const slash = inner.findIndex((c) => c.kind === 'slash');
378
+ return slash < 0 ? inner.length === 3 : slash === 3 && inner.length === 5;
379
+ }
380
+ if (name === 'linear') {
381
+ // Each stop: a number output and at most two percentage positions (literals once `read`
382
+ // resolved the math functions).
383
+ return inner.some((c) => c.kind === 'comma') && argumentsOf(inner).every((stop) => stop.length >= 1 && stop.length <= 3 && stop[0].kind === 'number' && stop.slice(1).every((c) => c.kind === 'percentage'));
384
+ }
385
+ if (name === 'steps') {
386
+ // The step count: an integer literal, or a math function Chrome checks later.
387
+ const count = (c, minimum) => (c.kind === 'number' ? c.integer && c.value >= minimum : c.kind === 'function' && MATH_FUNCTIONS.has(c.name));
388
+ if (inner.length === 1) return count(inner[0], 1);
389
+ if (inner.length === 3 && inner[1].kind === 'comma') {
390
+ const position = keyword(inner[2], ['start', 'end', 'jump-start', 'jump-end', 'jump-none', 'jump-both']);
391
+ return position !== null && count(inner[0], position === 'jump-none' ? 2 : 1);
392
+ }
393
+ return false;
394
+ }
395
+ if (name === 'cubic-bezier') {
396
+ // Four numbers (literals once `read` resolved the math functions); the x coordinates are in [0, 1].
397
+ const args = argumentsOf(inner);
398
+ return args.length === 4 && args.every((arg, index) => arg.length === 1 && arg[0].kind === 'number' && (index % 2 === 1 || (arg[0].value >= 0 && arg[0].value <= 1)));
399
+ }
400
+ return true;
401
+ }
402
+ /** `calc()` of simplified terms: numbers, then percentages, then units in ASCII order. */
403
+ function calcText(terms) {
404
+ if (terms.length === 1 && !Number.isFinite(terms[0][1])) {
405
+ // `calc(1px / 0)` is `calc(infinity * 1px)`.
406
+ const [unit, value] = terms[0];
407
+ const infinity = Number.isNaN(value) ? 'NaN' : value < 0 ? '-infinity' : 'infinity';
408
+ return `calc(${infinity}${unit ? ` * 1${unit}` : ''})`;
409
+ }
410
+ const rank = (unit) => (unit === '' ? 0 : unit === '%' ? 1 : 2);
411
+ terms.sort((a, b) => rank(a[0]) - rank(b[0]) || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
412
+ let out = 'calc(';
413
+ terms.forEach(([unit, value], index) => {
414
+ if (index === 0) out += cssNumber(value);
415
+ else if (value < 0) out += ` - ${cssNumber(-value)}`;
416
+ else out += ` + ${cssNumber(value)}`;
417
+ out += unit;
418
+ });
419
+ return `${out})`;
420
+ }
421
+ /**
422
+ * Units whose values Chrome resolves while parsing (absolute lengths, times, angles and
423
+ * frequencies after `sumTerms` canonicalized them): `abs()` / `sign()` / `min()` / `max()` /
424
+ * `clamp()` of them simplify; relative lengths and percentages stay as written.
425
+ */
426
+ const RESOLVED_UNITS = ['', 'px', 's', 'deg', 'hz'];
427
+ /** Math functions whose arguments are `calc()` sums. */
428
+ const SUM_FUNCTIONS = ['min', 'max', 'clamp', 'abs', 'sign'];
429
+ /** A sum's single term when Chrome resolves it while parsing, else null. */
430
+ const resolved = (terms) => (terms && terms.length === 1 && RESOLVED_UNITS.includes(terms[0][0]) ? terms[0] : null);
431
+ /**
432
+ * `min()` / `max()` / `clamp()` Chrome simplifies: one argument (`clamp()`'s `none` bounds
433
+ * left out) is its `calc()` sum, arguments that all resolve in one unit their value.
434
+ */
435
+ function minMax(name, inner) {
436
+ const args = argumentsOf(inner);
437
+ const present = args.filter((argument) => !isNone(argument));
438
+ if (present.length === 1) { const terms = sumTerms(present[0]); return terms && calcText(terms); }
439
+ let unit = null;
440
+ const values = [];
441
+ for (const argument of args) {
442
+ if (isNone(argument)) { values.push(null); continue; }
443
+ // functionValid type-checked the arguments: resolved ones share the unit.
444
+ const term = resolved(sumTerms(argument));
445
+ if (!term) return null;
446
+ unit = term[0];
447
+ values.push(term[1]);
448
+ }
449
+ const given = values.filter((value) => value !== null);
450
+ const value = name === 'min' ? Math.min(...given) : name === 'max' ? Math.max(...given) : Math.max(values[0] ?? -Infinity, Math.min(values[1], values[2] ?? Infinity));
451
+ return calcText([[unit, value]]);
452
+ }
453
+ /** `abs()` / `sign()` of a sum Chrome resolves: its `calc()` value (`sign()` a number). */
454
+ function absSign(name, inner) {
455
+ const term = resolved(sumTerms(inner));
456
+ if (!term) return null;
457
+ // `sign()` keeps a (signed) zero and NaN.
458
+ return name === 'abs' ? calcText([[term[0], Math.abs(term[1])]]) : calcText([['', term[1] === 0 || Number.isNaN(term[1]) ? term[1] : Math.sign(term[1])]]);
459
+ }
460
+ /**
461
+ * The arguments of a math function Chrome keeps: each a simplified sum without the `calc()`
462
+ * wrapper (`min(1px, calc(1px + 1em))` is `min(1px, 1em + 1px)`), or as written.
463
+ */
464
+ function sumArguments(inner) {
465
+ return argumentsOf(inner).map((argument) => {
466
+ const terms = isNone(argument) ? null : sumTerms(argument);
467
+ return terms ? calcText(terms).slice(5, -1) : join(argument);
468
+ }).join(', ');
469
+ }
470
+ /** Blink's alpha spelling: the shortest of two or three decimals naming the same 8-bit alpha. */
471
+ function alphaText(alpha) {
472
+ const a = Math.min(1, Math.max(0, alpha));
473
+ const byte = Math.round(a * 255);
474
+ const two = Math.round(a * 100) / 100;
475
+ return cssNumber(Math.round(two * 255) === byte ? two : Math.round(a * 1000) / 1000);
476
+ }
477
+ function rgbText(channels, alpha) {
478
+ // Channels computed from hsl() land a hair off a half: round at 1e-9 first.
479
+ const [r, g, b] = channels.map((c) => Math.min(255, Math.max(0, Math.round(Math.round(c * 1e9) / 1e9))));
480
+ return alpha >= 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${alphaText(alpha)})`;
481
+ }
482
+ /** `#rgb[a]` / `#rrggbb[aa]` as Chrome serializes them. */
483
+ function hexColor(hex) {
484
+ if (!/^[0-9a-fA-F]+$/.test(hex)) return null;
485
+ const digits = [...hex].map((c) => parseInt(c, 16));
486
+ let pairs;
487
+ if (digits.length === 3 || digits.length === 4) pairs = digits.map((d) => d * 17);
488
+ else if (digits.length === 6 || digits.length === 8) { pairs = []; for (let i = 0; i < digits.length; i += 2) pairs.push(digits[i] * 16 + digits[i + 1]); }
489
+ else return null;
490
+ return rgbText(pairs.slice(0, 3), pairs.length > 3 ? pairs[3] / 255 : 1);
491
+ }
492
+ /** `rgb()` / `hsl()` in the legacy (commas) or modern (spaces, `/ alpha`) syntax. */
493
+ function colorFunction(name, inner) {
494
+ // functionValid admitted three channels and an optional alpha, or the relative syntax (kept as written).
495
+ const values = inner.filter((c) => c.kind !== 'comma' && c.kind !== 'slash');
496
+ if (values.length > 4) return null;
497
+ const parts = values.slice(0, 3), alphaPart = values[3] ?? null;
498
+ // A channel: a literal, `none` (zero) or a math function that computes to one.
499
+ const computed = (c, unit) => {
500
+ if (isIdent(c, ['none'])) return 0;
501
+ const term = mathValue(c);
502
+ return term && term[0] === unit ? term[1] : null;
503
+ };
504
+ const num = (c) => (c.kind === 'number' ? c.value : computed(c, ''));
505
+ const pct = (c) => (c.kind === 'percentage' ? c.value : computed(c, '%'));
506
+ let alpha = 1;
507
+ if (alphaPart !== null) { const n = num(alphaPart); const p = pct(alphaPart); if (n !== null) alpha = n; else if (p !== null) alpha = p / 100; else return null; }
508
+ let channels;
509
+ if (name.startsWith('rgb')) {
510
+ channels = parts.map((c) => { const n = num(c); if (n !== null) return n; const p = pct(c); return p === null ? null : p * 2.55; });
511
+ if (channels.some((c) => c === null)) return null;
512
+ } else {
513
+ const h = parts[0];
514
+ let hue;
515
+ const turns = { deg: 1, grad: 0.9, rad: 180 / Math.PI, turn: 360 };
516
+ // functionValid admitted a number, an angle, `none` or a math function hue.
517
+ if (h.kind === 'number') hue = h.value; else if (h.kind === 'dimension') hue = h.value * turns[h.unit]; else hue = computed(h, '') ?? computed(h, 'deg');
518
+ if (hue === null) return null;
519
+ const sat = pct(parts[1]), light = pct(parts[2]);
520
+ if (sat === null || light === null) return null;
521
+ const S = sat / 100, L = light / 100, H = (((hue % 360) + 360) % 360) / 360;
522
+ const q = L < 0.5 ? L * (1 + S) : L + S - L * S, p = 2 * L - q;
523
+ const channel = (t) => { t = ((t % 1) + 1) % 1; return 255 * (t < 1 / 6 ? p + (q - p) * 6 * t : t < 0.5 ? q : t < 2 / 3 ? p + (q - p) * (2 / 3 - t) * 6 : p); };
524
+ channels = [channel(H + 1 / 3), channel(H), channel(H - 1 / 3)];
525
+ }
526
+ return rgbText(channels, alpha);
527
+ }
528
+ /** `lab()` / `lch()` / `oklab()` / `oklch()`: a percentage lightness spelled as a number. */
529
+ function labFunction(name, inner) {
530
+ return `${name}(${inner.map((c, index) => (index === 0 && c.kind === 'percentage' ? cssNumber(c.value / (name.startsWith('ok') ? 100 : 1)) : c.text)).join(' ')})`;
531
+ }
532
+ /** `steps(n, end | jump-end)` is `steps(n)`; the position keyword lowercase. */
533
+ function stepsText(inner) {
534
+ if (inner.length !== 3 || inner[1].kind !== 'comma' || ident(inner[2]) === null) return null;
535
+ const position = ident(inner[2]);
536
+ return position === 'end' || position === 'jump-end' ? `steps(${inner[0].text})` : `steps(${inner[0].text}, ${position})`;
537
+ }
538
+ /** `linear()` with every stop's input position filled in. */
539
+ function linearText(inner) {
540
+ const stops = [];
541
+ for (const item of split(inner, isComma)) {
542
+ // functionValid admitted a number and at most two percentages per stop.
543
+ const positions = item.slice(1).map((c) => c.value);
544
+ if (!positions.length) stops.push([item[0].text, [null]]);
545
+ // A stop with two input positions is two stops.
546
+ else for (const position of positions) stops.push([item[0].text, [position]]);
547
+ }
548
+ const flat = stops.flatMap(([, positions]) => positions);
549
+ if (flat[0] === null) flat[0] = 0;
550
+ if (flat[flat.length - 1] === null) flat[flat.length - 1] = 100;
551
+ // A position below the largest before it is that largest.
552
+ let largest = -Infinity;
553
+ for (let at = 0; at < flat.length; at++) if (flat[at] !== null) { largest = Math.max(largest, flat[at]); flat[at] = largest; }
554
+ for (let at = 0; at < flat.length; at++) {
555
+ if (flat[at] !== null) continue;
556
+ let end = at;
557
+ while (flat[end] === null) end++;
558
+ const low = flat[at - 1], high = flat[end], steps = end - at + 1;
559
+ for (let index = at; index < end; index++) flat[index] = low + ((high - low) * (index - at + 1)) / steps;
560
+ at = end;
561
+ }
562
+ let index = 0;
563
+ return `linear(${stops.map(([output, positions]) => `${output} ${positions.map(() => `${cssNumber(flat[index++])}%`).join(' ')}`).join(', ')})`;
564
+ }
565
+
566
+ /** Functions whose identifier arguments are names (kept as written). */
567
+ const NAMING_ARGUMENTS = new Set(['stylistic', 'styleset', 'character-variant', 'swash', 'ornaments', 'annotation', 'local', 'format', 'attr', 'counter', 'counters', 'symbols', 'src', 'var', 'env']);
568
+ /** A math function Chrome simplified to one term (its text spells `calc(...)`): `[unit, value]`, else null. */
569
+ function mathValue(comp) {
570
+ if (!isMath(comp) || !comp.text.startsWith('calc(')) return null;
571
+ const terms = sumTerms(components(comp.text.slice(5, -1)));
572
+ return terms && terms.length === 1 ? terms[0] : null;
573
+ }
574
+ /** Tokens -> components (a function / block is one component); `null` on bad input. */
575
+ function read(tokens, state, close) {
576
+ const out = [];
577
+ while (state.at < tokens.length) {
578
+ const token = tokens[state.at++];
579
+ switch (token.type) {
580
+ case 'ws': break;
581
+ case 'ident': out.push({ kind: 'ident', value: token.value, text: token.value }); break;
582
+ case 'number': out.push({ kind: 'number', value: f32(token.value), integer: token.integer, text: cssNumber(token.value) }); break;
583
+ case 'percentage': {
584
+ const value = f32(Number(token.raw));
585
+ out.push({ kind: 'percentage', value, text: `${cssNumber(Number(token.raw))}%` });
586
+ break;
587
+ }
588
+ case 'dimension': {
589
+ // A length past f32's range (`1e39px`) is the largest float, as Chrome clamps it
590
+ // (`3.40282e+38px`), never infinite (#262; the Rust port's tokenizer likewise).
591
+ const number = Math.max(-F32_MAX, Math.min(F32_MAX, token.value));
592
+ const unit = token.unit.toLowerCase();
593
+ out.push({ kind: 'dimension', value: f32(number), unit, text: `${cssNumber(number)}${unit}` });
594
+ break;
595
+ }
596
+ case 'hash': out.push({ kind: 'hash', value: token.value, text: `#${token.value}` }); break;
597
+ case 'string': out.push({ kind: 'string', value: token.value, text: quoted(token.value) }); break;
598
+ case 'url': out.push({ kind: 'url', value: token.value, text: `url(${quoted(token.value)})` }); break;
599
+ case 'comma': out.push({ kind: 'comma', text: ',' }); break;
600
+ case 'delim':
601
+ if (token.value === '!') return null;
602
+ out.push(token.value === '/' ? { kind: 'slash', text: '/' } : { kind: 'delim', value: token.value, text: token.value });
603
+ break;
604
+ case 'function': {
605
+ const name = token.name.toLowerCase();
606
+ const inner = read(tokens, state, ')');
607
+ if (inner === null) return null;
608
+ if (name === 'url') {
609
+ if (inner.length !== 1 || inner[0].kind !== 'string') return null;
610
+ out.push({ kind: 'url', value: inner[0].value, text: `url(${quoted(inner[0].value)})` });
611
+ } else {
612
+ // Keyword arguments are case-insensitive: Chrome writes them lowercase.
613
+ if (!NAMING_ARGUMENTS.has(name)) for (const comp of inner) if (comp.kind === 'ident' && !comp.value.startsWith('--')) comp.text = comp.value.toLowerCase();
614
+ // A math argument Chrome resolves to a number (or, in linear(), a percentage) is that literal.
615
+ if (name === 'cubic-bezier' || name === 'linear') inner.forEach((comp, index) => {
616
+ const term = mathValue(comp);
617
+ if (term && term[0] === '') inner[index] = { kind: 'number', value: f32(term[1]), integer: false, text: cssNumber(term[1]) };
618
+ else if (term && term[0] === '%' && name === 'linear') inner[index] = { kind: 'percentage', value: f32(term[1]), text: `${cssNumber(term[1])}%` };
619
+ });
620
+ if (!functionValid(name, inner)) return null;
621
+ const special = name === 'calc' ? calcSum(inner) : name === 'min' || name === 'max' || name === 'clamp' ? minMax(name, inner) : name === 'abs' || name === 'sign' ? absSign(name, inner) : name === 'rgb' || name === 'rgba' || name === 'hsl' || name === 'hsla' ? colorFunction(name, inner) : name === 'lab' || name === 'lch' || name === 'oklab' || name === 'oklch' ? labFunction(name, inner) : name === 'steps' ? stepsText(inner) : name === 'linear' ? linearText(inner) : null;
622
+ const text = special ?? `${name}(${SUM_FUNCTIONS.includes(name) ? sumArguments(inner) : join(inner)})`;
623
+ out.push({ kind: 'function', name, text });
624
+ }
625
+ break;
626
+ }
627
+ case 'open': {
628
+ if (token.value === '{') return null;
629
+ const closer = token.value === '(' ? ')' : ']';
630
+ const inner = read(tokens, state, closer);
631
+ if (inner === null) return null;
632
+ out.push({ kind: 'function', name: token.value, text: `${token.value}${join(inner)}${closer}` });
633
+ break;
634
+ }
635
+ case 'close':
636
+ if (token.value === close) return out;
637
+ return null;
638
+ default: return null;
639
+ }
640
+ }
641
+ return close ? out : out;
642
+ }
643
+
644
+ /** Tokenize a declaration value into components; `null` for unbalanced or bad input. */
645
+ function components(value) {
646
+ const tokens = tokenize(value);
647
+ if (tokens === null) return null;
648
+ return read(tokens, { at: 0 }, null);
649
+ }
650
+
651
+ // Component predicates: the canonical text when the component is of the type, else null.
652
+ const ident = (c) => (c.kind === 'ident' ? c.value.toLowerCase() : null);
653
+ const isIdent = (c, names) => { const name = ident(c); return name !== null && names.includes(name); };
654
+ const keyword = (c, names) => { const name = ident(c); return name !== null && names.includes(name) ? name : null; };
655
+ const isComma = (c) => c.kind === 'comma';
656
+ const isSlash = (c) => c.kind === 'slash';
657
+ const innerOf = (c) => components(c.text.slice(c.text.search(/[([]/) + 1, -1));
658
+ function visit(comps, callback) {
659
+ for (const comp of comps) {
660
+ callback(comp);
661
+ if (comp.kind === 'function') { const inner = innerOf(comp); if (inner) visit(inner, callback); }
662
+ }
663
+ }
664
+ function mathOf(c, accepts, percent, number) {
665
+ if (c.kind !== 'function' || !MATH_FUNCTIONS.has(c.name)) return false;
666
+ const inner = innerOf(c);
667
+ if (!inner) return false;
668
+ let found = false, bad = false;
669
+ visit(inner, (comp) => {
670
+ if (comp.kind === 'dimension') { if (accepts(comp.unit)) found = true; else bad = true; }
671
+ else if (comp.kind === 'percentage') { if (percent) found = true; else bad = true; }
672
+ });
673
+ return !bad && (found || number);
674
+ }
675
+ const isLengthUnit = (unit) => LENGTH_UNITS.has(unit);
676
+ function length(c, negative) {
677
+ if (c.kind === 'dimension' && LENGTH_UNITS.has(c.unit)) return negative || c.value >= 0 ? c.text : null;
678
+ if (c.kind === 'number' && c.value === 0) return '0px';
679
+ return mathOf(c, isLengthUnit, false, false) ? c.text : null;
680
+ }
681
+ const percentage = (c, negative) => (c.kind === 'percentage' && (negative || c.value >= 0) ? c.text : null);
682
+ function lengthPercentage(c, negative) {
683
+ if (mathOf(c, isLengthUnit, true, false)) return c.text;
684
+ return length(c, negative) ?? percentage(c, negative);
685
+ }
686
+ function number(c, negative) {
687
+ if (c.kind === 'number') return negative || c.value >= 0 ? c.text : null;
688
+ return mathOf(c, () => false, false, true) ? c.text : null;
689
+ }
690
+ const integer = (c, minimum) => (c.kind === 'number' ? (c.integer && c.value >= minimum ? c.text : null) : mathOf(c, () => false, false, true) ? c.text : null);
691
+ function time(c, negative) {
692
+ if (c.kind === 'dimension' && (c.unit === 's' || c.unit === 'ms')) return negative || c.value >= 0 ? c.text : null;
693
+ return mathOf(c, (unit) => unit === 's' || unit === 'ms', false, false) ? c.text : null;
694
+ }
695
+ const ANGLES = ['deg', 'grad', 'rad', 'turn'];
696
+ function angle(c) {
697
+ if (c.kind === 'dimension' && ANGLES.includes(c.unit)) return c.text;
698
+ if (c.kind === 'number' && c.value === 0) return '0deg';
699
+ return mathOf(c, (unit) => ANGLES.includes(unit), false, false) ? c.text : null;
700
+ }
701
+ /** `<angle>` without the unitless zero (`offset-rotate`). */
702
+ const angleStrict = (c) => (c.kind === 'number' ? null : angle(c));
703
+ function color(c) {
704
+ if (c.kind === 'hash') return hexColor(c.value);
705
+ if (c.kind === 'ident') {
706
+ const name = c.value.toLowerCase();
707
+ return SYSTEM_COLORS.has(name) || canonicalizeColor(name, true) !== null ? name : null;
708
+ }
709
+ if (c.kind === 'function' && COLOR_FUNCTIONS.has(c.name)) return c.text;
710
+ return null;
711
+ }
712
+ const url = (c) => (c.kind === 'url' ? c.text : null);
713
+ const image = (c) => url(c) ?? (c.kind === 'function' && IMAGE_FUNCTIONS.has(c.name) ? c.text : null);
714
+ const string = (c) => (c.kind === 'string' ? c.text : null);
715
+ function customIdent(c, excluded) {
716
+ if (c.kind !== 'ident') return null;
717
+ const lower = c.value.toLowerCase();
718
+ return !CSS_WIDE.has(lower) && lower !== 'default' && !excluded.includes(lower) ? c.value : null;
719
+ }
720
+ const dashedIdent = (c) => (c.kind === 'ident' && c.value.startsWith('--') && c.value.length > 2 ? c.value : null);
721
+ const functionNamed = (c, names) => (c.kind === 'function' && names.includes(c.name) ? c.text : null);
722
+
723
+ /** Whether the raw value holds a var() / env() / attr() / if() / inherit() reference. */
724
+ function hasSubstitution(value) {
725
+ const tokens = tokenize(value);
726
+ if (!tokens) return false;
727
+ return tokens.some((t) => t.type === 'function' && ['var', 'env', 'attr', 'if', 'inherit'].includes(t.name.toLowerCase()));
728
+ }
729
+ const substitutionText = (value) => value.trim().replace(/\s+/g, ' ');
730
+
731
+ // ---------------------------------------------------------------- grammars ----
732
+ const INITIAL = 'initial';
733
+ const marginV = (c) => lengthPercentage(c, true) ?? keyword(c, ['auto']);
734
+ const paddingV = (c) => lengthPercentage(c, false);
735
+ const scrollMarginV = (c) => length(c, true);
736
+ const scrollPaddingV = (c) => lengthPercentage(c, false) ?? keyword(c, ['auto']);
737
+ const lineWidth = (c) => length(c, false) ?? keyword(c, ['thin', 'medium', 'thick']);
738
+ const LINE_STYLES = ['none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'];
739
+ const lineStyle = (c) => keyword(c, LINE_STYLES);
740
+ const cornerShape = (c) => keyword(c, ['round', 'scoop', 'bevel', 'notch', 'square', 'squircle']) ?? functionNamed(c, ['superellipse']);
741
+ const radius = (c) => lengthPercentage(c, false);
742
+ const gapV = (c) => lengthPercentage(c, false) ?? keyword(c, ['normal']);
743
+ const overflowV = (c) => keyword(c, ['visible', 'hidden', 'clip', 'scroll', 'auto', 'overlay']);
744
+ const overscrollV = (c) => keyword(c, ['auto', 'contain', 'none', 'chain']);
745
+ const spacingV = (c) => length(c, false);
746
+ const delayV = (c) => keyword(c, ['normal']) ?? time(c, false);
747
+ const ruleInsetV = (c) => lengthPercentage(c, true) ?? keyword(c, ['overlap-join']);
748
+ const ruleBreakV = (c) => keyword(c, ['none', 'normal', 'intersection']);
749
+ const ruleVisibilityV = (c) => keyword(c, ['all', 'normal', 'around', 'between']);
750
+ const markerV = (c) => keyword(c, ['none']) ?? url(c);
751
+
752
+ const single = (comps, ty) => (comps.length === 1 ? ty(comps[0]) : null);
753
+ function list(comps, ty, max) {
754
+ if (comps.length === 0 || comps.length > max) return null;
755
+ const out = [];
756
+ for (const comp of comps) { const value = ty(comp); if (value === null) return null; out.push(value); }
757
+ return out;
758
+ }
759
+ function box4(comps, ty) {
760
+ const v = list(comps, ty, 4);
761
+ if (!v) return null;
762
+ return v.length === 1 ? [v[0], v[0], v[0], v[0]] : v.length === 2 ? [v[0], v[1], v[0], v[1]] : v.length === 3 ? [v[0], v[1], v[2], v[1]] : v;
763
+ }
764
+ function pair(comps, ty) {
765
+ const v = list(comps, ty, 2);
766
+ return v ? [v[0], v[1] ?? v[0]] : null;
767
+ }
768
+ const named = (names, values) => (values ? names.map((name, index) => [name, values[index]]) : null);
769
+ const same = (names, value) => (value === null ? null : names.map((name) => [name, value]));
770
+ const one = (ty) => (comps) => { if (!comps.length) return null; const value = ty(comps[0]); return value === null ? null : [1, value]; };
771
+ /** `a || b || ...`: each slot at most once, in any order; earlier slots win a component. */
772
+ function anyOrder(comps, slots) {
773
+ const out = slots.map(() => null);
774
+ let at = 0;
775
+ while (at < comps.length) {
776
+ let matched = false;
777
+ for (let index = 0; index < slots.length; index++) {
778
+ if (out[index] !== null) continue;
779
+ const result = slots[index](comps.slice(at));
780
+ if (result) { out[index] = result[1]; at += result[0]; matched = true; break; }
781
+ }
782
+ if (!matched) return null;
783
+ }
784
+ return out;
785
+ }
786
+ function split(comps, test) {
787
+ const items = [[]];
788
+ for (const comp of comps) { if (test(comp)) items.push([]); else items[items.length - 1].push(comp); }
789
+ return items;
790
+ }
791
+ const commas = (comps) => { const items = split(comps, isComma); return items.every((item) => item.length) ? items : null; };
792
+ const slashes = (comps) => split(comps, isSlash);
793
+ const namesOrInitial = (names, parsed) => names.map((name, index) => [name, parsed[index] ?? INITIAL]);
794
+ const BORDER_IMAGE_INITIAL = ['none', '100%', '1', '0', 'stretch'];
795
+
796
+ const outlineStyle = (c) => keyword(c, ['auto']) ?? lineStyle(c);
797
+ function line(comps, defaults) {
798
+ const parsed = anyOrder(comps, [one(lineWidth), one(lineStyle), one(color)]);
799
+ return parsed && parsed.map((value, index) => value ?? defaults[index]);
800
+ }
801
+ function side(comps, name, defaults) {
802
+ const parts = line(comps, defaults);
803
+ return parts && [[`${name}-width`, parts[0]], [`${name}-style`, parts[1]], [`${name}-color`, parts[2]]];
804
+ }
805
+
806
+ function borderRadius(comps, legacy) {
807
+ const parts = slashes(comps);
808
+ let horizontal, vertical;
809
+ if (parts.length === 1 && legacy && parts[0].length === 2) {
810
+ const a = radius(parts[0][0]), b = radius(parts[0][1]);
811
+ if (a === null || b === null) return null;
812
+ horizontal = [a, a, a, a]; vertical = [b, b, b, b];
813
+ } else if (parts.length === 1) {
814
+ horizontal = box4(parts[0], radius); vertical = horizontal;
815
+ } else if (parts.length === 2) {
816
+ horizontal = box4(parts[0], radius); vertical = box4(parts[1], radius);
817
+ } else return null;
818
+ if (!horizontal || !vertical) return null;
819
+ return ['border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius']
820
+ .map((name, index) => [name, horizontal[index] === vertical[index] ? horizontal[index] : `${horizontal[index]} ${vertical[index]}`]);
821
+ }
822
+
823
+ function ruleInset(comps, kind) {
824
+ const parts = slashes(comps);
825
+ let cap, junction;
826
+ if (parts.length === 1) { cap = pair(parts[0], ruleInsetV); junction = cap; }
827
+ else if (parts.length === 2) { cap = pair(parts[0], ruleInsetV); junction = pair(parts[1], ruleInsetV); }
828
+ else return null;
829
+ if (!cap || !junction) return null;
830
+ return [[`${kind}-rule-inset-cap-start`, cap[0]], [`${kind}-rule-inset-cap-end`, cap[1]], [`${kind}-rule-inset-junction-start`, junction[0]], [`${kind}-rule-inset-junction-end`, junction[1]]];
831
+ }
832
+
833
+ function containIntrinsicSize(comps) {
834
+ const axis = (comps) => {
835
+ if (comps.length >= 2 && isIdent(comps[0], ['auto'])) {
836
+ const value = length(comps[1], false) ?? keyword(comps[1], ['none']);
837
+ return value === null ? null : [2, `auto ${value}`];
838
+ }
839
+ if (!comps.length) return null;
840
+ const value = length(comps[0], false) ?? keyword(comps[0], ['none']);
841
+ return value === null ? null : [1, value];
842
+ };
843
+ const first = axis(comps);
844
+ if (!first) return null;
845
+ let height = first[1];
846
+ const rest = comps.slice(first[0]);
847
+ // A trailing lone `auto` after `auto <length>` repeats the width (Chrome).
848
+ if (rest.length && !(rest.length === 1 && isIdent(rest[0], ['auto']) && first[1].startsWith('auto '))) {
849
+ const second = axis(rest);
850
+ if (!second || first[0] + second[0] !== comps.length) return null;
851
+ height = second[1];
852
+ }
853
+ return [['contain-intrinsic-width', first[1]], ['contain-intrinsic-height', height]];
854
+ }
855
+
856
+ function baseline(comps) {
857
+ if (comps.length >= 2 && isIdent(comps[0], ['first']) && isIdent(comps[1], ['baseline'])) return [2, 'baseline'];
858
+ if (comps.length >= 2 && isIdent(comps[0], ['last']) && isIdent(comps[1], ['baseline'])) return [2, 'last baseline'];
859
+ if (comps.length >= 1 && isIdent(comps[0], ['baseline'])) return [1, 'baseline'];
860
+ return null;
861
+ }
862
+ function overflowPosition(comps, positions) {
863
+ if (comps.length >= 2 && isIdent(comps[0], ['safe', 'unsafe'])) {
864
+ const position = keyword(comps[1], positions);
865
+ return position === null ? null : [2, `${ident(comps[0])} ${position}`];
866
+ }
867
+ if (!comps.length) return null;
868
+ const value = keyword(comps[0], positions);
869
+ return value === null ? null : [1, value];
870
+ }
871
+ const CONTENT_POSITIONS = ['center', 'start', 'end', 'flex-start', 'flex-end'];
872
+ const SELF_POSITIONS = ['center', 'start', 'end', 'self-start', 'self-end', 'flex-start', 'flex-end'];
873
+ const DISTRIBUTIONS = ['space-between', 'space-around', 'space-evenly', 'stretch'];
874
+ function firstKeyword(comps, names) { if (!comps.length) return null; const value = keyword(comps[0], names); return value === null ? null : [1, value]; }
875
+
876
+ function placeContent(comps) {
877
+ const align = (comps) => baseline(comps) ?? firstKeyword(comps, ['normal', ...DISTRIBUTIONS]) ?? overflowPosition(comps, CONTENT_POSITIONS);
878
+ const justify = (comps) => firstKeyword(comps, ['normal', 'left', 'right', ...DISTRIBUTIONS]) ?? overflowPosition(comps, ['center', 'start', 'end', 'flex-start', 'flex-end', 'left', 'right']);
879
+ const a = align(comps);
880
+ if (!a) return null;
881
+ // `[first] baseline` takes a justify value (`start` when there is none); Chrome 156 rejects
882
+ // `last baseline` here.
883
+ if (a[1] === 'last baseline') return null;
884
+ let j;
885
+ if (a[0] === comps.length) j = a[1] === 'baseline' ? 'start' : a[1];
886
+ else {
887
+ const rest = justify(comps.slice(a[0]));
888
+ if (!rest || a[0] + rest[0] !== comps.length) return null;
889
+ j = rest[1];
890
+ }
891
+ return [['align-content', a[1]], ['justify-content', j]];
892
+ }
893
+ const selfAlignment = (comps, extra) => baseline(comps) ?? firstKeyword(comps, extra) ?? overflowPosition(comps, SELF_POSITIONS);
894
+ /** A `justify-items` value from the front of `comps`: `[used, text]`. */
895
+ function justifyItems(comps) {
896
+ let legacy = null;
897
+ if (comps.length === 1 && isIdent(comps[0], ['legacy'])) legacy = [1, 'legacy'];
898
+ else if (comps.length === 2 && isIdent(comps[0], ['legacy'])) { const v = keyword(comps[1], ['left', 'right', 'center']); legacy = v === null ? null : [2, `legacy ${v}`]; }
899
+ else if (comps.length === 2 && isIdent(comps[1], ['legacy'])) { const v = keyword(comps[0], ['left', 'right', 'center']); legacy = v === null ? null : [2, `legacy ${v}`]; }
900
+ return legacy ?? selfAlignment(comps, ['normal', 'stretch', 'left', 'right', 'anchor-center']) ?? overflowPosition(comps, ['left', 'right']);
901
+ }
902
+ function placeItems(comps) {
903
+ const a = selfAlignment(comps, ['normal', 'stretch']);
904
+ if (!a) return null;
905
+ let justify = a[1];
906
+ if (a[0] !== comps.length) {
907
+ const rest = comps.slice(a[0]);
908
+ const result = justifyItems(rest);
909
+ if (!result || a[0] + result[0] !== comps.length) return null;
910
+ justify = result[1];
911
+ }
912
+ return [['align-items', a[1]], ['justify-items', justify]];
913
+ }
914
+ function placeSelf(comps) {
915
+ const a = selfAlignment(comps, ['auto', 'normal', 'stretch', 'anchor-center']);
916
+ if (!a) return null;
917
+ let justify = a[1];
918
+ if (a[0] !== comps.length) {
919
+ const rest = comps.slice(a[0]);
920
+ const result = selfAlignment(rest, ['auto', 'normal', 'stretch', 'anchor-center', 'left', 'right']) ?? overflowPosition(rest, ['left', 'right']);
921
+ if (!result || a[0] + result[0] !== comps.length) return null;
922
+ justify = result[1];
923
+ }
924
+ return [['align-self', a[1]], ['justify-self', justify]];
925
+ }
926
+
927
+ /** One `<bg-position>`: its x and y specified values. */
928
+ function position(comps) {
929
+ const lp = (c) => lengthPercentage(c, true);
930
+ const horizontal = (c) => keyword(c, ['left', 'right']);
931
+ const vertical = (c) => keyword(c, ['top', 'bottom']);
932
+ const center = (c) => keyword(c, ['center']);
933
+ if (comps.length === 1) {
934
+ const [a] = comps;
935
+ const v = vertical(a);
936
+ if (v !== null) return ['center', v];
937
+ const x = horizontal(a) ?? center(a) ?? lp(a);
938
+ return x === null ? null : [x, 'center'];
939
+ }
940
+ if (comps.length === 2) {
941
+ const [a, b] = comps;
942
+ const ax = horizontal(a) ?? center(a) ?? lp(a), by = vertical(b) ?? center(b) ?? lp(b);
943
+ if (ax !== null && by !== null) return [ax, by];
944
+ const ay = vertical(a) ?? center(a), bx = horizontal(b) ?? center(b);
945
+ return ay !== null && bx !== null ? [bx, ay] : null;
946
+ }
947
+ if (comps.length === 3 || comps.length === 4) {
948
+ // Keyword + offset pairs; a lone keyword takes no offset; `center` takes the axis the
949
+ // other keyword leaves.
950
+ let at = 0, x = null, y = null, middle = null;
951
+ while (at < comps.length) {
952
+ const kw = keyword(comps[at], ['left', 'right', 'top', 'bottom', 'center']);
953
+ if (kw === null) return null;
954
+ const offset = comps[at + 1] ? lp(comps[at + 1]) : null;
955
+ const used = offset !== null && kw !== 'center' ? 2 : 1;
956
+ const text = used === 2 ? `${kw} ${offset}` : kw;
957
+ if (kw === 'left' || kw === 'right') { if (x !== null) return null; x = text; }
958
+ else if (kw === 'top' || kw === 'bottom') { if (y !== null) return null; y = text; }
959
+ else { if (middle !== null) return null; middle = text; }
960
+ at += used;
961
+ }
962
+ if (x !== null && y !== null && middle === null) return [x, y];
963
+ if (x !== null && y === null && middle !== null) return [x, middle];
964
+ if (x === null && y !== null && middle !== null) return [middle, y];
965
+ return null;
966
+ }
967
+ return null;
968
+ }
969
+ /** A comma list of positions; `three`: three-value positions too (background only). */
970
+ function positions(comps, three) {
971
+ const layers = commas(comps);
972
+ if (!layers) return null;
973
+ const xs = [], ys = [];
974
+ for (const layer of layers) { if (!three && layer.length === 3) return null; const p = position(layer); if (!p) return null; xs.push(p[0]); ys.push(p[1]); }
975
+ return [xs.join(', '), ys.join(', ')];
976
+ }
977
+
978
+ const RANGE_NAMES = ['cover', 'contain', 'entry', 'exit', 'entry-crossing', 'exit-crossing', 'scroll'];
979
+ function rangeValue(comps, initial, end) {
980
+ if (!comps.length) return null;
981
+ const first = comps[0];
982
+ const kw = keyword(first, [initial]);
983
+ if (kw !== null) return [1, kw, null];
984
+ const name = keyword(first, RANGE_NAMES);
985
+ if (name !== null) {
986
+ const offset = comps[1] ? lengthPercentage(comps[1], true) : null;
987
+ if (offset !== null) return [2, offset === (end ? '100%' : '0%') ? name : `${name} ${offset}`, name];
988
+ return [1, name, name];
989
+ }
990
+ const value = lengthPercentage(first, true);
991
+ return value === null ? null : [1, value, null];
992
+ }
993
+ function rangePair(comps, start, end, initial) {
994
+ const first = rangeValue(comps, initial === 'auto' ? 'normal' : initial, false) ?? rangeValue(comps, initial, false);
995
+ if (!first) return null;
996
+ let endValue;
997
+ if (first[0] === comps.length) endValue = first[2] ?? initial;
998
+ else {
999
+ const second = rangeValue(comps.slice(first[0]), initial, true);
1000
+ if (!second || first[0] + second[0] !== comps.length) return null;
1001
+ endValue = second[1];
1002
+ }
1003
+ return [[start, first[1]], [end, endValue]];
1004
+ }
1005
+
1006
+ function textDecoration(comps) {
1007
+ const order = ['underline', 'overline', 'line-through', 'blink', 'spelling-error', 'grammar-error'];
1008
+ const lines = (comps) => {
1009
+ if (!comps.length) return null;
1010
+ if (isIdent(comps[0], ['none'])) return [1, 'none'];
1011
+ const seen = [];
1012
+ for (const comp of comps) { const name = keyword(comp, order); if (name === null || seen.includes(name)) break; seen.push(name); }
1013
+ if (!seen.length) return null;
1014
+ return [seen.length, order.filter((name) => seen.includes(name)).join(' ')];
1015
+ };
1016
+ const thickness = (c) => keyword(c, ['auto', 'from-font']) ?? lengthPercentage(c, true);
1017
+ const style = (c) => keyword(c, ['solid', 'double', 'dotted', 'dashed', 'wavy']);
1018
+ const parsed = anyOrder(comps, [lines, one(thickness), one(style), one(color)]);
1019
+ return parsed && namesOrInitial(['text-decoration-line', 'text-decoration-thickness', 'text-decoration-style', 'text-decoration-color'], parsed);
1020
+ }
1021
+
1022
+ function textEmphasis(comps) {
1023
+ const style = (comps) => {
1024
+ if (!comps.length) return null;
1025
+ const first = comps[0];
1026
+ const text = string(first);
1027
+ if (text !== null) return [1, text];
1028
+ if (isIdent(first, ['none'])) return [1, 'none'];
1029
+ let fill = null, shape = null, used = 0;
1030
+ for (const comp of comps.slice(0, 2)) {
1031
+ if (fill === null) { const v = keyword(comp, ['filled', 'open']); if (v !== null) { fill = v; used++; continue; } }
1032
+ if (shape === null) { const v = keyword(comp, ['dot', 'circle', 'double-circle', 'triangle', 'sesame']); if (v !== null) { shape = v; used++; continue; } }
1033
+ break;
1034
+ }
1035
+ if (!used) return null;
1036
+ return [used, [fill, shape].filter((v) => v !== null).join(' ')];
1037
+ };
1038
+ const parsed = anyOrder(comps, [style, one(color)]);
1039
+ return parsed && namesOrInitial(['text-emphasis-style', 'text-emphasis-color'], parsed);
1040
+ }
1041
+
1042
+ function listStyle(comps) {
1043
+ const pos = (c) => keyword(c, ['inside', 'outside']);
1044
+ const kind = (c) => string(c) ?? (() => { const v = customIdent(c, ['none']); return v === null ? null : v.toLowerCase(); })();
1045
+ let nones = 0;
1046
+ const rest = comps.filter((c) => { if (isIdent(c, ['none'])) { nones++; return false; } return true; });
1047
+ const parsed = anyOrder(rest, [one(pos), one(image), one(kind)]);
1048
+ if (!parsed) return null;
1049
+ if (nones === 1 && parsed[2] === null) parsed[2] = 'none';
1050
+ else if (nones === 1 && parsed[1] === null) parsed[1] = 'none';
1051
+ else if (nones === 2 && parsed[1] === null && parsed[2] === null) { parsed[1] = 'none'; parsed[2] = 'none'; }
1052
+ else if (nones !== 0) return null;
1053
+ return namesOrInitial(['list-style-position', 'list-style-image', 'list-style-type'], parsed);
1054
+ }
1055
+
1056
+ function columns(comps) {
1057
+ const parts = slashes(comps);
1058
+ let main, height = null;
1059
+ if (parts.length === 1) main = parts[0];
1060
+ else if (parts.length === 2 && parts[1].length === 1) {
1061
+ main = parts[0];
1062
+ height = length(parts[1][0], false) ?? keyword(parts[1][0], ['auto']);
1063
+ if (height === null) return null;
1064
+ } else return null;
1065
+ if (!main.length || main.length > 2) return null;
1066
+ let width = null, count = null;
1067
+ for (const comp of main) {
1068
+ if (isIdent(comp, ['auto'])) continue;
1069
+ const w = width === null ? length(comp, false) : null;
1070
+ if (w !== null) { width = w; continue; }
1071
+ const n = count === null ? integer(comp, 1) : null;
1072
+ if (n !== null) { count = n; continue; }
1073
+ return null;
1074
+ }
1075
+ return [['column-width', width ?? 'auto'], ['column-count', count ?? 'auto'], ['column-height', height ?? 'auto'], ['column-wrap', 'auto']];
1076
+ }
1077
+
1078
+ function container(comps) {
1079
+ const parts = slashes(comps);
1080
+ let names, kind = null;
1081
+ if (parts.length === 1) names = parts[0];
1082
+ else if (parts.length === 2 && parts[1].length) { names = parts[0]; kind = parts[1]; }
1083
+ else return null;
1084
+ let name;
1085
+ if (names.length === 1) {
1086
+ if (isIdent(names[0], ['none'])) name = 'none';
1087
+ else { name = customIdent(names[0], ['none', 'and', 'or', 'not']); if (name === null) return null; }
1088
+ } else {
1089
+ const list = names.map((c) => customIdent(c, ['none', 'and', 'or', 'not']));
1090
+ if (list.some((v) => v === null)) return null;
1091
+ name = list.join(' ');
1092
+ }
1093
+ if (!name) return null;
1094
+ let type;
1095
+ if (kind === null) type = 'normal';
1096
+ else if (kind.length === 1) { type = keyword(kind[0], ['normal', 'size', 'inline-size', 'scroll-state', 'anchored']); if (type === null) return null; }
1097
+ else {
1098
+ const list = kind.map((c) => keyword(c, ['size', 'inline-size', 'scroll-state', 'anchored']));
1099
+ // Each keyword once, and at most one of size / inline-size.
1100
+ if (list.some((v, index) => v === null || list.indexOf(v) !== index) || list.filter((v) => v.endsWith('size')).length > 1) return null;
1101
+ type = list.join(' ');
1102
+ }
1103
+ return [['container-name', name], ['container-type', type]];
1104
+ }
1105
+
1106
+ function whiteSpace(comps) {
1107
+ if (comps.length === 1) {
1108
+ const mapped = { normal: ['collapse', 'wrap'], pre: ['preserve', 'nowrap'], nowrap: ['collapse', 'nowrap'], 'pre-wrap': ['preserve', 'wrap'], 'pre-line': ['preserve-breaks', 'wrap'], 'break-spaces': ['break-spaces', 'wrap'] }[ident(comps[0])];
1109
+ if (mapped) return [['white-space-collapse', mapped[0]], ['text-wrap-mode', mapped[1]]];
1110
+ }
1111
+ const parsed = anyOrder(comps, [one((c) => keyword(c, ['collapse', 'preserve', 'preserve-breaks', 'break-spaces'])), one((c) => keyword(c, ['wrap', 'nowrap']))]);
1112
+ return parsed && namesOrInitial(['white-space-collapse', 'text-wrap-mode'], parsed);
1113
+ }
1114
+
1115
+ function fontSynthesis(comps) {
1116
+ const names = ['font-synthesis-weight', 'font-synthesis-style', 'font-synthesis-small-caps'];
1117
+ if (comps.length === 1 && isIdent(comps[0], ['none'])) return same(names, 'none');
1118
+ const parsed = anyOrder(comps, [one((c) => keyword(c, ['weight'])), one((c) => keyword(c, ['style'])), one((c) => keyword(c, ['small-caps']))]);
1119
+ return parsed && names.map((name, index) => [name, parsed[index] !== null ? 'auto' : 'none']);
1120
+ }
1121
+
1122
+ function textBox(comps) {
1123
+ if (comps.length === 1 && isIdent(comps[0], ['normal'])) return [['text-box-trim', 'none'], ['text-box-edge', 'auto']];
1124
+ const trim = (c) => keyword(c, ['none', 'trim-start', 'trim-end', 'trim-both']);
1125
+ const edge = (comps) => {
1126
+ if (!comps.length) return null;
1127
+ const first = comps[0];
1128
+ if (isIdent(first, ['auto'])) return [1, 'auto'];
1129
+ const a = keyword(first, ['text', 'cap', 'ex', 'ideographic', 'ideographic-ink']);
1130
+ if (a === null) return null;
1131
+ const b = comps[1] ? keyword(comps[1], ['text', 'alphabetic', 'ideographic', 'ideographic-ink']) : null;
1132
+ if (b !== null) return [2, a === b ? a : `${a} ${b}`];
1133
+ return ['text', 'ideographic', 'ideographic-ink'].includes(a) ? [1, a] : null;
1134
+ };
1135
+ const parsed = anyOrder(comps, [one(trim), edge]);
1136
+ return parsed && [['text-box-trim', parsed[0] ?? 'trim-both'], ['text-box-edge', parsed[1] ?? 'auto']];
1137
+ }
1138
+
1139
+ const TRY_TACTICS = ['flip-block', 'flip-inline', 'flip-start', 'flip-x', 'flip-y'];
1140
+ function tryFallbacks(comps) {
1141
+ const items = commas(comps);
1142
+ if (!items) return null;
1143
+ const out = [];
1144
+ for (const item of items) {
1145
+ let name = null;
1146
+ const tactics = [];
1147
+ for (const comp of item) {
1148
+ const dashed = name === null ? dashedIdent(comp) : null;
1149
+ if (dashed !== null) { name = dashed; continue; }
1150
+ const tactic = keyword(comp, TRY_TACTICS);
1151
+ if (tactic === null || tactics.includes(tactic)) return null;
1152
+ tactics.push(tactic);
1153
+ }
1154
+ out.push([...(name === null ? [] : [name]), ...tactics].join(' '));
1155
+ }
1156
+ return out.join(', ');
1157
+ }
1158
+ function positionTry(comps) {
1159
+ const orders = ['normal', 'most-width', 'most-height', 'most-block-size', 'most-inline-size'];
1160
+ let order = 'normal', rest = comps;
1161
+ if (comps.length > 1 && isIdent(comps[0], orders)) { order = ident(comps[0]); rest = comps.slice(1); }
1162
+ const fallbacks = rest.length === 1 && isIdent(rest[0], ['none']) ? 'none' : tryFallbacks(rest);
1163
+ return fallbacks === null ? null : [['position-try-order', order], ['position-try-fallbacks', fallbacks]];
1164
+ }
1165
+
1166
+ function scrollTimeline(comps) {
1167
+ const layers = commas(comps);
1168
+ if (!layers) return null;
1169
+ const nameList = [], axisList = [];
1170
+ for (const layer of layers) {
1171
+ if (layer.length > 2) return null;
1172
+ const name = keyword(layer[0], ['none']) ?? dashedIdent(layer[0]);
1173
+ if (name === null) return null;
1174
+ nameList.push(name);
1175
+ if (layer[1]) { const axis = keyword(layer[1], ['block', 'inline', 'x', 'y']); if (axis === null) return null; axisList.push(axis); } else axisList.push('block');
1176
+ }
1177
+ return [['scroll-timeline-name', nameList.join(', ')], ['scroll-timeline-axis', axisList.join(', ')]];
1178
+ }
1179
+ function viewTimeline(comps) {
1180
+ const layers = commas(comps);
1181
+ if (!layers) return null;
1182
+ const nameList = [], axisList = [], insetList = [];
1183
+ for (const layer of layers) {
1184
+ const name = keyword(layer[0], ['none']) ?? dashedIdent(layer[0]);
1185
+ if (name === null) return null;
1186
+ nameList.push(name);
1187
+ const inset = (comps) => {
1188
+ const value = (c) => keyword(c, ['auto']) ?? lengthPercentage(c, true);
1189
+ if (!comps.length) return null;
1190
+ const a = value(comps[0]);
1191
+ if (a === null) return null;
1192
+ const b = comps[1] ? value(comps[1]) : null;
1193
+ return b !== null ? [2, a === b ? a : `${a} ${b}`] : [1, a];
1194
+ };
1195
+ const parsed = anyOrder(layer.slice(1), [one((c) => keyword(c, ['block', 'inline', 'x', 'y'])), inset]);
1196
+ if (!parsed) return null;
1197
+ axisList.push(parsed[0] ?? 'block');
1198
+ insetList.push(parsed[1] ?? 'auto');
1199
+ }
1200
+ return [['view-timeline-name', nameList.join(', ')], ['view-timeline-axis', axisList.join(', ')], ['view-timeline-inset', insetList.join(', ')]];
1201
+ }
1202
+
1203
+ const LIGATURES = [['common-ligatures', 'no-common-ligatures'], ['discretionary-ligatures', 'no-discretionary-ligatures'], ['historical-ligatures', 'no-historical-ligatures'], ['contextual', 'no-contextual']];
1204
+ const NUMERIC = [['lining-nums', 'oldstyle-nums'], ['proportional-nums', 'tabular-nums'], ['diagonal-fractions', 'stacked-fractions'], ['ordinal'], ['slashed-zero']];
1205
+ const EAST_ASIAN = [['jis78', 'jis83', 'jis90', 'jis04', 'simplified', 'traditional'], ['full-width', 'proportional-width'], ['ruby']];
1206
+ const CAPS = ['small-caps', 'all-small-caps', 'petite-caps', 'all-petite-caps', 'unicase', 'titling-caps'];
1207
+ function groupSet(comps, groups) {
1208
+ const picked = groups.map(() => null);
1209
+ let used = 0;
1210
+ for (const comp of comps) {
1211
+ const name = ident(comp);
1212
+ if (name === null) break;
1213
+ const group = groups.findIndex((g) => g.includes(name));
1214
+ if (group < 0) break;
1215
+ if (picked[group] !== null) return null;
1216
+ picked[group] = name;
1217
+ used++;
1218
+ }
1219
+ return used ? [used, picked.filter((v) => v !== null).join(' ')] : null;
1220
+ }
1221
+ const ALTERNATES_ORDER = ['stylistic', 'historical-forms', 'styleset', 'character-variant', 'swash', 'ornaments', 'annotation'];
1222
+ const isAlternate = (c) => isIdent(c, ['historical-forms']) || (c.kind === 'function' && ALTERNATES_ORDER.includes(c.name) && c.name !== 'historical-forms');
1223
+ /**
1224
+ * `font-variant-alternates` values: `historical-forms` and the feature functions, each at most
1225
+ * once, in Chrome's order; `[used, text]` from the front of `comps`.
1226
+ */
1227
+ function alternates(comps) {
1228
+ const found = [];
1229
+ for (const comp of comps) {
1230
+ if (!isAlternate(comp)) break;
1231
+ const rank = ALTERNATES_ORDER.indexOf(comp.kind === 'ident' ? 'historical-forms' : comp.name);
1232
+ if (found.some(([seen]) => seen === rank)) return null;
1233
+ found.push([rank, comp.kind === 'ident' ? 'historical-forms' : comp.text]);
1234
+ }
1235
+ if (!found.length) return null;
1236
+ const used = found.length;
1237
+ found.sort((a, b) => a[0] - b[0]);
1238
+ return [used, found.map(([, text]) => text).join(' ')];
1239
+ }
1240
+ function fontVariant(comps) {
1241
+ const order = ['font-variant-ligatures', 'font-variant-caps', 'font-variant-numeric', 'font-variant-east-asian', 'font-variant-alternates', 'font-variant-position', 'font-variant-emoji'];
1242
+ if (comps.length === 1 && isIdent(comps[0], ['normal'])) return same(order, 'normal');
1243
+ if (comps.length === 1 && isIdent(comps[0], ['none'])) { const out = same(order, 'normal'); out[0][1] = 'none'; return out; }
1244
+ // The alternates values may sit anywhere among the others.
1245
+ const alternateComps = comps.filter(isAlternate), others = comps.filter((c) => !isAlternate(c));
1246
+ let alternatesValue = null;
1247
+ if (alternateComps.length) { const a = alternates(alternateComps); if (!a || a[0] !== alternateComps.length) return null; alternatesValue = a[1]; }
1248
+ const parsed = others.length ? anyOrder(others, [(c) => groupSet(c, LIGATURES), (c) => groupSet(c, NUMERIC), (c) => groupSet(c, EAST_ASIAN), one((c) => keyword(c, CAPS)), () => null, one((c) => keyword(c, ['sub', 'super'])), one((c) => keyword(c, ['text', 'emoji', 'unicode']))]) : order.map(() => null);
1249
+ if (!parsed) return null;
1250
+ parsed[4] = alternatesValue;
1251
+ const names = ['font-variant-ligatures', 'font-variant-numeric', 'font-variant-east-asian', 'font-variant-caps', 'font-variant-alternates', 'font-variant-position', 'font-variant-emoji'];
1252
+ return names.map((name, index) => [name, parsed[index] ?? 'normal']);
1253
+ }
1254
+
1255
+ function flex(comps) {
1256
+ const names = ['flex-grow', 'flex-shrink', 'flex-basis'];
1257
+ if (comps.length === 1 && isIdent(comps[0], ['none'])) return named(names, ['0', '0', 'auto']);
1258
+ if (comps.length === 1 && isIdent(comps[0], ['auto'])) return named(names, ['1', '1', 'auto']);
1259
+ const basis = (c) => keyword(c, ['auto', 'content', 'min-content', 'max-content', 'fit-content']) ?? lengthPercentage(c, false);
1260
+ let grow = null, shrink = null, basisValue = null, at = 0;
1261
+ while (at < comps.length) {
1262
+ const comp = comps[at];
1263
+ if (grow === null) {
1264
+ const n = number(comp, false);
1265
+ if (n !== null) {
1266
+ grow = n;
1267
+ const next = comps[at + 1] ? number(comps[at + 1], false) : null;
1268
+ if (next !== null) { shrink = next; at++; }
1269
+ at++;
1270
+ continue;
1271
+ }
1272
+ }
1273
+ if (basisValue === null) {
1274
+ const b = basis(comp);
1275
+ if (b !== null) { basisValue = b; at++; continue; }
1276
+ }
1277
+ return null;
1278
+ }
1279
+ if (grow === null && basisValue === null) return null;
1280
+ return named(names, [grow ?? '1', shrink ?? '1', basisValue ?? '0%']);
1281
+ }
1282
+
1283
+ const SYSTEM_FONTS = ['caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar'];
1284
+ const FONT_SIZES = ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', 'xxx-large', 'larger', 'smaller', '-webkit-xxx-large'];
1285
+ const STRETCHES = ['ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed', 'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded'];
1286
+ const GENERIC_FAMILIES = ['serif', 'sans-serif', 'cursive', 'fantasy', 'monospace', 'system-ui', 'math', 'emoji', 'fangsong', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', '-webkit-body', '-webkit-pictograph'];
1287
+ const reserved = (name, extra) => { const lower = name.toLowerCase(); return CSS_WIDE.has(lower) || lower === 'default' || extra.includes(lower); };
1288
+ /**
1289
+ * A family name as Chrome serializes it: unquoted when it is one identifier (an optional `-`,
1290
+ * a name-start character, name characters; no escapes) Chrome would not read as a keyword, a
1291
+ * string otherwise.
1292
+ */
1293
+ const familyText = (name) => (/^-?[A-Za-z_\u0080-\uffff][A-Za-z0-9_\-\u0080-\uffff]*$/.test(name) && !reserved(name, GENERIC_FAMILIES) ? name : quoted(name));
1294
+ const familyString = (comp) => familyText(comp.value);
1295
+ /** CSSOM "serialize an identifier". */
1296
+ function serializeIdentifier(text) {
1297
+ const chars = [...text];
1298
+ let out = '';
1299
+ chars.forEach((c, index) => {
1300
+ const code = c.codePointAt(0);
1301
+ if (code === 0) out += '\ufffd';
1302
+ else if ((code >= 1 && code <= 0x1f) || code === 0x7f || (index === 0 && /[0-9]/.test(c)) || (index === 1 && /[0-9]/.test(c) && chars[0] === '-')) out += `\\${code.toString(16)} `;
1303
+ else if (index === 0 && c === '-' && chars.length === 1) out += '\\-';
1304
+ else if (code >= 0x80 || /[-_0-9A-Za-z]/.test(c)) out += c;
1305
+ else out += `\\${c}`;
1306
+ });
1307
+ return out;
1308
+ }
1309
+ /** A quoted animation name: an identifier unless it is empty (invalid) or reads as `none`, a CSS-wide keyword or `default` (kept quoted). */
1310
+ const animationNameString = (c) => (c.value === '' ? null : reserved(c.value, ['none']) ? c.text : serializeIdentifier(c.value));
1311
+ function fontFamily(comps) {
1312
+ const items = commas(comps);
1313
+ if (!items) return null;
1314
+ const families = [];
1315
+ for (const family of items) {
1316
+ if (family.length === 1 && string(family[0]) !== null) { families.push(familyString(family[0])); continue; }
1317
+ if (family.some((c) => c.kind !== 'ident')) return null;
1318
+ const words = family.map((c) => c.value);
1319
+ if (words.length === 1 && CSS_WIDE.has(words[0].toLowerCase())) return null;
1320
+ // Chrome quotes a family name of several identifiers.
1321
+ const name = words.join(' ');
1322
+ const lower = name.toLowerCase();
1323
+ // A generic family is a keyword: lowercase.
1324
+ families.push(words.length > 1 ? quoted(name) : GENERIC_FAMILIES.includes(lower) ? lower : familyText(name));
1325
+ }
1326
+ return families.join(', ');
1327
+ }
1328
+ /** `oblique <angle>`: Chrome checks a literal angle's number, in its own unit, against [-90, 90]; a zero angle is `normal`. */
1329
+ function oblique(c) {
1330
+ // A unitless zero is not an angle here.
1331
+ if (c.kind === 'number') return null;
1332
+ const text = angle(c);
1333
+ if (text === null) return null;
1334
+ if (c.kind === 'dimension' && (c.value < -90 || c.value > 90)) return null;
1335
+ if ((c.kind === 'dimension' || c.kind === 'number') && c.value === 0) return 'normal';
1336
+ return `oblique ${text}`;
1337
+ }
1338
+ function font(comps) {
1339
+ if (comps.length === 1) { const name = keyword(comps[0], SYSTEM_FONTS); if (name !== null) return { pending: name }; }
1340
+ let style = null, caps = null, weight = null, stretch = null, at = 0;
1341
+ while (at < comps.length && at < 4) {
1342
+ const comp = comps[at];
1343
+ if (isIdent(comp, ['normal'])) { at++; continue; }
1344
+ if (style === null) {
1345
+ const value = keyword(comp, ['italic', 'oblique']);
1346
+ if (value !== null) {
1347
+ if (value === 'oblique' && comps[at + 1] && comps[at + 1].kind === 'dimension' && angle(comps[at + 1]) !== null) {
1348
+ style = oblique(comps[at + 1]);
1349
+ if (style === null) return null;
1350
+ at += 2;
1351
+ continue;
1352
+ }
1353
+ style = value; at++; continue;
1354
+ }
1355
+ }
1356
+ if (caps === null) { const value = keyword(comp, ['small-caps']); if (value !== null) { caps = value; at++; continue; } }
1357
+ if (weight === null) {
1358
+ const value = keyword(comp, ['bold', 'bolder', 'lighter']) ?? (comp.kind === 'number' && comp.value >= 1 && comp.value <= 1000 ? number(comp, false) : null);
1359
+ if (value !== null) { weight = value; at++; continue; }
1360
+ }
1361
+ if (stretch === null) { const value = keyword(comp, STRETCHES); if (value !== null) { stretch = value; at++; continue; } }
1362
+ break;
1363
+ }
1364
+ const sizeComp = comps[at];
1365
+ if (!sizeComp) return null;
1366
+ const size = keyword(sizeComp, FONT_SIZES) ?? lengthPercentage(sizeComp, false);
1367
+ if (size === null) return null;
1368
+ at++;
1369
+ let lineHeight = 'normal';
1370
+ if (comps[at] && isSlash(comps[at])) {
1371
+ const value = comps[at + 1];
1372
+ if (!value) return null;
1373
+ lineHeight = keyword(value, ['normal']) ?? number(value, false) ?? lengthPercentage(value, false);
1374
+ if (lineHeight === null) return null;
1375
+ at += 2;
1376
+ }
1377
+ const rest = comps.slice(at);
1378
+ if (!rest.length) return null;
1379
+ const family = fontFamily(rest);
1380
+ if (family === null) return null;
1381
+ return [['font-style', style ?? 'normal'], ['font-variant-caps', caps ?? 'normal'], ['font-variant-ligatures', 'normal'], ['font-variant-numeric', 'normal'], ['font-variant-east-asian', 'normal'], ['font-variant-alternates', 'normal'], ['font-size-adjust', 'none'], ['font-language-override', 'normal'], ['font-kerning', 'auto'], ['font-optical-sizing', 'auto'], ['font-feature-settings', 'normal'], ['font-variation-settings', 'normal'], ['font-variant-position', 'normal'], ['font-variant-emoji', 'normal'], ['font-weight', weight ?? 'normal'], ['font-stretch', stretch ?? 'normal'], ['font-size', size], ['line-height', lineHeight], ['font-family', family]];
1382
+ }
1383
+
1384
+ /** `<grid-line>` with Chrome's canonical part order (`span`, integer, name). */
1385
+ function gridLine(comps) {
1386
+ if (comps.length === 1 && isIdent(comps[0], ['auto'])) return 'auto';
1387
+ if (!comps.length) return null;
1388
+ let span = false, int = null, name = null;
1389
+ for (const comp of comps) {
1390
+ if (isIdent(comp, ['span']) && !span) { span = true; continue; }
1391
+ const n = int === null ? integer(comp, -Infinity) : null;
1392
+ if (n !== null) { if (n === '0') return null; int = n; continue; }
1393
+ const c = name === null ? customIdent(comp, ['auto', 'span']) : null;
1394
+ if (c !== null) { name = c; continue; }
1395
+ return null;
1396
+ }
1397
+ if (int === null && name === null) return null;
1398
+ if (span && int !== null && int.startsWith('-')) return null;
1399
+ return [span ? 'span' : null, int, name].filter((v) => v !== null).join(' ');
1400
+ }
1401
+ /** A line's default end / omitted value: a lone custom ident repeats, else `auto`. */
1402
+ function lineDefault(start) {
1403
+ const lone = start !== '' && !start.includes(' ') && start !== 'auto' && (start.startsWith('--') || !/^[0-9+-]/.test(start));
1404
+ return lone ? start : 'auto';
1405
+ }
1406
+ function gridLines(comps, start, end) {
1407
+ const parts = slashes(comps);
1408
+ if (parts.length === 1) { const a = gridLine(parts[0]); return a === null ? null : [[start, a], [end, lineDefault(a)]]; }
1409
+ if (parts.length === 2) { const a = gridLine(parts[0]), b = gridLine(parts[1]); return a === null || b === null ? null : [[start, a], [end, b]]; }
1410
+ return null;
1411
+ }
1412
+ function gridArea(comps) {
1413
+ const parts = slashes(comps);
1414
+ if (!parts.length || parts.length > 4) return null;
1415
+ const lines = parts.map(gridLine);
1416
+ if (lines.some((v) => v === null)) return null;
1417
+ const rowStart = lines[0];
1418
+ const columnStart = lines[1] ?? lineDefault(rowStart);
1419
+ const rowEnd = lines[2] ?? lineDefault(rowStart);
1420
+ const columnEnd = lines[3] ?? lineDefault(columnStart);
1421
+ return [['grid-row-start', rowStart], ['grid-column-start', columnStart], ['grid-row-end', rowEnd], ['grid-column-end', columnEnd]];
1422
+ }
1423
+ /** A track breadth: a non-negative `<length-percentage>`, a sizing keyword, or (where `flex`) a non-negative `fr`. */
1424
+ function trackBreadth(c, flex) {
1425
+ if (c.kind === 'dimension' && c.unit === 'fr') return flex && c.value >= 0;
1426
+ if (c.kind === 'ident') return isIdent(c, ['auto', 'min-content', 'max-content']);
1427
+ if (c.kind === 'function' && c.name === '[') return false;
1428
+ return lengthPercentage(c, false) !== null;
1429
+ }
1430
+ /** At least one track, and no two line-name blocks side by side. */
1431
+ function tracksValid(comps) {
1432
+ const names = (c) => c.kind === 'function' && c.name === '[';
1433
+ return comps.some((c) => !names(c)) && !comps.some((c, index) => index > 0 && names(c) && names(comps[index - 1]));
1434
+ }
1435
+ /** An auto-repeated track: line names or a fixed size (a `<length-percentage>`, or a `minmax()` with a fixed bound). */
1436
+ function trackFixed(c) {
1437
+ const fixed = (x) => x.kind !== 'ident' && lengthPercentage(x, false) !== null;
1438
+ if (c.kind === 'function' && c.name === '[') return true;
1439
+ if (c.kind === 'function' && c.name === 'minmax') { const inner = components(c.text.slice(7, -1)); return !!inner && inner.length === 3 && (fixed(inner[0]) || fixed(inner[2])); }
1440
+ if (c.kind === 'function' && c.name === 'fit-content') return false;
1441
+ return fixed(c);
1442
+ }
1443
+ /** A track component spelled like Chrome: a unitless zero track size as `0px`, keywords lowercase, inside minmax() / fit-content() / repeat() too. */
1444
+ function trackText(c, repeatCount) {
1445
+ if (c.kind === 'number' && !repeatCount) return '0px';
1446
+ if (c.kind === 'ident') return c.value.toLowerCase();
1447
+ if (c.kind === 'function' && ['minmax', 'fit-content', 'repeat'].includes(c.name)) {
1448
+ const inner = components(c.text.slice(c.name.length + 1, -1));
1449
+ if (!inner) return c.text;
1450
+ let seenComma = false;
1451
+ const spelled = inner.map((part) => { const count = c.name === 'repeat' && !seenComma; if (isComma(part)) seenComma = true; return { ...part, text: trackText(part, count) }; });
1452
+ return `${c.name}(${join(spelled)})`;
1453
+ }
1454
+ return c.text;
1455
+ }
1456
+ /** One track-list component: line names, a track size, or (at the top level) `repeat()`. */
1457
+ function trackOk(c, repeatAllowed) {
1458
+ if (c.kind !== 'function') return trackBreadth(c, true);
1459
+ if (c.name === '[') return true;
1460
+ const inner = components(c.text.slice(c.name.length + 1, -1));
1461
+ if (c.name === 'minmax') return !!inner && inner.length === 3 && isComma(inner[1]) && trackBreadth(inner[0], false) && trackBreadth(inner[2], true);
1462
+ if (c.name === 'fit-content') return !!inner && inner.length === 1 && lengthPercentage(inner[0], false) !== null;
1463
+ if (c.name === 'repeat') {
1464
+ if (!repeatAllowed || !inner) return false;
1465
+ const comma = inner.findIndex(isComma);
1466
+ if (comma < 0) return false;
1467
+ const head = inner.slice(0, comma), tracks = inner.slice(comma + 1);
1468
+ const auto = head.length === 1 && isIdent(head[0], ['auto-fill', 'auto-fit']);
1469
+ const count = head.length === 1 && (head[0].kind === 'number' ? head[0].integer && head[0].value >= 1 : auto);
1470
+ return count && tracksValid(tracks) && tracks.every((track) => trackOk(track, false) && (!auto || trackFixed(track)));
1471
+ }
1472
+ return lengthPercentage(c, false) !== null;
1473
+ }
1474
+ function trackList(comps) {
1475
+ if (!comps.length) return null;
1476
+ if (comps.length === 1 && isIdent(comps[0], ['none'])) return 'none';
1477
+ const subgrid = isIdent(comps[0], ['subgrid']);
1478
+ for (let index = 0; index < comps.length; index++) {
1479
+ const comp = comps[index];
1480
+ const ok = comp.kind === 'ident' ? (index === 0 && subgrid) || isIdent(comp, ['auto', 'min-content', 'max-content', 'masonry']) : trackOk(comp, true);
1481
+ if (!ok) return null;
1482
+ }
1483
+ if (!subgrid && !tracksValid(comps)) return null;
1484
+ return join(comps.map((c) => ({ ...c, text: trackText(c, false) })));
1485
+ }
1486
+ /**
1487
+ * A `grid-template-areas` string's cell tokens: runs of name code points, a run of `.` one null
1488
+ * cell `.`; null for a row without cells or with any other character.
1489
+ */
1490
+ function areaRow(comp) {
1491
+ if (comp.kind !== 'string') return null;
1492
+ const tokens = comp.value.match(/[A-Za-z0-9_\-\u0080-\uffff]+|\.+|[^ \t\n\r\f]/g) ?? [];
1493
+ if (!tokens.length || tokens.some((token) => !/^([A-Za-z0-9_\-\u0080-\uffff]+|\.+)$/.test(token))) return null;
1494
+ return tokens.map((token) => (token[0] === '.' ? '.' : token));
1495
+ }
1496
+ /**
1497
+ * `grid-template-areas` rows Chrome accepts (as many cells each, every named area a filled
1498
+ * rectangle) as quoted rows.
1499
+ */
1500
+ function areasText(rows) {
1501
+ if (rows.some((row) => row === null || row.length !== rows[0].length)) return null;
1502
+ const boxes = new Map();
1503
+ rows.forEach((row, y) => row.forEach((cell, x) => {
1504
+ if (cell === '.') return;
1505
+ const box = boxes.get(cell) ?? { x0: x, x1: x, y0: y, y1: y, count: 0 };
1506
+ Object.assign(box, { x0: Math.min(box.x0, x), x1: Math.max(box.x1, x), y0: Math.min(box.y0, y), y1: Math.max(box.y1, y), count: box.count + 1 });
1507
+ boxes.set(cell, box);
1508
+ }));
1509
+ for (const box of boxes.values()) if ((box.x1 - box.x0 + 1) * (box.y1 - box.y0 + 1) !== box.count) return null;
1510
+ return rows.map((row) => `"${row.join(' ')}"`).join(' ');
1511
+ }
1512
+ function gridTemplate(comps) {
1513
+ if (comps.length === 1 && isIdent(comps[0], ['none'])) return ['none', 'none', 'none'];
1514
+ const hasStrings = comps.some((c) => string(c) !== null);
1515
+ const parts = slashes(comps);
1516
+ if (!hasStrings) {
1517
+ if (parts.length !== 2) return null;
1518
+ const rows = trackList(parts[0]), cols = trackList(parts[1]);
1519
+ return rows === null || cols === null ? null : [rows, cols, 'none'];
1520
+ }
1521
+ let rows, cols = null;
1522
+ if (parts.length === 1) rows = parts[0];
1523
+ // The explicit columns of the areas form take no `repeat()`.
1524
+ else if (parts.length === 2 && !parts[1].some((c) => functionNamed(c, ['repeat']) !== null)) { rows = parts[0]; cols = trackList(parts[1]); if (cols === null) return null; }
1525
+ else return null;
1526
+ const areas = [], tracks = [];
1527
+ let at = 0;
1528
+ const isNames = (c) => c.kind === 'function' && c.name === '[';
1529
+ const names = () => { if (rows[at] && isNames(rows[at])) { tracks.push(rows[at].text); at++; } };
1530
+ while (at < rows.length) {
1531
+ names();
1532
+ const area = rows[at] ? areaRow(rows[at]) : null;
1533
+ if (area === null) return null;
1534
+ areas.push(area);
1535
+ at++;
1536
+ const c = rows[at];
1537
+ let size = null;
1538
+ if (c && string(c) === null && !isNames(c)) size = lengthPercentage(c, false) ?? keyword(c, ['auto', 'min-content', 'max-content']) ?? functionNamed(c, ['minmax', 'fit-content']) ?? (c.kind === 'dimension' && c.unit === 'fr' ? c.text : null);
1539
+ if (size !== null) { tracks.push(size); at++; } else tracks.push('auto');
1540
+ names();
1541
+ }
1542
+ const merged = [];
1543
+ for (const track of tracks) {
1544
+ const last = merged[merged.length - 1];
1545
+ if (last !== undefined && track.startsWith('[') && last.startsWith('[')) { merged[merged.length - 1] = `${last.slice(0, -1)} ${track.slice(1)}`; continue; }
1546
+ merged.push(track);
1547
+ }
1548
+ const areasValue = areasText(areas);
1549
+ return areasValue === null ? null : [merged.join(' '), cols ?? 'none', areasValue];
1550
+ }
1551
+ function grid(comps) {
1552
+ const parts = slashes(comps);
1553
+ const autoFlow = (side) => {
1554
+ let flow = false, dense = false;
1555
+ const rest = [];
1556
+ for (const comp of side) {
1557
+ if (isIdent(comp, ['auto-flow']) && !flow) flow = true;
1558
+ else if (isIdent(comp, ['dense']) && !dense) dense = true;
1559
+ else rest.push(comp);
1560
+ }
1561
+ return flow ? [dense, rest] : null;
1562
+ };
1563
+ if (parts.length === 2) {
1564
+ const [rows, cols] = parts;
1565
+ const r = autoFlow(rows);
1566
+ if (r) {
1567
+ const auto = r[1].length ? trackList(r[1]) : 'auto';
1568
+ const c = trackList(cols);
1569
+ if (auto === null || c === null) return null;
1570
+ return [['grid-template-columns', c], ['grid-template-rows', 'none'], ['grid-template-areas', 'none'], ['grid-auto-flow', r[0] ? 'dense' : 'row'], ['grid-auto-columns', 'auto'], ['grid-auto-rows', auto]];
1571
+ }
1572
+ const k = autoFlow(cols);
1573
+ if (k) {
1574
+ const auto = k[1].length ? trackList(k[1]) : 'auto';
1575
+ const t = trackList(rows);
1576
+ if (auto === null || t === null) return null;
1577
+ return [['grid-template-columns', 'none'], ['grid-template-rows', t], ['grid-template-areas', 'none'], ['grid-auto-flow', k[0] ? 'column dense' : 'column'], ['grid-auto-columns', auto], ['grid-auto-rows', 'auto']];
1578
+ }
1579
+ }
1580
+ if (comps.some((c) => isIdent(c, ['auto-flow', 'dense']))) return null;
1581
+ const template = gridTemplate(comps);
1582
+ return template && [['grid-template-rows', template[0]], ['grid-template-columns', template[1]], ['grid-template-areas', template[2]], ['grid-auto-flow', 'row'], ['grid-auto-columns', 'auto'], ['grid-auto-rows', 'auto']];
1583
+ }
1584
+
1585
+ /** Collapse a one-to-four value side list the `margin` way. */
1586
+ function collapse4(values) {
1587
+ const full = values.length === 1 ? [values[0], values[0], values[0], values[0]] : values.length === 2 ? [values[0], values[1], values[0], values[1]] : values.length === 3 ? [values[0], values[1], values[2], values[1]] : values.length === 4 ? values : null;
1588
+ return full ? boxText(full) : values.join(' ');
1589
+ }
1590
+ function borderImage(comps, webkit) {
1591
+ let source = null, slice = null, width = null, outset = null, repeat = null, at = 0;
1592
+ while (at < comps.length) {
1593
+ const comp = comps[at];
1594
+ if (source === null) { const value = image(comp) ?? keyword(comp, ['none']); if (value !== null) { source = value; at++; continue; } }
1595
+ if (repeat === null) {
1596
+ const kw = (c) => keyword(c, ['stretch', 'repeat', 'round', 'space']);
1597
+ const first = kw(comp);
1598
+ if (first !== null) {
1599
+ const second = comps[at + 1] ? kw(comps[at + 1]) : null;
1600
+ at += second !== null ? 2 : 1;
1601
+ repeat = second !== null && second !== first ? `${first} ${second}` : first;
1602
+ continue;
1603
+ }
1604
+ }
1605
+ if (slice === null) {
1606
+ const values = [];
1607
+ let fill = false;
1608
+ while (comps[at]) {
1609
+ const c = comps[at];
1610
+ if (isIdent(c, ['fill']) && !fill) fill = true;
1611
+ else if (fill && values.length) break; // `fill` goes before or after the numbers
1612
+ else { const value = values.length < 4 ? number(c, false) ?? percentage(c, false) : null; if (value === null) break; values.push(value); }
1613
+ at++;
1614
+ }
1615
+ if (!values.length) return null;
1616
+ slice = collapse4(values) + (fill || webkit ? ' fill' : '');
1617
+ const quad = (ty) => {
1618
+ const list = [];
1619
+ while (comps[at] && list.length < 4) { const value = ty(comps[at]); if (value === null) break; list.push(value); at++; }
1620
+ return list.length ? collapse4(list) : null;
1621
+ };
1622
+ if (comps[at] && isSlash(comps[at])) {
1623
+ at++;
1624
+ const w = quad((c) => number(c, false) ?? lengthPercentage(c, false) ?? keyword(c, ['auto']));
1625
+ let o = null;
1626
+ if (comps[at] && isSlash(comps[at])) {
1627
+ at++;
1628
+ o = quad((c) => number(c, false) ?? length(c, false));
1629
+ if (o === null) return null;
1630
+ } else if (w === null) return null;
1631
+ width = w; outset = o;
1632
+ }
1633
+ continue;
1634
+ }
1635
+ return null;
1636
+ }
1637
+ if ([source, slice, width, outset, repeat].every((v) => v === null)) return null;
1638
+ return [source, slice, width, outset, repeat];
1639
+ }
1640
+ /** `path()`'s string holds path data: at least one command, only path data characters. */
1641
+ function pathDataValid(text) {
1642
+ const start = text.indexOf('"'), end = text.lastIndexOf('"');
1643
+ return end > start && start >= 0 && /^[\sMmLlHhVvCcSsQqTtAaZz0-9.,+eE-]*$/.test(text.slice(start + 1, end)) && text.slice(start + 1, end).trim() !== '';
1644
+ }
1645
+ /** `<offset-rotate>` from the front: `[auto | reverse] || <angle>` (an angle has a unit). */
1646
+ function rotatePart(comps) {
1647
+ if (!comps.length) return null;
1648
+ const first = comps[0];
1649
+ const kw = keyword(first, ['auto', 'reverse']);
1650
+ if (kw !== null) { const a = comps[1] ? angleStrict(comps[1]) : null; return a !== null ? [2, `${kw} ${a}`] : [1, kw]; }
1651
+ const a = angleStrict(first);
1652
+ if (a === null) return null;
1653
+ const kw2 = comps[1] ? keyword(comps[1], ['auto', 'reverse']) : null;
1654
+ return kw2 !== null ? [2, `${kw2} ${a}`] : [1, a];
1655
+ }
1656
+ function pathText(text) {
1657
+ // pathDataValid found the quoted data.
1658
+ const start = text.indexOf('"');
1659
+ const end = text.lastIndexOf('"');
1660
+ const inner = text.slice(start + 1, end > start ? end : text.length);
1661
+ const tokens = [];
1662
+ let current = '';
1663
+ for (const c of inner) {
1664
+ if (/[A-Za-z]/.test(c) && c !== 'e' && c !== 'E') { if (current) { tokens.push(current); current = ''; } tokens.push(c); }
1665
+ else if (/\s/.test(c) || c === ',') { if (current) { tokens.push(current); current = ''; } }
1666
+ else if (c === '-' && current && !/[eE]$/.test(current)) { tokens.push(current); current = c; }
1667
+ else current += c;
1668
+ }
1669
+ if (current) tokens.push(current);
1670
+ return `${text.slice(0, start)}"${tokens.join(' ')}")`;
1671
+ }
1672
+ function offset(comps) {
1673
+ const parts = slashes(comps);
1674
+ let main, anchorPart = null;
1675
+ if (parts.length === 1) main = parts[0];
1676
+ else if (parts.length === 2) { main = parts[0]; anchorPart = parts[1]; }
1677
+ else return null;
1678
+ let anchor;
1679
+ if (anchorPart === null) anchor = 'auto';
1680
+ else if (anchorPart.length === 1 && isIdent(anchorPart[0], ['auto'])) anchor = 'auto';
1681
+ // Three-value positions are background-only.
1682
+ else if (anchorPart.length === 3) return null;
1683
+ else { const p = position(anchorPart); if (!p) return null; anchor = `${p[0]} ${p[1]}`; }
1684
+ let at = 0, positionValue = 'normal';
1685
+ if (main.length) {
1686
+ if (isIdent(main[0], ['auto', 'normal'])) { positionValue = ident(main[0]); at = 1; }
1687
+ else {
1688
+ for (let len = 4; len >= 1; len--) {
1689
+ if (main.length < len) continue;
1690
+ const slice = main.slice(0, len);
1691
+ if (slice.every((c) => keyword(c, ['left', 'right', 'top', 'bottom', 'center']) !== null || lengthPercentage(c, true) !== null)) {
1692
+ const p = position(slice);
1693
+ if (p) { if (len === 3) return null; positionValue = `${p[0]} ${p[1]}`; at = len; break; }
1694
+ }
1695
+ }
1696
+ }
1697
+ }
1698
+ let path = 'none', distance = '0px', rotate = 'auto';
1699
+ const comp = main[at];
1700
+ if (comp) {
1701
+ {
1702
+ const value = isIdent(comp, ['none']) ? 'none' : functionNamed(comp, ['path', 'ray', 'url', 'circle', 'ellipse', 'inset', 'polygon', 'xywh', 'rect', 'shape']) ?? url(comp);
1703
+ if (value !== null) {
1704
+ if (value.startsWith('path(') && !pathDataValid(value)) return null;
1705
+ path = value.startsWith('path(') ? pathText(value) : value;
1706
+ at++;
1707
+ const parsed = anyOrder(main.slice(at), [one((c) => lengthPercentage(c, true)), rotatePart]);
1708
+ if (!parsed) return null;
1709
+ at = main.length;
1710
+ if (parsed[0] !== null) distance = parsed[0];
1711
+ if (parsed[1] !== null) rotate = parsed[1];
1712
+ }
1713
+ }
1714
+ }
1715
+ if (at !== main.length || (!main.length && parts.length === 1)) return null;
1716
+ return [['offset-position', positionValue], ['offset-path', path], ['offset-distance', distance], ['offset-rotate', rotate], ['offset-anchor', anchor]];
1717
+ }
1718
+ function repeatStyle(comps) {
1719
+ if (!comps.length) return null;
1720
+ const xy = keyword(comps[0], ['repeat-x', 'repeat-y']);
1721
+ if (xy !== null) return [1, xy];
1722
+ const values = ['repeat', 'space', 'round', 'no-repeat'];
1723
+ const a = keyword(comps[0], values);
1724
+ if (a === null) return null;
1725
+ const b = comps[1] ? keyword(comps[1], values) : null;
1726
+ if (b === null) return [1, a];
1727
+ const text = a === b ? a : a === 'repeat' && b === 'no-repeat' ? 'repeat-x' : a === 'no-repeat' && b === 'repeat' ? 'repeat-y' : `${a} ${b}`;
1728
+ return [2, text];
1729
+ }
1730
+ function layerPosition(comps) {
1731
+ const part = (c) => keyword(c, ['left', 'right', 'top', 'bottom', 'center']) !== null || lengthPercentage(c, true) !== null;
1732
+ let available = 0;
1733
+ while (available < comps.length && part(comps[available])) available++;
1734
+ available = Math.min(available, 4);
1735
+ for (let len = available; len >= 1; len--) { const p = position(comps.slice(0, len)); if (p) return [len, p]; }
1736
+ return null;
1737
+ }
1738
+ function bgSize(comps) {
1739
+ if (!comps.length) return null;
1740
+ const cover = keyword(comps[0], ['cover', 'contain']);
1741
+ if (cover !== null) return [1, cover];
1742
+ const size = (c) => lengthPercentage(c, false) ?? keyword(c, ['auto']);
1743
+ const a = size(comps[0]);
1744
+ if (a === null) return null;
1745
+ const b = comps[1] ? size(comps[1]) : null;
1746
+ if (b !== null) return [2, `${a} ${b}`];
1747
+ // A single width other than `auto` spells its height `auto`.
1748
+ return a === 'auto' ? [1, a] : [1, `${a} auto`];
1749
+ }
1750
+ function layer(comps, mask, last) {
1751
+ const out = { image: null, position: null, size: null, repeat: null, attachment: null, origin: null, clip: null, color: null, composite: null, mode: null };
1752
+ const boxes = mask ? ['border-box', 'padding-box', 'content-box', 'fill-box', 'stroke-box', 'view-box', 'no-clip'] : ['border-box', 'padding-box', 'content-box', 'text', 'border-area'];
1753
+ let at = 0;
1754
+ while (at < comps.length) {
1755
+ const rest = comps.slice(at), comp = comps[at];
1756
+ if (out.image === null) { const value = image(comp) ?? keyword(comp, ['none']); if (value !== null) { out.image = value; at++; continue; } }
1757
+ if (out.position === null) {
1758
+ const p = layerPosition(rest);
1759
+ if (p) {
1760
+ out.position = p[1]; at += p[0];
1761
+ if (comps[at] && isSlash(comps[at])) { const s = bgSize(comps.slice(at + 1)); if (!s) return null; out.size = s[1]; at += 1 + s[0]; }
1762
+ continue;
1763
+ }
1764
+ }
1765
+ if (out.repeat === null) { const r = repeatStyle(rest); if (r) { out.repeat = r[1]; at += r[0]; continue; } }
1766
+ if (!mask && out.attachment === null) { const value = keyword(comp, ['scroll', 'fixed', 'local']); if (value !== null) { out.attachment = value; at++; continue; } }
1767
+ if (out.clip === null || out.origin === null) {
1768
+ const value = keyword(comp, boxes);
1769
+ if (value !== null) {
1770
+ const clipOnly = value === 'text' || value === 'no-clip';
1771
+ if (out.origin === null && !clipOnly) out.origin = value;
1772
+ else if (out.clip === null) out.clip = value;
1773
+ else return null;
1774
+ at++;
1775
+ continue;
1776
+ }
1777
+ }
1778
+ if (mask) {
1779
+ if (out.composite === null) { const value = keyword(comp, ['add', 'subtract', 'intersect', 'exclude']); if (value !== null) { out.composite = value; at++; continue; } }
1780
+ if (out.mode === null) { const value = keyword(comp, ['alpha', 'luminance', 'match-source']); if (value !== null) { out.mode = value; at++; continue; } }
1781
+ } else if (last && out.color === null) { const value = color(comp); if (value !== null) { out.color = value; at++; continue; } }
1782
+ return null;
1783
+ }
1784
+ if (out.origin !== null && out.clip === null) out.clip = out.origin;
1785
+ return out;
1786
+ }
1787
+ function background(comps) {
1788
+ const items = commas(comps);
1789
+ if (!items) return null;
1790
+ const layers = items.map((item, index) => layer(item, false, index + 1 === items.length));
1791
+ if (layers.some((l) => l === null)) return null;
1792
+ const column = (pick) => layers.map((l) => pick(l) ?? INITIAL).join(', ');
1793
+ return [['background-image', column((l) => l.image)], ['background-position-x', column((l) => l.position && l.position[0])], ['background-position-y', column((l) => l.position && l.position[1])], ['background-size', column((l) => l.size)], ['background-repeat', column((l) => l.repeat)], ['background-attachment', column((l) => l.attachment)], ['background-origin', column((l) => l.origin)], ['background-clip', column((l) => l.clip)], ['background-color', layers[layers.length - 1].color ?? INITIAL]];
1794
+ }
1795
+ function maskShorthand(comps) {
1796
+ const items = commas(comps);
1797
+ if (!items) return null;
1798
+ const layers = items.map((item) => layer(item, true, false));
1799
+ if (layers.some((l) => l === null)) return null;
1800
+ const column = (pick, initial) => layers.map((l) => pick(l) ?? initial).join(', ');
1801
+ return [['mask-image', column((l) => l.image, 'none')], ['-webkit-mask-position-x', column((l) => l.position && l.position[0], '0%')], ['-webkit-mask-position-y', column((l) => l.position && l.position[1], '0%')], ['mask-size', column((l) => l.size, 'auto')], ['mask-repeat', column((l) => l.repeat, 'repeat')], ['mask-origin', column((l) => l.origin, 'border-box')], ['mask-clip', column((l) => l.clip, 'border-box')], ['mask-composite', column((l) => l.composite, 'add')], ['mask-mode', column((l) => l.mode, 'match-source')]];
1802
+ }
1803
+ const EASINGS = ['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out', 'step-start', 'step-end'];
1804
+ const easing = (c) => keyword(c, EASINGS) ?? functionNamed(c, ['cubic-bezier', 'steps', 'linear']);
1805
+ function transition(comps) {
1806
+ const items = commas(comps);
1807
+ if (!items) return null;
1808
+ const columns = [[], [], [], [], []];
1809
+ for (const item of items) {
1810
+ const property = (c) => {
1811
+ if (isIdent(c, ['none'])) return 'none';
1812
+ if (isIdent(c, ['all'])) return 'all';
1813
+ const value = customIdent(c, ['none', 'initial']);
1814
+ return value === null ? null : value.startsWith('--') ? value : value.toLowerCase();
1815
+ };
1816
+ const parsed = anyOrder(item, [one((c) => time(c, false)), one(easing), one((c) => time(c, true)), one((c) => keyword(c, ['normal', 'allow-discrete'])), one(property)]);
1817
+ if (!parsed) return null;
1818
+ const prop = parsed[4] ?? 'all';
1819
+ if (prop === 'none' && items.length > 1) return null;
1820
+ columns[0].push(parsed[3] ?? 'normal');
1821
+ columns[1].push(parsed[0] ?? '0s');
1822
+ columns[2].push(parsed[1] ?? 'ease');
1823
+ columns[3].push(parsed[2] ?? '0s');
1824
+ columns[4].push(prop);
1825
+ }
1826
+ const [behavior, duration, timing, delay, property] = columns.map((c) => c.join(', '));
1827
+ return [['transition-behavior', behavior], ['transition-duration', duration], ['transition-timing-function', timing], ['transition-delay', delay], ['transition-property', property]];
1828
+ }
1829
+ function animation(comps) {
1830
+ const items = commas(comps);
1831
+ if (!items) return null;
1832
+ const columns = [[], [], [], [], [], [], [], []];
1833
+ const initials = ['auto', 'ease', '0s', '1', 'normal', 'none', 'running', 'none'];
1834
+ for (const item of items) {
1835
+ // A quoted name serializes as an identifier when it is one Chrome would not read as a keyword.
1836
+ const name = (c) => (isIdent(c, ['none']) ? 'none' : c.kind === 'string' ? animationNameString(c) : customIdent(c, ['none']));
1837
+ const parsed = anyOrder(item, [one((c) => time(c, false) ?? keyword(c, ['auto'])), one(easing), one((c) => time(c, true)), one((c) => keyword(c, ['infinite']) ?? number(c, false)), one((c) => keyword(c, ['normal', 'reverse', 'alternate', 'alternate-reverse'])), one((c) => keyword(c, ['none', 'forwards', 'backwards', 'both'])), one((c) => keyword(c, ['running', 'paused'])), one(name)]);
1838
+ if (!parsed) return null;
1839
+ initials.forEach((initial, index) => columns[index].push(parsed[index] ?? initial));
1840
+ }
1841
+ const names = ['animation-duration', 'animation-timing-function', 'animation-delay', 'animation-iteration-count', 'animation-direction', 'animation-fill-mode', 'animation-play-state', 'animation-name'];
1842
+ return [...names.map((n, index) => [n, columns[index].join(', ')]), ['animation-timeline', 'auto'], ['animation-range-start', 'normal'], ['animation-range-end', 'normal']];
1843
+ }
1844
+ function timelineTrigger(comps) {
1845
+ const defaults = () => [['timeline-trigger-source', 'auto'], ['timeline-trigger-activation-range-start', 'normal'], ['timeline-trigger-activation-range-end', 'normal'], ['timeline-trigger-active-range-start', 'auto'], ['timeline-trigger-active-range-end', 'auto']];
1846
+ if (comps.length === 1 && isIdent(comps[0], ['none'])) return [['timeline-trigger-name', 'none'], ...defaults()];
1847
+ // A comma list of triggers: each longhand lists its value per trigger.
1848
+ const items = commas(comps);
1849
+ if (!items) return null;
1850
+ if (items.length > 1) {
1851
+ const columns = [];
1852
+ for (const item of items) {
1853
+ const parsed = timelineTriggerItem(item, defaults);
1854
+ if (!parsed) return null;
1855
+ parsed.forEach(([name, value], index) => { if (!columns[index]) columns[index] = [name, []]; columns[index][1].push(value); });
1856
+ }
1857
+ return columns.map(([name, values]) => [name, values.join(', ')]);
1858
+ }
1859
+ return timelineTriggerItem(comps, defaults);
1860
+ }
1861
+ function timelineTriggerItem(comps, defaults) {
1862
+ // The name and the source may each be left out (`--t / entry`, `/ entry`).
1863
+ let at = 0, name = 'none', source = 'auto';
1864
+ if (comps[at] && dashedIdent(comps[at]) !== null) name = dashedIdent(comps[at++]);
1865
+ const sourceOf = (c) => functionNamed(c, ['view', 'scroll']) ?? keyword(c, ['auto', 'none']);
1866
+ if (comps[at] && sourceOf(comps[at]) !== null) source = sourceOf(comps[at++]);
1867
+ const more = defaults().slice(1);
1868
+ const rest = comps.slice(at);
1869
+ if (rest.length) {
1870
+ const parts = slashes(rest);
1871
+ // An empty activation range before the slash keeps its default.
1872
+ if (!(parts[0].length === 0 && parts.length === 2)) {
1873
+ const activation = rangePair(parts[0], 'timeline-trigger-activation-range-start', 'timeline-trigger-activation-range-end', 'normal');
1874
+ if (!activation) return null;
1875
+ more[0][1] = activation[0][1]; more[1][1] = activation[1][1];
1876
+ }
1877
+ if (parts[1]) {
1878
+ const active = rangePair(parts[1], 'timeline-trigger-active-range-start', 'timeline-trigger-active-range-end', 'auto');
1879
+ if (!active) return null;
1880
+ more[2][1] = active[0][1]; more[3][1] = active[1][1];
1881
+ }
1882
+ if (parts.length > 2) return null;
1883
+ }
1884
+ return [['timeline-trigger-name', name], ['timeline-trigger-source', source], ...more];
1885
+ }
1886
+
1887
+ const BORDER_SIDE = ['medium', 'none', 'currentcolor'];
1888
+ const GRAMMARS = {
1889
+ margin: (c) => named(['margin-top', 'margin-right', 'margin-bottom', 'margin-left'], box4(c, marginV)),
1890
+ padding: (c) => named(['padding-top', 'padding-right', 'padding-bottom', 'padding-left'], box4(c, paddingV)),
1891
+ inset: (c) => named(['top', 'right', 'bottom', 'left'], box4(c, marginV)),
1892
+ 'scroll-margin': (c) => named(['scroll-margin-top', 'scroll-margin-right', 'scroll-margin-bottom', 'scroll-margin-left'], box4(c, scrollMarginV)),
1893
+ 'scroll-padding': (c) => named(['scroll-padding-top', 'scroll-padding-right', 'scroll-padding-bottom', 'scroll-padding-left'], box4(c, scrollPaddingV)),
1894
+ 'border-width': (c) => named(['border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width'], box4(c, lineWidth)),
1895
+ 'border-style': (c) => named(['border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style'], box4(c, lineStyle)),
1896
+ 'border-color': (c) => named(['border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color'], box4(c, color)),
1897
+ 'corner-shape': (c) => named(['corner-top-left-shape', 'corner-top-right-shape', 'corner-bottom-right-shape', 'corner-bottom-left-shape'], box4(c, cornerShape)),
1898
+ 'border-radius': (c) => borderRadius(c, false),
1899
+ '-webkit-border-radius': (c) => borderRadius(c, true),
1900
+ 'margin-block': (c) => named(['margin-block-start', 'margin-block-end'], pair(c, marginV)),
1901
+ 'margin-inline': (c) => named(['margin-inline-start', 'margin-inline-end'], pair(c, marginV)),
1902
+ 'padding-block': (c) => named(['padding-block-start', 'padding-block-end'], pair(c, paddingV)),
1903
+ 'padding-inline': (c) => named(['padding-inline-start', 'padding-inline-end'], pair(c, paddingV)),
1904
+ 'inset-block': (c) => named(['inset-block-start', 'inset-block-end'], pair(c, marginV)),
1905
+ 'inset-inline': (c) => named(['inset-inline-start', 'inset-inline-end'], pair(c, marginV)),
1906
+ 'scroll-margin-block': (c) => named(['scroll-margin-block-start', 'scroll-margin-block-end'], pair(c, scrollMarginV)),
1907
+ 'scroll-margin-inline': (c) => named(['scroll-margin-inline-start', 'scroll-margin-inline-end'], pair(c, scrollMarginV)),
1908
+ 'scroll-padding-block': (c) => named(['scroll-padding-block-start', 'scroll-padding-block-end'], pair(c, scrollPaddingV)),
1909
+ 'scroll-padding-inline': (c) => named(['scroll-padding-inline-start', 'scroll-padding-inline-end'], pair(c, scrollPaddingV)),
1910
+ 'border-block-width': (c) => named(['border-block-start-width', 'border-block-end-width'], pair(c, lineWidth)),
1911
+ 'border-block-style': (c) => named(['border-block-start-style', 'border-block-end-style'], pair(c, lineStyle)),
1912
+ 'border-block-color': (c) => named(['border-block-start-color', 'border-block-end-color'], pair(c, color)),
1913
+ 'border-inline-width': (c) => named(['border-inline-start-width', 'border-inline-end-width'], pair(c, lineWidth)),
1914
+ 'border-inline-style': (c) => named(['border-inline-start-style', 'border-inline-end-style'], pair(c, lineStyle)),
1915
+ 'border-inline-color': (c) => named(['border-inline-start-color', 'border-inline-end-color'], pair(c, color)),
1916
+ gap: (c) => named(['row-gap', 'column-gap'], pair(c, gapV)),
1917
+ overflow: (c) => named(['overflow-x', 'overflow-y'], pair(c, overflowV)),
1918
+ 'overscroll-behavior': (c) => named(['overscroll-behavior-x', 'overscroll-behavior-y'], pair(c, overscrollV)),
1919
+ 'border-spacing': (c) => named(['-webkit-border-horizontal-spacing', '-webkit-border-vertical-spacing'], pair(c, spacingV)),
1920
+ 'interest-delay': (c) => named(['interest-delay-start', 'interest-delay-end'], pair(c, delayV)),
1921
+ 'corner-top-shape': (c) => named(['corner-top-left-shape', 'corner-top-right-shape'], pair(c, cornerShape)),
1922
+ 'corner-right-shape': (c) => named(['corner-top-right-shape', 'corner-bottom-right-shape'], pair(c, cornerShape)),
1923
+ 'corner-bottom-shape': (c) => named(['corner-bottom-left-shape', 'corner-bottom-right-shape'], pair(c, cornerShape)),
1924
+ 'corner-left-shape': (c) => named(['corner-top-left-shape', 'corner-bottom-left-shape'], pair(c, cornerShape)),
1925
+ 'corner-block-start-shape': (c) => named(['corner-start-start-shape', 'corner-start-end-shape'], pair(c, cornerShape)),
1926
+ 'corner-block-end-shape': (c) => named(['corner-end-start-shape', 'corner-end-end-shape'], pair(c, cornerShape)),
1927
+ 'corner-inline-start-shape': (c) => named(['corner-start-start-shape', 'corner-end-start-shape'], pair(c, cornerShape)),
1928
+ 'corner-inline-end-shape': (c) => named(['corner-start-end-shape', 'corner-end-end-shape'], pair(c, cornerShape)),
1929
+ 'column-rule-inset-cap': (c) => named(['column-rule-inset-cap-start', 'column-rule-inset-cap-end'], pair(c, ruleInsetV)),
1930
+ 'column-rule-inset-junction': (c) => named(['column-rule-inset-junction-start', 'column-rule-inset-junction-end'], pair(c, ruleInsetV)),
1931
+ 'row-rule-inset-cap': (c) => named(['row-rule-inset-cap-start', 'row-rule-inset-cap-end'], pair(c, ruleInsetV)),
1932
+ 'row-rule-inset-junction': (c) => named(['row-rule-inset-junction-start', 'row-rule-inset-junction-end'], pair(c, ruleInsetV)),
1933
+ 'column-rule-inset-start': (c) => same(['column-rule-inset-cap-start', 'column-rule-inset-junction-start'], single(c, ruleInsetV)),
1934
+ 'column-rule-inset-end': (c) => same(['column-rule-inset-cap-end', 'column-rule-inset-junction-end'], single(c, ruleInsetV)),
1935
+ 'row-rule-inset-start': (c) => same(['row-rule-inset-cap-start', 'row-rule-inset-junction-start'], single(c, ruleInsetV)),
1936
+ 'row-rule-inset-end': (c) => same(['row-rule-inset-cap-end', 'row-rule-inset-junction-end'], single(c, ruleInsetV)),
1937
+ 'column-rule-inset': (c) => ruleInset(c, 'column'),
1938
+ 'row-rule-inset': (c) => ruleInset(c, 'row'),
1939
+ 'rule-inset': (c) => { const a = ruleInset(c, 'column'), b = ruleInset(c, 'row'); return a && b && [...a, ...b]; },
1940
+ 'rule-inset-cap': (c) => { const p = pair(c, ruleInsetV); return p && [['row-rule-inset-cap-start', p[0]], ['row-rule-inset-cap-end', p[1]], ['column-rule-inset-cap-start', p[0]], ['column-rule-inset-cap-end', p[1]]]; },
1941
+ 'rule-inset-junction': (c) => { const p = pair(c, ruleInsetV); return p && [['row-rule-inset-junction-start', p[0]], ['row-rule-inset-junction-end', p[1]], ['column-rule-inset-junction-start', p[0]], ['column-rule-inset-junction-end', p[1]]]; },
1942
+ 'rule-inset-start': (c) => same(['column-rule-inset-cap-start', 'column-rule-inset-junction-start', 'row-rule-inset-cap-start', 'row-rule-inset-junction-start'], single(c, ruleInsetV)),
1943
+ 'rule-inset-end': (c) => same(['column-rule-inset-cap-end', 'column-rule-inset-junction-end', 'row-rule-inset-cap-end', 'row-rule-inset-junction-end'], single(c, ruleInsetV)),
1944
+ 'rule-break': (c) => same(['column-rule-break', 'row-rule-break'], single(c, ruleBreakV)),
1945
+ 'rule-color': (c) => same(['column-rule-color', 'row-rule-color'], single(c, color)),
1946
+ 'rule-style': (c) => same(['column-rule-style', 'row-rule-style'], single(c, lineStyle)),
1947
+ 'rule-width': (c) => same(['column-rule-width', 'row-rule-width'], single(c, lineWidth)),
1948
+ 'rule-visibility-items': (c) => same(['column-rule-visibility-items', 'row-rule-visibility-items'], single(c, ruleVisibilityV)),
1949
+ marker: (c) => same(['marker-start', 'marker-mid', 'marker-end'], single(c, markerV)),
1950
+ 'contain-intrinsic-size': containIntrinsicSize,
1951
+ 'place-content': placeContent,
1952
+ 'place-items': placeItems,
1953
+ 'place-self': placeSelf,
1954
+ 'background-position': (c) => { const p = positions(c, true); return p && [['background-position-x', p[0]], ['background-position-y', p[1]]]; },
1955
+ 'mask-position': (c) => { const p = positions(c, false); return p && [['-webkit-mask-position-x', p[0]], ['-webkit-mask-position-y', p[1]]]; },
1956
+ // `-webkit-mask-position` keeps the background syntax's three-value form.
1957
+ '-webkit-mask-position': (c) => { const p = positions(c, true); return p && [['-webkit-mask-position-x', p[0]], ['-webkit-mask-position-y', p[1]]]; },
1958
+ 'animation-range': (c) => rangePair(c, 'animation-range-start', 'animation-range-end', 'normal'),
1959
+ 'timeline-trigger-activation-range': (c) => rangePair(c, 'timeline-trigger-activation-range-start', 'timeline-trigger-activation-range-end', 'normal'),
1960
+ 'timeline-trigger-active-range': (c) => rangePair(c, 'timeline-trigger-active-range-start', 'timeline-trigger-active-range-end', 'auto'),
1961
+ 'border-top': (c) => side(c, 'border-top', BORDER_SIDE),
1962
+ 'border-right': (c) => side(c, 'border-right', BORDER_SIDE),
1963
+ 'border-bottom': (c) => side(c, 'border-bottom', BORDER_SIDE),
1964
+ 'border-left': (c) => side(c, 'border-left', BORDER_SIDE),
1965
+ 'border-block-start': (c) => side(c, 'border-block-start', [INITIAL, INITIAL, INITIAL]),
1966
+ 'border-block-end': (c) => side(c, 'border-block-end', [INITIAL, INITIAL, INITIAL]),
1967
+ 'border-inline-start': (c) => side(c, 'border-inline-start', [INITIAL, INITIAL, INITIAL]),
1968
+ 'border-inline-end': (c) => side(c, 'border-inline-end', [INITIAL, INITIAL, INITIAL]),
1969
+ 'column-rule': (c) => side(c, 'column-rule', BORDER_SIDE),
1970
+ 'row-rule': (c) => side(c, 'row-rule', BORDER_SIDE),
1971
+ 'border-block': (c) => bothSides(c, 'border-block'),
1972
+ 'border-inline': (c) => bothSides(c, 'border-inline'),
1973
+ rule: (c) => { const p = line(c, BORDER_SIDE); return p && [['column-rule-width', p[0]], ['column-rule-style', p[1]], ['column-rule-color', p[2]], ['row-rule-width', p[0]], ['row-rule-style', p[1]], ['row-rule-color', p[2]]]; },
1974
+ border: (c) => {
1975
+ const p = line(c, BORDER_SIDE);
1976
+ if (!p) return null;
1977
+ const out = [];
1978
+ ['width', 'style', 'color'].forEach((part, index) => { for (const s of ['top', 'right', 'bottom', 'left']) out.push([`border-${s}-${part}`, p[index]]); });
1979
+ ['border-image-source', 'border-image-slice', 'border-image-width', 'border-image-outset', 'border-image-repeat'].forEach((name, index) => out.push([name, BORDER_IMAGE_INITIAL[index]]));
1980
+ return out;
1981
+ },
1982
+ outline: (c) => { const p = anyOrder(c, [one(color), one(outlineStyle), one(lineWidth)]); return p && namesOrInitial(['outline-color', 'outline-style', 'outline-width'], p); },
1983
+ '-webkit-text-stroke': (c) => { const p = anyOrder(c, [one(lineWidth), one(color)]); return p && namesOrInitial(['-webkit-text-stroke-width', '-webkit-text-stroke-color'], p); },
1984
+ 'text-decoration': textDecoration,
1985
+ 'text-emphasis': textEmphasis,
1986
+ 'flex-flow': (c) => { const p = anyOrder(c, [one((x) => keyword(x, ['row', 'row-reverse', 'column', 'column-reverse'])), one((x) => keyword(x, ['nowrap', 'wrap', 'wrap-reverse']))]); return p && [['flex-direction', p[0] ?? 'row'], ['flex-wrap', p[1] ?? 'nowrap']]; },
1987
+ 'list-style': listStyle,
1988
+ columns,
1989
+ container,
1990
+ 'text-wrap': (c) => { const p = anyOrder(c, [one((x) => keyword(x, ['wrap', 'nowrap'])), one((x) => keyword(x, ['auto', 'balance', 'stable', 'pretty', 'avoid-orphans']))]); return p && namesOrInitial(['text-wrap-mode', 'text-wrap-style'], p); },
1991
+ 'white-space': whiteSpace,
1992
+ 'font-synthesis': fontSynthesis,
1993
+ 'text-box': textBox,
1994
+ 'position-try': positionTry,
1995
+ 'scroll-timeline': scrollTimeline,
1996
+ 'view-timeline': viewTimeline,
1997
+ 'font-variant': fontVariant,
1998
+ flex,
1999
+ font,
2000
+ 'grid-area': gridArea,
2001
+ 'grid-row': (c) => gridLines(c, 'grid-row-start', 'grid-row-end'),
2002
+ 'grid-column': (c) => gridLines(c, 'grid-column-start', 'grid-column-end'),
2003
+ 'grid-template': (c) => { const t = gridTemplate(c); return t && [['grid-template-rows', t[0]], ['grid-template-columns', t[1]], ['grid-template-areas', t[2]]]; },
2004
+ grid,
2005
+ 'border-image': (c) => { const p = borderImage(c, false); return p && ['border-image-source', 'border-image-slice', 'border-image-width', 'border-image-outset', 'border-image-repeat'].map((name, index) => [name, p[index] ?? BORDER_IMAGE_INITIAL[index]]); },
2006
+ '-webkit-mask-box-image': (c) => { const p = borderImage(c, true); return p && namesOrInitial(['-webkit-mask-box-image-source', '-webkit-mask-box-image-slice', '-webkit-mask-box-image-width', '-webkit-mask-box-image-outset', '-webkit-mask-box-image-repeat'], p); },
2007
+ offset,
2008
+ background,
2009
+ mask: maskShorthand,
2010
+ transition,
2011
+ animation,
2012
+ 'timeline-trigger': timelineTrigger,
2013
+ };
2014
+ function bothSides(comps, name) {
2015
+ const p = line(comps, BORDER_SIDE);
2016
+ return p && [[`${name}-start-width`, p[0]], [`${name}-end-width`, p[0]], [`${name}-start-style`, p[1]], [`${name}-end-style`, p[1]], [`${name}-start-color`, p[2]], [`${name}-end-color`, p[2]]];
2017
+ }
2018
+
2019
+ // -------------------------------------------------------------- serializers ----
2020
+ /** Side values collapsed the `margin` way. */
2021
+ function boxText([top, right, bottom, left]) {
2022
+ if (left !== right) return `${top} ${right} ${bottom} ${left}`;
2023
+ if (bottom !== top) return `${top} ${right} ${bottom}`;
2024
+ if (right !== top) return `${top} ${right}`;
2025
+ return top;
2026
+ }
2027
+ const pairText = (a, b) => (a === b ? a : `${a} ${b}`);
2028
+ const allSame = (values) => (values.every((v) => v === values[0]) ? values[0] : '');
2029
+ const joined = (parts) => parts.filter((part) => part !== '').join(' ');
2030
+ /** A comma-separated list's items (commas inside functions do not split). */
2031
+ function layers(value) {
2032
+ const out = [];
2033
+ let depth = 0, start = 0;
2034
+ for (let index = 0; index < value.length; index++) {
2035
+ const c = value[index];
2036
+ if (c === '(' || c === '[') depth++;
2037
+ else if (c === ')' || c === ']') depth = Math.max(0, depth - 1);
2038
+ else if (c === ',' && depth === 0) { out.push(value.slice(start, index).trim()); start = index + 1; }
2039
+ }
2040
+ out.push(value.slice(start).trim());
2041
+ return out;
2042
+ }
2043
+ const omitIf = (value, initial) => (value === INITIAL || value === initial ? '' : value);
2044
+ function lineText(width, style, colorValue) {
2045
+ return joined([omitIf(width, 'medium'), omitIf(style, 'none'), omitIf(colorValue, 'currentcolor')]);
2046
+ }
2047
+ function ruleInsetText(v, kind) {
2048
+ const [cs, ce, js, je] = ['cap-start', 'cap-end', 'junction-start', 'junction-end'].map((part) => v(`${kind}-rule-inset-${part}`));
2049
+ if ([ce, js, je].every((value) => value === cs)) return cs;
2050
+ return `${cs} ${ce} / ${js} ${je}`;
2051
+ }
2052
+ function rangeText(start, end, initial) {
2053
+ const name = start.split(' ')[0];
2054
+ const fallback = RANGE_NAMES.includes(name) ? name : initial === 'auto' && start === 'normal' ? 'auto' : initial;
2055
+ // An active range's `auto` end is left out too.
2056
+ return end === fallback || (initial === 'auto' && end === 'auto') ? start : `${start} ${end}`;
2057
+ }
2058
+ /** `rangeText` per item of comma lists (a timeline trigger list). */
2059
+ function rangesText(start, end, initial) {
2060
+ const starts = layers(start), ends = layers(end);
2061
+ return starts.length === ends.length ? starts.map((s, index) => rangeText(s, ends[index], initial)).join(', ') : '';
2062
+ }
2063
+ /** A trigger range's parts that differ from `initial` (the end only when it is not its start's default). */
2064
+ function triggerParts(start, end, initial) {
2065
+ const name = start.split(' ')[0], parts = [];
2066
+ if (start !== initial) parts.push(start);
2067
+ if (end !== initial && end !== name) parts.push(end);
2068
+ return parts.join(' ');
2069
+ }
2070
+ /** One timeline trigger: each part that differs from its initial value; the active range follows a slash when its start is set, even after nothing. */
2071
+ function triggerText(name, source, activation, active) {
2072
+ let text = joined([name === 'none' ? '' : name, source === 'auto' ? '' : source, triggerParts(activation[0], activation[1], 'normal')]);
2073
+ if (active[0] && active[0] !== 'auto') text = `${text} / ${triggerParts(active[0], active[1], 'auto')}`;
2074
+ else if (active[1] && active[1] !== 'auto') text = joined([text, active[1]]);
2075
+ return text || 'none';
2076
+ }
2077
+ const STRETCH_KEYWORDS = { '50%': 'ultra-condensed', '62.5%': 'extra-condensed', '75%': 'condensed', '87.5%': 'semi-condensed', '100%': 'normal', '112.5%': 'semi-expanded', '125%': 'expanded', '150%': 'extra-expanded', '200%': 'ultra-expanded' };
2078
+ function fontText(v) {
2079
+ for (const [name, initial] of [['font-variant-ligatures', 'normal'], ['font-variant-numeric', 'normal'], ['font-variant-east-asian', 'normal'], ['font-variant-alternates', 'normal'], ['font-variant-position', 'normal'], ['font-variant-emoji', 'normal'], ['font-size-adjust', 'none'], ['font-kerning', 'auto'], ['font-optical-sizing', 'auto'], ['font-feature-settings', 'normal'], ['font-variation-settings', 'normal'], ['font-language-override', 'normal']]) {
2080
+ if (v(name) !== initial) return '';
2081
+ }
2082
+ const caps = v('font-variant-caps');
2083
+ if (caps !== 'normal' && caps !== 'small-caps') return '';
2084
+ let stretch = v('font-stretch');
2085
+ if (stretch.endsWith('%')) { stretch = STRETCH_KEYWORDS[stretch]; if (!stretch) return ''; }
2086
+ const style = v('font-style'), weight = v('font-weight'), lineHeight = v('line-height');
2087
+ const size = lineHeight === 'normal' ? v('font-size') : `${v('font-size')} / ${lineHeight}`;
2088
+ return joined([style === 'normal' ? '' : style, caps === 'normal' ? '' : caps, weight === 'normal' ? '' : weight, stretch === 'normal' ? '' : stretch, size, v('font-family')]);
2089
+ }
2090
+ function tracks(rows) {
2091
+ const out = [];
2092
+ let current = '', depth = 0;
2093
+ for (const c of rows) {
2094
+ if (c === '[' || c === '(') { depth++; current += c; }
2095
+ else if (c === ']' || c === ')') { depth--; current += c; }
2096
+ else if (c === ' ' && depth === 0) { if (current) { out.push(current); current = ''; } }
2097
+ else current += c;
2098
+ }
2099
+ if (current) out.push(current);
2100
+ return out;
2101
+ }
2102
+ function quotedStrings(text) {
2103
+ const out = [];
2104
+ let current = null, escaped = false;
2105
+ for (const c of text) {
2106
+ if (current === null) { if (c === '"') current = '"'; continue; }
2107
+ current += c;
2108
+ if (escaped) escaped = false;
2109
+ else if (c === '\\') escaped = true;
2110
+ else if (c === '"') { out.push(current); current = null; }
2111
+ }
2112
+ return out;
2113
+ }
2114
+ function gridTemplateText(rows, cols, areas) {
2115
+ if (areas === 'none') return rows === 'none' && cols === 'none' ? 'none' : `${rows} / ${cols}`;
2116
+ const strings = quotedStrings(areas);
2117
+ const rowTracks = tracks(rows);
2118
+ if (rowTracks.filter((t) => !t.startsWith('[')).length !== strings.length) return '';
2119
+ const parts = [];
2120
+ let index = 0;
2121
+ for (const track of rowTracks) {
2122
+ if (track.startsWith('[')) parts.push(track);
2123
+ else { parts.push(strings[index++]); if (track !== 'auto') parts.push(track); }
2124
+ }
2125
+ const text = parts.join(' ');
2126
+ return cols === 'none' ? text : `${text} / ${cols}`;
2127
+ }
2128
+ function backgroundText(v) {
2129
+ const names = ['background-image', 'background-position-x', 'background-position-y', 'background-size', 'background-repeat', 'background-attachment', 'background-origin', 'background-clip'];
2130
+ const columns = names.map((name) => layers(v(name)));
2131
+ // As many layers as the longest list; a shorter list leaves its value out of the later
2132
+ // layers, and an empty layer past the images is dropped.
2133
+ const count = Math.max(...columns.map((c) => c.length));
2134
+ const colorValue = v('background-color');
2135
+ const out = [];
2136
+ for (let index = 0; index < count; index++) {
2137
+ const item = (column) => { const value = columns[column][index]; return value === undefined || value === INITIAL ? '' : value; };
2138
+ const x = item(1), y = item(2), size = item(3);
2139
+ // Blink writes the position's parts it has; a size needs a position, and with neither
2140
+ // part set that is `0% 0%` (` 0% 0%` when it starts the layer).
2141
+ let pos = joined([x, y]);
2142
+ if (size) { if (!pos) pos = item(0) ? '0% 0%' : ' 0% 0%'; pos += ` / ${size}`; }
2143
+ const last = index + 1 === count;
2144
+ const text = joined([item(0), pos, item(4), item(5), item(6), item(7), last && colorValue !== INITIAL ? colorValue : '']);
2145
+ if (text) out.push(text);
2146
+ else if (index < columns[0].length) out.push('none');
2147
+ }
2148
+ return out.join(', ');
2149
+ }
2150
+ function maskText(v) {
2151
+ const names = [['mask-image', 'none'], ['-webkit-mask-position-x', '0%'], ['-webkit-mask-position-y', '0%'], ['mask-size', 'auto'], ['mask-repeat', 'repeat'], ['mask-origin', 'border-box'], ['mask-clip', 'border-box'], ['mask-composite', 'add'], ['mask-mode', 'match-source']];
2152
+ const columns = names.map(([name]) => layers(v(name)));
2153
+ // As many layers as the longest list; a shorter list leaves its value out of the later layers.
2154
+ const count = Math.max(...columns.map((c) => c.length));
2155
+ const out = [];
2156
+ for (let index = 0; index < count; index++) {
2157
+ const raw = (column) => columns[column][index] ?? INITIAL;
2158
+ const item = (column) => { const value = raw(column); return value === names[column][1] || value === INITIAL ? '' : value; };
2159
+ const part = (column, initial) => (raw(column) === INITIAL ? initial : raw(column));
2160
+ const size = item(3), x = part(1, '0%'), y = part(2, '0%');
2161
+ // Blink's position before a size starts its layer with a space.
2162
+ const pos = !item(1) && !item(2) && !size ? '' : !item(1) && !item(2) && !item(0) ? ` ${x} ${y} / ${size}` : size ? `${x} ${y} / ${size}` : `${x} ${y}`;
2163
+ const origin = part(5, 'border-box'), clip = part(6, 'border-box');
2164
+ // `no-clip` is a clip-only keyword: it reads back without the default origin.
2165
+ const boxes = origin === 'border-box' && clip === 'border-box' ? '' : origin === clip ? origin : origin === 'border-box' && clip === 'no-clip' ? clip : `${origin} ${clip}`;
2166
+ out.push(joined([item(0), pos, item(4), boxes, item(7), item(8)]) || 'none');
2167
+ }
2168
+ return out.join(', ');
2169
+ }
2170
+ function transitionText(v) {
2171
+ const properties = layers(v('transition-property')), durations = layers(v('transition-duration')), timings = layers(v('transition-timing-function')), delays = layers(v('transition-delay')), behaviors = layers(v('transition-behavior'));
2172
+ const zero = (t) => t === '0s' || t === '0ms';
2173
+ // As many layers as the longest list; a shorter list leaves its value out.
2174
+ const count = Math.max(properties.length, durations.length, timings.length, delays.length, behaviors.length);
2175
+ return Array.from({ length: count }, (_, index) => {
2176
+ const property = properties[index] ?? 'all';
2177
+ const duration = durations[index] ?? '0s', timing = timings[index] ?? 'ease', delay = delays[index] ?? '0s', behavior = behaviors[index] ?? 'normal';
2178
+ const parts = [];
2179
+ if (property !== 'all') parts.push(property);
2180
+ if (!zero(duration)) parts.push(duration);
2181
+ if (timing !== 'ease') parts.push(timing);
2182
+ if (!zero(delay)) parts.push(delay);
2183
+ if (behavior !== 'normal') parts.push(behavior);
2184
+ return parts.length ? parts.join(' ') : 'all';
2185
+ }).join(', ');
2186
+ }
2187
+ function animationText(v) {
2188
+ if (v('animation-timeline') !== 'auto' || v('animation-range-start') !== 'normal' || v('animation-range-end') !== 'normal') return '';
2189
+ const names = ['animation-duration', 'animation-timing-function', 'animation-delay', 'animation-iteration-count', 'animation-direction', 'animation-fill-mode', 'animation-play-state', 'animation-name'];
2190
+ const columns = names.map((name) => layers(v(name)));
2191
+ // As many layers as the longest list; a shorter list leaves its value out.
2192
+ const count = Math.max(...columns.map((c) => c.length));
2193
+ const out = [];
2194
+ for (let index = 0; index < count; index++) out.push(columns.map((c) => c[index]).filter((value) => value !== undefined).join(' '));
2195
+ return out.join(', ');
2196
+ }
2197
+
2198
+ const PAIRS = new Set(['contain-intrinsic-size', 'margin-block', 'margin-inline', 'padding-block', 'padding-inline', 'inset-block', 'inset-inline', 'scroll-margin-block', 'scroll-margin-inline', 'scroll-padding-block', 'scroll-padding-inline', 'border-block-width', 'border-block-style', 'border-block-color', 'border-inline-width', 'border-inline-style', 'border-inline-color', 'gap', 'overflow', 'overscroll-behavior', 'border-spacing', 'interest-delay', 'column-rule-inset-cap', 'column-rule-inset-junction', 'row-rule-inset-cap', 'row-rule-inset-junction', 'corner-top-shape', 'corner-right-shape', 'corner-bottom-shape', 'corner-left-shape', 'corner-block-start-shape', 'corner-block-end-shape', 'corner-inline-start-shape', 'corner-inline-end-shape']);
2199
+ const SAME = new Set(['marker', 'rule-break', 'rule-color', 'rule-style', 'rule-width', 'rule-visibility-items', 'column-rule-inset-start', 'column-rule-inset-end', 'row-rule-inset-start', 'row-rule-inset-end', 'rule-inset-start', 'rule-inset-end']);
2200
+ const BOXES = {
2201
+ margin: ['margin-top', 'margin-right', 'margin-bottom', 'margin-left'],
2202
+ padding: ['padding-top', 'padding-right', 'padding-bottom', 'padding-left'],
2203
+ inset: ['top', 'right', 'bottom', 'left'],
2204
+ 'scroll-margin': ['scroll-margin-top', 'scroll-margin-right', 'scroll-margin-bottom', 'scroll-margin-left'],
2205
+ 'scroll-padding': ['scroll-padding-top', 'scroll-padding-right', 'scroll-padding-bottom', 'scroll-padding-left'],
2206
+ 'border-width': ['border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width'],
2207
+ 'border-style': ['border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style'],
2208
+ 'border-color': ['border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color'],
2209
+ 'corner-shape': ['corner-top-left-shape', 'corner-top-right-shape', 'corner-bottom-right-shape', 'corner-bottom-left-shape'],
2210
+ };
2211
+
2212
+ /** Serialize `grammar` from its longhands' values (`texts` in table order). */
2213
+ function serializeValue(grammar, entry, texts) {
2214
+ const v = (name) => { const index = entry.longhands.indexOf(name); return index < 0 ? '' : texts[index]; };
2215
+ if (BOXES[grammar]) return boxText(BOXES[grammar].map(v));
2216
+ if (PAIRS.has(grammar)) return pairText(texts[0], texts[1]);
2217
+ if (SAME.has(grammar)) return allSame(texts);
2218
+ switch (grammar) {
2219
+ case 'border-radius':
2220
+ case '-webkit-border-radius': {
2221
+ const corners = ['border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius'].map(v);
2222
+ const horizontal = [], vertical = [];
2223
+ for (const corner of corners) { const space = corner.indexOf(' '); const h = space < 0 ? corner : corner.slice(0, space); horizontal.push(h); vertical.push(space < 0 ? h : corner.slice(space + 1)); }
2224
+ const h = boxText(horizontal);
2225
+ return horizontal.every((value, index) => value === vertical[index]) ? h : `${h} / ${boxText(vertical)}`;
2226
+ }
2227
+ case 'column-rule-inset': return ruleInsetText(v, 'column');
2228
+ case 'row-rule-inset': return ruleInsetText(v, 'row');
2229
+ case 'rule-inset': { const column = ruleInsetText(v, 'column'); return column === ruleInsetText(v, 'row') ? column : ''; }
2230
+ case 'rule-inset-cap':
2231
+ case 'rule-inset-junction': {
2232
+ const part = grammar === 'rule-inset-cap' ? 'cap' : 'junction';
2233
+ const row = pairText(v(`row-rule-inset-${part}-start`), v(`row-rule-inset-${part}-end`));
2234
+ return row === pairText(v(`column-rule-inset-${part}-start`), v(`column-rule-inset-${part}-end`)) ? row : '';
2235
+ }
2236
+ case 'place-items': return pairText(v('align-items'), v('justify-items'));
2237
+ case 'place-self': return pairText(v('align-self'), v('justify-self'));
2238
+ case 'place-content': return pairText(v('align-content'), v('justify-content'));
2239
+ case 'background-position':
2240
+ case 'mask-position':
2241
+ case '-webkit-mask-position': {
2242
+ const xs = layers(texts[0]), ys = layers(texts[1]);
2243
+ if (xs.length !== ys.length) {
2244
+ // Uneven lists: each layer's set parts; layers with none drop out.
2245
+ const part = (list, index) => (list[index] === undefined || list[index] === INITIAL ? '' : list[index]);
2246
+ return Array.from({ length: Math.max(xs.length, ys.length) }, (_, index) => joined([part(xs, index), part(ys, index)])).filter((layer) => layer).join(', ');
2247
+ }
2248
+ if ([...xs, ...ys].includes(INITIAL)) return '';
2249
+ return xs.map((x, index) => `${x} ${ys[index]}`).join(', ');
2250
+ }
2251
+ case 'animation-range': return rangeText(v('animation-range-start'), v('animation-range-end'), 'normal');
2252
+ case 'timeline-trigger-activation-range': return rangesText(v('timeline-trigger-activation-range-start'), v('timeline-trigger-activation-range-end'), 'normal');
2253
+ case 'timeline-trigger-active-range': return rangesText(v('timeline-trigger-active-range-start'), v('timeline-trigger-active-range-end'), 'auto');
2254
+ case 'border-top': case 'border-right': case 'border-bottom': case 'border-left': case 'column-rule': case 'row-rule':
2255
+ return lineText(v(`${grammar}-width`), v(`${grammar}-style`), v(`${grammar}-color`));
2256
+ // The logical sides leave omitted parts `initial` and keep every other value.
2257
+ case 'border-block-start': case 'border-block-end': case 'border-inline-start': case 'border-inline-end':
2258
+ return joined(['width', 'style', 'color'].map((part) => omitIf(v(`${grammar}-${part}`), null)));
2259
+ case 'border-block': case 'border-inline': case 'rule': {
2260
+ const [start, end] = grammar === 'rule' ? ['column-rule', 'row-rule'] : [`${grammar}-start`, `${grammar}-end`];
2261
+ const a = ['width', 'style', 'color'].map((part) => v(`${start}-${part}`)), b = ['width', 'style', 'color'].map((part) => v(`${end}-${part}`));
2262
+ return a.every((value, index) => value === b[index]) ? lineText(...a) : '';
2263
+ }
2264
+ case 'border': {
2265
+ const image = ['border-image-source', 'border-image-slice', 'border-image-width', 'border-image-outset', 'border-image-repeat'].map(v);
2266
+ if (image.some((value, index) => value !== BORDER_IMAGE_INITIAL[index])) return '';
2267
+ const parts = [];
2268
+ for (const part of ['width', 'style', 'color']) {
2269
+ const sides = ['top', 'right', 'bottom', 'left'].map((s) => v(`border-${s}-${part}`));
2270
+ if (sides.some((value) => value !== sides[0])) return '';
2271
+ parts.push(sides[0]);
2272
+ }
2273
+ return lineText(...parts);
2274
+ }
2275
+ case 'outline': return joined(['outline-color', 'outline-style', 'outline-width'].map((name) => omitIf(v(name), null)));
2276
+ case '-webkit-text-stroke': return joined(['-webkit-text-stroke-width', '-webkit-text-stroke-color'].map((name) => omitIf(v(name), null)));
2277
+ case 'text-decoration': {
2278
+ const lineValue = v('text-decoration-line');
2279
+ const text = joined([omitIf(lineValue, 'none'), omitIf(v('text-decoration-thickness'), 'auto'), omitIf(v('text-decoration-style'), 'solid'), omitIf(v('text-decoration-color'), 'currentcolor')]);
2280
+ return text || (lineValue === INITIAL ? 'none' : lineValue);
2281
+ }
2282
+ case 'text-emphasis': return joined([omitIf(v('text-emphasis-style'), 'none'), omitIf(v('text-emphasis-color'), 'currentcolor')]) || 'none';
2283
+ case 'flex-flow': {
2284
+ const direction = v('flex-direction'), wrap = v('flex-wrap');
2285
+ return wrap === 'nowrap' ? direction : direction === 'row' ? wrap : `${direction} ${wrap}`;
2286
+ }
2287
+ case 'list-style': return joined(['list-style-position', 'list-style-image', 'list-style-type'].map((name) => omitIf(v(name), null)));
2288
+ case 'columns': {
2289
+ // Chrome 156 leaves `column-wrap` out of the spelling, whatever its value.
2290
+ const width = v('column-width'), count = v('column-count'), height = v('column-height');
2291
+ const main = width === 'auto' && count === 'auto' ? 'auto' : width === 'auto' ? count : count === 'auto' ? width : `${width} ${count}`;
2292
+ return height === 'auto' ? main : `${main} / ${height}`;
2293
+ }
2294
+ case 'container': { const kind = v('container-type'); return kind === 'normal' ? v('container-name') : `${v('container-name')} / ${kind}`; }
2295
+ case 'text-wrap': {
2296
+ const mode = v('text-wrap-mode') === INITIAL ? 'wrap' : v('text-wrap-mode');
2297
+ const style = v('text-wrap-style') === INITIAL ? 'auto' : v('text-wrap-style');
2298
+ return style === 'auto' ? mode : mode === 'wrap' ? style : `${mode} ${style}`;
2299
+ }
2300
+ case 'white-space': {
2301
+ const collapse = v('white-space-collapse') === INITIAL ? 'collapse' : v('white-space-collapse');
2302
+ const mode = v('text-wrap-mode') === INITIAL ? 'wrap' : v('text-wrap-mode');
2303
+ return { 'collapse wrap': 'normal', 'preserve nowrap': 'pre', 'collapse nowrap': 'nowrap', 'preserve wrap': 'pre-wrap', 'preserve-breaks wrap': 'pre-line', 'break-spaces wrap': 'break-spaces' }[`${collapse} ${mode}`] ?? `${collapse} ${mode}`;
2304
+ }
2305
+ case 'font-synthesis': {
2306
+ const parts = [];
2307
+ const values = ['font-synthesis-weight', 'font-synthesis-style', 'font-synthesis-small-caps'].map(v);
2308
+ for (const [index, name] of ['weight', 'style', 'small-caps'].entries()) {
2309
+ if (values[index] === 'auto') parts.push(name);
2310
+ else if (values[index] !== 'none') return '';
2311
+ }
2312
+ return parts.length ? parts.join(' ') : 'none';
2313
+ }
2314
+ case 'text-box': {
2315
+ const trim = v('text-box-trim'), edge = v('text-box-edge');
2316
+ if (trim === 'none' && edge === 'auto') return 'normal';
2317
+ if (trim === 'trim-both') return edge === 'auto' ? 'trim-both' : edge;
2318
+ return edge === 'auto' ? trim : `${trim} ${edge}`;
2319
+ }
2320
+ case 'position-try': return v('position-try-order') === 'normal' ? v('position-try-fallbacks') : `${v('position-try-order')} ${v('position-try-fallbacks')}`;
2321
+ case 'scroll-timeline': {
2322
+ const names = layers(v('scroll-timeline-name')), axes = layers(v('scroll-timeline-axis'));
2323
+ if (names.length !== axes.length) return '';
2324
+ return names.map((name, index) => (axes[index] === 'block' ? name : `${name} ${axes[index]}`)).join(', ');
2325
+ }
2326
+ case 'view-timeline': {
2327
+ const names = layers(v('view-timeline-name')), axes = layers(v('view-timeline-axis')), insets = layers(v('view-timeline-inset'));
2328
+ if (names.length !== axes.length || names.length !== insets.length) return '';
2329
+ return names.map((name, index) => joined([name, axes[index] === 'block' ? '' : axes[index], insets[index] === 'auto' ? '' : insets[index]])).join(', ');
2330
+ }
2331
+ case 'font-variant': {
2332
+ const values = ['font-variant-ligatures', 'font-variant-caps', 'font-variant-alternates', 'font-variant-numeric', 'font-variant-east-asian', 'font-variant-position', 'font-variant-emoji'].map(v);
2333
+ if (values.every((value) => value === 'normal')) return 'normal';
2334
+ if (values[0] === 'none') return values.slice(1).every((value) => value === 'normal') ? 'none' : '';
2335
+ return joined(values.map((value) => (value === 'normal' ? '' : value)));
2336
+ }
2337
+ case 'flex': return `${v('flex-grow')} ${v('flex-shrink')} ${v('flex-basis')}`;
2338
+ case 'font': return fontText(v);
2339
+ case 'grid-area': {
2340
+ const [rs, cs, re, ce] = ['grid-row-start', 'grid-column-start', 'grid-row-end', 'grid-column-end'].map(v);
2341
+ const parts = [rs, cs, re, ce];
2342
+ if (ce === lineDefault(cs)) { parts.pop(); if (re === lineDefault(rs)) { parts.pop(); if (cs === lineDefault(rs)) parts.pop(); } }
2343
+ return parts.join(' / ');
2344
+ }
2345
+ case 'grid-row':
2346
+ case 'grid-column': return texts[1] === lineDefault(texts[0]) ? texts[0] : `${texts[0]} / ${texts[1]}`;
2347
+ case 'grid-template': return gridTemplateText(v('grid-template-rows'), v('grid-template-columns'), v('grid-template-areas'));
2348
+ case 'grid': {
2349
+ const rows = v('grid-template-rows'), cols = v('grid-template-columns'), areas = v('grid-template-areas'), flow = v('grid-auto-flow'), autoCols = v('grid-auto-columns'), autoRows = v('grid-auto-rows');
2350
+ if (flow === 'row' && autoRows === 'auto' && autoCols === 'auto') return gridTemplateText(rows, cols, areas);
2351
+ if (areas !== 'none') return '';
2352
+ const dense = flow.endsWith('dense');
2353
+ if ((flow.startsWith('row') || flow === 'dense') && rows === 'none' && autoCols === 'auto') return `${joined(['auto-flow', dense ? 'dense' : '', autoRows === 'auto' ? '' : autoRows])} / ${cols}`;
2354
+ if (flow.startsWith('column') && cols === 'none' && autoRows === 'auto') return `${rows} / ${joined(['auto-flow', dense ? 'dense' : '', autoCols === 'auto' ? '' : autoCols])}`;
2355
+ return '';
2356
+ }
2357
+ case 'border-image': {
2358
+ const [source, slice, width, outsetValue, repeat] = ['border-image-source', 'border-image-slice', 'border-image-width', 'border-image-outset', 'border-image-repeat'].map(v);
2359
+ if (slice === '100%' && width === '1' && outsetValue === '0' && repeat === 'stretch') return source;
2360
+ return `${source} ${slice} / ${width} / ${outsetValue} ${repeat}`;
2361
+ }
2362
+ case 'offset': {
2363
+ const [pos, path, distance, rotate, anchor] = ['offset-position', 'offset-path', 'offset-distance', 'offset-rotate', 'offset-anchor'].map(v);
2364
+ const moved = distance !== '0px' || rotate !== 'auto';
2365
+ const main = joined([pos === 'normal' ? '' : pos, path === 'none' && !moved ? '' : path, distance === '0px' ? '' : distance, rotate === 'auto' ? '' : rotate]) || 'normal';
2366
+ return anchor === 'auto' ? main : `${main} / ${anchor}`;
2367
+ }
2368
+ case 'background': return backgroundText(v);
2369
+ case 'mask': return maskText(v);
2370
+ case 'transition': return transitionText(v);
2371
+ case 'animation': return animationText(v);
2372
+ case 'timeline-trigger': {
2373
+ const columns = ['timeline-trigger-name', 'timeline-trigger-source', 'timeline-trigger-activation-range-start', 'timeline-trigger-activation-range-end', 'timeline-trigger-active-range-start', 'timeline-trigger-active-range-end'].map((name) => layers(v(name)));
2374
+ // Lists of different lengths have no shorthand spelling.
2375
+ const count = columns[0].length;
2376
+ if (columns.some((column) => column.length !== count)) return '';
2377
+ return Array.from({ length: count }, (_, index) => {
2378
+ const [name, source, as, ae, bs, be] = columns.map((column) => column[index]);
2379
+ return triggerText(name, source, [as, ae], [bs, be]);
2380
+ }).join(', ');
2381
+ }
2382
+ default: return '';
2383
+ }
2384
+ }
2385
+
2386
+ // ------------------------------------------------------------------- table ----
2387
+ const ENTRIES = new Map(SHORTHAND_TABLE.map(([name, canonical, grammar, allowInitial, longhands]) => [name, { name, canonical, grammar, allowInitial, longhands }]));
2388
+ let preferred = null;
2389
+ function preferredEntries() {
2390
+ if (!preferred) {
2391
+ preferred = [...ENTRIES.values()].filter((entry) => entry.name === entry.canonical)
2392
+ .sort((a, b) => b.longhands.length - a.longhands.length || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
2393
+ }
2394
+ return preferred;
2395
+ }
2396
+ const ALIASES = new Map(LONGHAND_ALIASES);
2397
+
2398
+ /** Every shorthand (and shorthand alias) name, sorted. */
2399
+ export const shorthandNames = () => [...ENTRIES.keys()];
2400
+ /** The shorthand's canonical name (`-webkit-flex` -> `flex`), or null for a non-shorthand. */
2401
+ export const canonical = (name) => ENTRIES.get(name)?.canonical ?? null;
2402
+ /** The longhands `name` sets, in Chrome's order; null for a non-shorthand. */
2403
+ export const longhands = (name) => ENTRIES.get(name)?.longhands ?? null;
2404
+ /** The longhand a longhand alias names (`word-wrap` -> `overflow-wrap`), or null. */
2405
+ export const longhandAlias = (name) => ALIASES.get(name) ?? null;
2406
+ export { LONGHAND_ALIASES };
2407
+
2408
+ // --------------------------------------------------------------------- api ----
2409
+ /**
2410
+ * Set shorthand `name` to `value`: `[{ name, value, pending }]` in the order Chrome adds them
2411
+ * (`pending` = [shorthand, text] for a var() value, whose longhand values are ''), or null when
2412
+ * the value is invalid for the shorthand.
2413
+ */
2414
+ export function expand(name, value) {
2415
+ const entry = ENTRIES.get(name);
2416
+ if (!entry) return null;
2417
+ const trimmed = String(value).trim();
2418
+ if (!trimmed) return null;
2419
+ const lower = trimmed.toLowerCase();
2420
+ if (CSS_WIDE.has(lower)) return entry.longhands.map((longhand) => ({ name: longhand, value: lower, pending: null }));
2421
+ if (hasSubstitution(trimmed)) {
2422
+ if (components(trimmed) === null) return null;
2423
+ const text = substitutionText(trimmed);
2424
+ return entry.longhands.map((longhand) => ({ name: longhand, value: '', pending: [entry.canonical, text] }));
2425
+ }
2426
+ const comps = components(trimmed);
2427
+ if (!comps || !comps.length) return null;
2428
+ const grammar = GRAMMARS[entry.grammar];
2429
+ const parsed = grammar ? grammar(comps) : null;
2430
+ if (!parsed) return null;
2431
+ if (parsed.pending) return entry.longhands.map((longhand) => ({ name: longhand, value: '', pending: [entry.canonical, parsed.pending] }));
2432
+ return parsed.map(([longhand, text]) => ({ name: longhand, value: text, pending: null }));
2433
+ }
2434
+
2435
+ /**
2436
+ * getPropertyValue(shorthand) from `declarations` ([{ name, value, important, pending }]):
2437
+ * '' when a longhand is missing, importance is mixed, or the values have no shorthand spelling.
2438
+ */
2439
+ export function shorthandValue(name, declarations) {
2440
+ const entry = ENTRIES.get(name);
2441
+ if (!entry) return '';
2442
+ const values = [];
2443
+ for (const longhand of entry.longhands) {
2444
+ let found = null;
2445
+ for (let index = declarations.length - 1; index >= 0; index--) if (declarations[index].name === longhand) { found = declarations[index]; break; }
2446
+ if (!found) return '';
2447
+ values.push(found);
2448
+ }
2449
+ if (values.some((v) => v.important) && values.some((v) => !v.important)) return '';
2450
+ const first = values[0];
2451
+ const samePending = (a, b) => (a === null || a === undefined ? b === null || b === undefined : !!b && a[0] === b[0] && a[1] === b[1]);
2452
+ if (first.pending || CSS_WIDE.has(first.value)) {
2453
+ if (values.every((v) => v.value === first.value && samePending(first.pending, v.pending))) {
2454
+ if (first.pending) {
2455
+ // A system font keyword leaves the font-variant longhands at `normal`.
2456
+ if (first.pending[0] === 'font' && entry.canonical === 'font-variant' && SYSTEM_FONTS.includes(first.pending[1])) return 'normal';
2457
+ return first.pending[0] === entry.canonical ? first.pending[1] : '';
2458
+ }
2459
+ return first.value;
2460
+ }
2461
+ }
2462
+ for (const v of values) {
2463
+ if (v.pending || (CSS_WIDE.has(v.value) && (v.value !== 'initial' || !entry.allowInitial))) return '';
2464
+ }
2465
+ return serializeValue(entry.grammar, ENTRIES.get(entry.grammar) ?? entry, values.map((v) => v.value));
2466
+ }
2467
+
2468
+ /** cssText: the CSSOM "serialize a CSS declaration block" algorithm with Chrome's shorthand preference. */
2469
+ export function blockText(declarations) {
2470
+ const done = declarations.map(() => false);
2471
+ const indexOf = new Map();
2472
+ declarations.forEach((d, index) => { if (!indexOf.has(d.name)) indexOf.set(d.name, index); });
2473
+ const parts = [];
2474
+ const push = (name, value, important) => parts.push(`${name}: ${value}${important ? ' !important' : ''};`);
2475
+ declarations.forEach((declaration, index) => {
2476
+ if (done[index]) return;
2477
+ for (const entry of preferredEntries()) {
2478
+ if (!entry.longhands.includes(declaration.name)) continue;
2479
+ const positions = entry.longhands.map((longhand) => indexOf.get(longhand));
2480
+ if (positions.some((p) => p === undefined || done[p])) continue;
2481
+ const value = shorthandValue(entry.canonical, declarations);
2482
+ if (!value) continue;
2483
+ push(entry.canonical, value, declaration.important);
2484
+ for (const p of positions) done[p] = true;
2485
+ return;
2486
+ }
2487
+ push(declaration.name, declaration.value, declaration.important);
2488
+ done[index] = true;
2489
+ });
2490
+ return parts.join(' ');
2491
+ }
2492
+
2493
+ /** A declaration list's `[name, value, important]` declarations, in order. */
2494
+ export function declarations(text) {
2495
+ const tokens = tokenize(String(text));
2496
+ const out = [];
2497
+ if (!tokens) return out;
2498
+ // Split at top-level semicolons, keeping each declaration's source span.
2499
+ const source = String(text);
2500
+ let depth = 0, start = 0, quote = '';
2501
+ for (let index = 0; index <= source.length; index++) {
2502
+ const c = source[index];
2503
+ if (index < source.length) {
2504
+ if (quote) { if (c === '\\') index++; else if (c === quote) quote = ''; continue; }
2505
+ if (c === '"' || c === "'") { quote = c; continue; }
2506
+ if (c === '/' && source[index + 1] === '*') { const end = source.indexOf('*/', index + 2); index = end < 0 ? source.length : end + 1; continue; }
2507
+ if (c === '(' || c === '[' || c === '{') { depth++; continue; }
2508
+ if (c === ')' || c === ']' || c === '}') { depth = Math.max(0, depth - 1); continue; }
2509
+ if (c !== ';' || depth > 0) continue;
2510
+ }
2511
+ const part = source.slice(start, index).replace(/\/\*[\s\S]*?\*\//g, '');
2512
+ start = index + 1;
2513
+ const colon = part.indexOf(':');
2514
+ if (colon < 0) continue;
2515
+ const name = part.slice(0, colon).trim();
2516
+ if (!/^-?[A-Za-z_-][A-Za-z0-9_-]*$/.test(name)) continue;
2517
+ let value = part.slice(colon + 1).trim();
2518
+ const important = /!\s*important\s*$/i.exec(value);
2519
+ if (important) value = value.slice(0, important.index).trim();
2520
+ else if (value.includes('!')) continue;
2521
+ out.push([name, value, !!important]);
2522
+ }
2523
+ return out;
2524
+ }
2525
+
2526
+ // ---------------------------------------------------------------- longhands ----
2527
+ // Longhand specified values the way Chrome 156 spells them (miaskiewicz/turbo-dom#97; the port
2528
+ // of rtdom::css_shorthand::longhand_value): the longhands of the shorthands (and a few common
2529
+ // ones) go through their value grammar ({ valid, text } or { valid: false }); any other
2530
+ // longhand gets the context-free spelling ({ valid: null, text }).
2531
+ const listed = (comps, item) => { const items = commas(comps); if (!items) return null; const out = items.map(item); return out.some((v) => v === null) ? null : out.join(', '); };
2532
+ const keywords = (comps, names) => (comps.length === 1 ? keyword(comps[0], names) : null);
2533
+ const oneOf = (ty) => (comps) => single(comps, ty);
2534
+ function radiusPair(comps) {
2535
+ if (comps.length === 1) return radius(comps[0]);
2536
+ if (comps.length !== 2) return null;
2537
+ const a = radius(comps[0]), b = radius(comps[1]);
2538
+ return a === null || b === null ? null : a === b ? a : `${a} ${b}`;
2539
+ }
2540
+ const flexBasis = (comps) => single(comps, (c) => keyword(c, ['auto', 'content', 'min-content', 'max-content', 'fit-content']) ?? lengthPercentage(c, false));
2541
+ const fontSize = (comps) => single(comps, (c) => keyword(c, FONT_SIZES) ?? lengthPercentage(c, false));
2542
+ const fontWeight = (comps) => single(comps, (c) => keyword(c, ['normal', 'bold', 'bolder', 'lighter']) ?? (c.kind === 'number' && c.value >= 1 && c.value <= 1000 ? number(c, false) : null));
2543
+ function fontStyle(comps) {
2544
+ if (comps.length === 1) return keyword(comps[0], ['normal', 'italic', 'oblique']);
2545
+ if (comps.length === 2 && isIdent(comps[0], ['oblique'])) return oblique(comps[1]);
2546
+ return null;
2547
+ }
2548
+ const lineHeight = (comps) => single(comps, (c) => keyword(c, ['normal']) ?? number(c, false) ?? lengthPercentage(c, false));
2549
+ const fontStretch = (comps) => single(comps, (c) => keyword(c, ['normal', ...STRETCHES]) ?? percentage(c, false));
2550
+ function variantGroups(comps, groups, extra) {
2551
+ if (comps.length === 1) { const value = keyword(comps[0], extra); if (value !== null) return value; }
2552
+ const result = groupSet(comps, groups);
2553
+ return result && result[0] === comps.length ? result[1] : null;
2554
+ }
2555
+ const zIndex = (comps) => single(comps, (c) => keyword(c, ['auto']) ?? integer(c, -Infinity));
2556
+ const SIZE_FUNCTIONS = ['anchor-size', 'calc-size'];
2557
+ const sizeV = (comps) => single(comps, (c) => keyword(c, ['auto', 'min-content', 'max-content', 'fit-content', 'stretch', '-webkit-fill-available']) ?? lengthPercentage(c, false) ?? functionNamed(c, SIZE_FUNCTIONS));
2558
+ const maxSizeV = (comps) => single(comps, (c) => keyword(c, ['none', 'min-content', 'max-content', 'fit-content', 'stretch', '-webkit-fill-available']) ?? lengthPercentage(c, false) ?? functionNamed(c, SIZE_FUNCTIONS));
2559
+ const imageList = (comps) => listed(comps, (item) => single(item, (c) => image(c) ?? keyword(c, ['none'])));
2560
+ const repeatList = (comps) => listed(comps, (item) => { const r = repeatStyle(item); return r && r[0] === item.length ? r[1] : null; });
2561
+ const sizeList = (comps) => listed(comps, (item) => { const r = bgSize(item); return r && r[0] === item.length ? r[1] : null; });
2562
+ function axisPosition(sides, center) {
2563
+ return (comps) => listed(comps, (item) => {
2564
+ if (item.length === 1) return keyword(item[0], [sides[0], center, sides[1]]) ?? lengthPercentage(item[0], true);
2565
+ if (item.length === 2 && isIdent(item[0], sides)) { const offset = lengthPercentage(item[1], true); return offset === null ? null : `${ident(item[0])} ${offset}`; }
2566
+ return null;
2567
+ });
2568
+ }
2569
+ const timeList = (comps) => listed(comps, (item) => single(item, (c) => time(c, true)));
2570
+ const durationList = (comps) => listed(comps, (item) => single(item, (c) => time(c, false)));
2571
+ const easingList = (comps) => listed(comps, (item) => single(item, easing));
2572
+ // `none` only stands alone.
2573
+ const transitionPropertyV = (comps) => (comps.length > 1 && comps.some((c) => isIdent(c, ['none'])) ? null : transitionPropertyList(comps));
2574
+ const transitionPropertyList = (comps) => listed(comps, (item) => single(item, (c) => keyword(c, ['none', 'all']) ?? (() => { const v = customIdent(c, ['none', 'initial']); return v === null ? null : v.startsWith('--') ? v : v.toLowerCase(); })()));
2575
+ const rangeOf = (end) => (comps) => { const r = rangeValue(comps, 'normal', end); return r && r[0] === comps.length ? r[1] : null; };
2576
+ function textDecorationLine(comps) {
2577
+ if (comps.length === 1 && isIdent(comps[0], ['none'])) return 'none';
2578
+ const order = ['underline', 'overline', 'line-through', 'blink', 'spelling-error', 'grammar-error'];
2579
+ const seen = [];
2580
+ for (const comp of comps) { const name = keyword(comp, order); if (name === null || seen.includes(name)) return null; seen.push(name); }
2581
+ return seen.length ? order.filter((n) => seen.includes(n)).join(' ') : null;
2582
+ }
2583
+ function gridAreas(comps) {
2584
+ if (comps.length === 1 && isIdent(comps[0], ['none'])) return 'none';
2585
+ return areasText(comps.map(areaRow));
2586
+ }
2587
+ const offsetPath = (comps) => single(comps, (c) => {
2588
+ if (isIdent(c, ['none'])) return 'none';
2589
+ const value = functionNamed(c, ['path', 'ray', 'circle', 'ellipse', 'inset', 'polygon', 'xywh', 'rect', 'shape']) ?? url(c);
2590
+ if (value === null || (value.startsWith('path(') && !pathDataValid(value))) return null;
2591
+ return value.startsWith('path(') ? pathText(value) : value;
2592
+ });
2593
+ // A percentage calc() stays as written.
2594
+ const opacityV = (comps) => single(comps, (c) => number(c, true) ?? (c.kind === 'percentage' ? cssNumber(c.value / 100) : mathOf(c, isLengthUnit, true, false) && c.text.includes('%') ? c.text : null));
2595
+ const DISPLAY_KEYWORDS = ['none', 'contents', 'block', 'inline', 'inline-block', 'flow-root', 'flex', 'inline-flex', 'grid', 'inline-grid', 'table', 'inline-table', 'table-row-group', 'table-header-group', 'table-footer-group', 'table-row', 'table-cell', 'table-column-group', 'table-column', 'table-caption', 'list-item', 'ruby', 'ruby-text', 'math', '-webkit-box', '-webkit-inline-box'];
2596
+ const DISPLAY_PAIRS = { 'block flow': 'block', 'inline flow': 'inline', 'inline flow-root': 'inline-block', 'block flow-root': 'flow-root', 'block flex': 'flex', 'inline flex': 'inline-flex', 'block grid': 'grid', 'inline grid': 'inline-grid', 'block table': 'table', 'inline table': 'inline-table', 'block ruby': 'block ruby', 'inline ruby': 'ruby' };
2597
+ function display(comps) {
2598
+ if (comps.length === 1) return keyword(comps[0], DISPLAY_KEYWORDS);
2599
+ if (comps.length !== 2) return null;
2600
+ const a = ident(comps[0]), b = ident(comps[1]);
2601
+ if (a === null || b === null) return null;
2602
+ return DISPLAY_PAIRS[`${a} ${b}`] ?? DISPLAY_PAIRS[`${b} ${a}`] ?? null;
2603
+ }
2604
+ const animationNames = (comps) => listed(comps, (item) => single(item, (c) => {
2605
+ if (isIdent(c, ['none'])) return 'none';
2606
+ if (c.kind === 'string') return animationNameString(c);
2607
+ return customIdent(c, ['none']);
2608
+ }));
2609
+ /** `box-shadow` / `text-shadow` (`box`: a spread length and `inset`): each shadow as Chrome orders it (color, lengths, `inset`). */
2610
+ function shadow(comps, box) {
2611
+ if (comps.length === 1 && isIdent(comps[0], ['none'])) return 'none';
2612
+ return listed(comps, (item) => {
2613
+ let shadowColor = null, inset = false;
2614
+ const lengths = [];
2615
+ for (const comp of item) {
2616
+ if (box && isIdent(comp, ['inset']) && !inset) { inset = true; continue; }
2617
+ // The blur radius (the third length) is not negative.
2618
+ const negative = comp.kind === 'dimension' && comp.value < 0;
2619
+ const l = lengths.length < (box ? 4 : 3) && !(lengths.length === 2 && negative) ? length(comp, true) : null;
2620
+ if (l !== null) { lengths.push(l); continue; }
2621
+ const c = shadowColor === null ? color(comp) : null;
2622
+ if (c === null) return null;
2623
+ shadowColor = c;
2624
+ }
2625
+ if (lengths.length < 2) return null;
2626
+ return [...(shadowColor === null ? [] : [shadowColor]), ...lengths, ...(inset ? ['inset'] : [])].join(' ');
2627
+ });
2628
+ }
2629
+ const SIDES4 = ['top', 'right', 'bottom', 'left'];
2630
+ const LOGICAL = ['block-start', 'block-end', 'inline-start', 'inline-end'];
2631
+ const LONGHAND_GRAMMARS = new Map();
2632
+ /** A position longhand: one, two or (`four`) four values, and for `transform-origin` (`z`) an optional depth length after two. */
2633
+ function origin(comps, four, z) {
2634
+ const pairOf = (list) => { const p = position(list); return p ? `${p[0]} ${p[1]}` : null; };
2635
+ if (comps.length === 1 || comps.length === 2) return pairOf(comps);
2636
+ if (comps.length === 3 && z) { const p = pairOf(comps.slice(0, 2)), depth = length(comps[2], true); return p === null || depth === null ? null : `${p} ${depth}`; }
2637
+ if (comps.length === 4 && four) return pairOf(comps);
2638
+ return null;
2639
+ }
2640
+ /** One to four values, collapsed the way Chrome writes a box. */
2641
+ function quad(comps, ty) {
2642
+ if (!comps.length || comps.length > 4) return null;
2643
+ const values = comps.map(ty);
2644
+ return values.includes(null) ? null : collapse4(values);
2645
+ }
2646
+ /** `border-image-slice`: up to four numbers / percentages, `fill` (before or after) last. */
2647
+ function imageSlice(comps) {
2648
+ let values = comps, filled = false;
2649
+ if (comps.length && isIdent(comps[0], ['fill'])) { values = comps.slice(1); filled = true; }
2650
+ else if (comps.length && isIdent(comps[comps.length - 1], ['fill'])) { values = comps.slice(0, -1); filled = true; }
2651
+ if (!values.length || values.length > 4) return null;
2652
+ const texts = values.map((c) => number(c, false) ?? percentage(c, false));
2653
+ if (texts.includes(null)) return null;
2654
+ return filled ? `${collapse4(texts)} fill` : collapse4(texts);
2655
+ }
2656
+ const add = (names, parse, strict = true) => { for (const name of names) LONGHAND_GRAMMARS.set(name, [parse, strict]); };
2657
+ add([...SIDES4.map((s) => `margin-${s}`), ...LOGICAL.map((s) => `margin-${s}`), ...SIDES4, ...LOGICAL.map((s) => `inset-${s}`)], oneOf(marginV));
2658
+ add([...SIDES4, ...LOGICAL].map((s) => `padding-${s}`), oneOf(paddingV));
2659
+ add([...SIDES4, ...LOGICAL].map((s) => `scroll-margin-${s}`), oneOf(scrollMarginV));
2660
+ add([...SIDES4, ...LOGICAL].map((s) => `scroll-padding-${s}`), oneOf(scrollPaddingV));
2661
+ add([...[...SIDES4, ...LOGICAL].map((s) => `border-${s}-width`), 'outline-width', 'column-rule-width', 'row-rule-width', '-webkit-text-stroke-width'], oneOf(lineWidth));
2662
+ add([...[...SIDES4, ...LOGICAL].map((s) => `border-${s}-style`), 'column-rule-style', 'row-rule-style'], oneOf(lineStyle));
2663
+ add(['outline-style'], oneOf(outlineStyle));
2664
+ add([...[...SIDES4, ...LOGICAL].map((s) => `border-${s}-color`), 'outline-color', 'column-rule-color', 'row-rule-color', 'text-decoration-color', 'text-emphasis-color', '-webkit-text-stroke-color', 'background-color', 'color'], oneOf(color));
2665
+ add(['row-gap', 'column-gap'], oneOf(gapV));
2666
+ add(['overflow-x', 'overflow-y'], oneOf(overflowV));
2667
+ add(['overscroll-behavior-x', 'overscroll-behavior-y'], oneOf(overscrollV));
2668
+ add(['-webkit-border-horizontal-spacing', '-webkit-border-vertical-spacing'], oneOf(spacingV));
2669
+ add(['interest-delay-start', 'interest-delay-end'], oneOf(delayV));
2670
+ add(['column', 'row'].flatMap((k) => ['cap-start', 'cap-end', 'junction-start', 'junction-end'].map((p) => `${k}-rule-inset-${p}`)), oneOf(ruleInsetV));
2671
+ add(['column-rule-break', 'row-rule-break'], oneOf(ruleBreakV));
2672
+ add(['column-rule-visibility-items', 'row-rule-visibility-items'], oneOf(ruleVisibilityV));
2673
+ add(['marker-start', 'marker-mid', 'marker-end'], oneOf(markerV));
2674
+ add(['top-left', 'top-right', 'bottom-right', 'bottom-left', 'start-start', 'start-end', 'end-start', 'end-end'].map((c) => `corner-${c}-shape`), oneOf(cornerShape));
2675
+ add(['top-left', 'top-right', 'bottom-right', 'bottom-left'].map((c) => `border-${c}-radius`), radiusPair);
2676
+ add(['flex-grow', 'flex-shrink'], (c) => single(c, (x) => number(x, false)));
2677
+ add(['flex-basis'], flexBasis);
2678
+ add(['flex-direction'], (c) => keywords(c, ['row', 'row-reverse', 'column', 'column-reverse']));
2679
+ add(['flex-wrap'], (c) => keywords(c, ['nowrap', 'wrap', 'wrap-reverse']));
2680
+ add(['font-size'], fontSize);
2681
+ add(['font-weight'], fontWeight);
2682
+ add(['font-style'], fontStyle);
2683
+ add(['font-family'], fontFamily);
2684
+ add(['line-height'], lineHeight);
2685
+ add(['font-stretch'], fontStretch);
2686
+ add(['font-variant-caps'], (c) => keywords(c, ['normal', ...CAPS]));
2687
+ add(['font-variant-position'], (c) => keywords(c, ['normal', 'sub', 'super']));
2688
+ add(['font-variant-emoji'], (c) => keywords(c, ['normal', 'text', 'emoji', 'unicode']));
2689
+ add(['font-variant-ligatures'], (c) => variantGroups(c, LIGATURES, ['normal', 'none']));
2690
+ add(['font-variant-numeric'], (c) => variantGroups(c, NUMERIC, ['normal']));
2691
+ add(['font-variant-east-asian'], (c) => variantGroups(c, EAST_ASIAN, ['normal']));
2692
+ add(['font-kerning'], (c) => keywords(c, ['auto', 'normal', 'none']));
2693
+ add(['font-optical-sizing'], (c) => keywords(c, ['auto', 'none']));
2694
+ add(['font-synthesis-weight', 'font-synthesis-style', 'font-synthesis-small-caps'], (c) => keywords(c, ['auto', 'none']));
2695
+ add(['position-try-fallbacks'], (c) => (c.length === 1 && isIdent(c[0], ['none']) ? 'none' : tryFallbacks(c)));
2696
+ add(['position-try-order'], (c) => keywords(c, ['normal', 'most-width', 'most-height', 'most-block-size', 'most-inline-size']));
2697
+ add(['text-decoration-line'], textDecorationLine);
2698
+ add(['text-decoration-style'], (c) => keywords(c, ['solid', 'double', 'dotted', 'dashed', 'wavy']));
2699
+ add(['list-style-position'], (c) => keywords(c, ['inside', 'outside']));
2700
+ add(['list-style-image'], (c) => single(c, (x) => image(x) ?? keyword(x, ['none'])), false);
2701
+ add(['z-index'], zIndex);
2702
+ add(['width', 'height', 'min-width', 'min-height', 'block-size', 'inline-size', 'min-block-size', 'min-inline-size'], sizeV);
2703
+ add(['max-width', 'max-height', 'max-block-size', 'max-inline-size'], maxSizeV);
2704
+ add(['column-count'], (c) => single(c, (x) => keyword(x, ['auto']) ?? integer(x, 1)));
2705
+ add(['column-width', 'column-height'], (c) => single(c, (x) => keyword(x, ['auto']) ?? length(x, false)));
2706
+ add(['column-wrap'], (c) => keywords(c, ['auto', 'nowrap', 'wrap']));
2707
+ add(['grid-row-start', 'grid-row-end', 'grid-column-start', 'grid-column-end'], gridLine);
2708
+ add(['grid-template-columns', 'grid-template-rows'], trackList);
2709
+ add(['transition-duration'], durationList);
2710
+ add(['animation-duration'], (c) => listed(c, (item) => single(item, (x) => time(x, false) ?? keyword(x, ['auto']))));
2711
+ add(['font-variant-alternates'], (c) => { if (c.length === 1 && isIdent(c[0], ['normal'])) return 'normal'; const a = alternates(c); return a && a[0] === c.length ? a[1] : null; });
2712
+ add(['text-align'], (c) => keywords(c, ['start', 'end', 'left', 'right', 'center', 'justify', 'match-parent', '-webkit-left', '-webkit-right', '-webkit-center', '-webkit-auto']));
2713
+ add(['transition-delay', 'animation-delay'], timeList);
2714
+ add(['transition-timing-function', 'animation-timing-function'], easingList, false);
2715
+ add(['transition-property'], transitionPropertyV);
2716
+ add(['background-image', 'mask-image'], imageList, false);
2717
+ add(['background-repeat', 'mask-repeat'], repeatList);
2718
+ add(['background-size', 'mask-size'], sizeList);
2719
+ add(['background-attachment'], (c) => listed(c, (item) => keywords(item, ['scroll', 'fixed', 'local'])));
2720
+ add(['background-origin'], (c) => listed(c, (item) => keywords(item, ['border-box', 'padding-box', 'content-box'])));
2721
+ add(['background-clip'], (c) => listed(c, (item) => keywords(item, ['border-box', 'padding-box', 'content-box', 'text', 'border-area'])));
2722
+ add(['background-position-x', '-webkit-mask-position-x'], axisPosition(['left', 'right'], 'center'));
2723
+ add(['background-position-y', '-webkit-mask-position-y'], axisPosition(['top', 'bottom'], 'center'));
2724
+ add(['animation-range-start'], rangeOf(false));
2725
+ add(['animation-range-end'], rangeOf(true));
2726
+ add(['offset-distance'], (c) => single(c, (x) => lengthPercentage(x, true)));
2727
+ add(['offset-anchor'], (c) => (c.length === 1 && isIdent(c[0], ['auto']) ? 'auto' : origin(c, true, false)));
2728
+ add(['offset-position'], (c) => (c.length === 1 && isIdent(c[0], ['auto', 'normal']) ? ident(c[0]) : origin(c, true, false)));
2729
+ add(['object-position', 'perspective-origin'], (c) => origin(c, true, false));
2730
+ add(['transform-origin'], (c) => origin(c, false, true));
2731
+ add(['border-image-slice'], imageSlice);
2732
+ add(['border-image-width'], (c) => quad(c, (x) => number(x, false) ?? lengthPercentage(x, false) ?? keyword(x, ['auto'])));
2733
+ add(['border-image-outset'], (c) => quad(c, (x) => number(x, false) ?? length(x, false)));
2734
+ add(['justify-items'], (c) => { const j = justifyItems(c); return j && j[0] === c.length ? j[1] : null; });
2735
+ // `none`, or a trigger name and one or two actions per item.
2736
+ add(['animation-trigger'], (c) => (c.length === 1 && isIdent(c[0], ['none']) ? 'none' : listed(c, (item) => {
2737
+ const ACTIONS = ['none', 'play', 'play-once', 'play-forwards', 'play-backwards', 'pause', 'reset', 'replay'];
2738
+ if (item.length < 2 || item.length > 3 || dashedIdent(item[0]) === null) return null;
2739
+ const actions = item.slice(1).map((x) => keyword(x, ACTIONS));
2740
+ return actions.includes(null) ? null : [dashedIdent(item[0]), ...actions].join(' ');
2741
+ })));
2742
+ add(['white-space-collapse'], (c) => keywords(c, ['collapse', 'preserve', 'preserve-breaks', 'break-spaces']));
2743
+ add(['text-wrap-mode'], (c) => keywords(c, ['wrap', 'nowrap']));
2744
+ add(['text-wrap-style'], (c) => keywords(c, ['auto', 'balance', 'stable', 'pretty', 'avoid-orphans']));
2745
+ add(['overflow-wrap'], (c) => keywords(c, ['normal', 'break-word', 'anywhere']));
2746
+ add(['grid-template-areas'], gridAreas);
2747
+ add(['offset-path'], offsetPath);
2748
+ add(['opacity'], opacityV);
2749
+ add(['display'], display, false);
2750
+ add(['animation-name'], animationNames);
2751
+ add(['box-shadow'], (c) => shadow(c, true));
2752
+ add(['text-shadow'], (c) => shadow(c, false));
2753
+ add(['offset-rotate'], (c) => { const r = rotatePart(c); return r && r[0] === c.length ? r[1] : null; });
2754
+
2755
+ // Properties whose identifiers are names (kept as written) rather than keywords.
2756
+ const NAMED = ['animation-name', 'font-family', 'container-name', 'counter-reset', 'counter-increment', 'counter-set', 'grid-area', 'grid-row-start', 'grid-row-end', 'grid-column-start', 'grid-column-end', 'grid-template-areas', 'view-transition-name', 'view-transition-class', 'anchor-name', 'position-anchor', 'anchor-scope', 'timeline-scope', 'font-palette', 'will-change', 'page', 'quotes', 'content'];
2757
+ const NAME_KEYWORDS = ['none', 'auto', 'span', 'normal', 'all', 'match-element'];
2758
+ const ZERO_LENGTH_PARTS = ['margin', 'padding', 'inset', 'width', 'height', 'size', 'gap', 'radius', 'spacing', 'indent', 'offset', 'flex-basis', 'font-size', 'column-width', 'column-height', 'perspective', 'shape-margin', 'position-x', 'position-y', 'thickness'];
2759
+ const zeroIsLength = (name) => SIDES4.includes(name) || ZERO_LENGTH_PARTS.some((part) => name.includes(part));
2760
+ const NAMING_FUNCTIONS = ['stylistic', 'styleset', 'character-variant', 'swash', 'ornaments', 'annotation', 'local', 'format', 'attr', 'counter', 'counters', 'symbols', 'url', 'src', '['];
2761
+ const LENGTH_FUNCTIONS = ['translate', 'translatex', 'translatey', 'translatez', 'translate3d', 'perspective'];
2762
+ /** A function's spelling: keyword arguments lowercase (recursively), a length function's `0` as `0px`. */
2763
+ function functionText(fn, comp) {
2764
+ if (NAMING_FUNCTIONS.includes(fn)) return comp.text;
2765
+ const open = comp.text.search(/[([]/);
2766
+ if (open < 0) return comp.text;
2767
+ const inner = components(comp.text.slice(open + 1, -1));
2768
+ if (!inner) return comp.text;
2769
+ const lengths = LENGTH_FUNCTIONS.includes(fn);
2770
+ const spelled = inner.map((part) => ({ ...part, text: part.kind === 'ident' && !part.value.startsWith('--') ? part.value.toLowerCase() : part.kind === 'number' && lengths && part.value === 0 ? '0px' : part.kind === 'function' ? functionText(part.name, part) : part.text }));
2771
+ return `${comp.text.slice(0, open + 1)}${join(spelled)}${comp.text.slice(-1)}`;
2772
+ }
2773
+ /** The context-free spelling: identifiers lowercase unless they name something, a lone `0` as `0px` where a length is meant. */
2774
+ function genericSpelling(name, comps) {
2775
+ const isNamed = NAMED.includes(name) || name.startsWith('--');
2776
+ return join(comps.map((comp) => {
2777
+ let text = comp.text;
2778
+ if (comp.kind === 'ident' && !comp.value.startsWith('--')) { const lower = comp.value.toLowerCase(); text = isNamed && !NAME_KEYWORDS.includes(lower) ? comp.value : lower; }
2779
+ else if (comp.kind === 'hash') text = color(comp) ?? comp.text;
2780
+ else if (comp.kind === 'function' && !isNamed) text = functionText(comp.name, comp);
2781
+ else if (comp.kind === 'number' && comp.value === 0 && comps.length === 1 && zeroIsLength(name)) text = '0px';
2782
+ return { ...comp, text };
2783
+ }));
2784
+ }
2785
+ /**
2786
+ * The specified value of longhand `name` set to `value`, as Chrome 156 spells it:
2787
+ * { valid: true, text } / { valid: false } from the longhand's grammar, or
2788
+ * { valid: null, text } (the context-free spelling) for a longhand without one.
2789
+ */
2790
+ export function longhandValue(name, value) {
2791
+ const trimmed = String(value).trim();
2792
+ if (!trimmed) return { valid: false };
2793
+ const lower = trimmed.toLowerCase();
2794
+ if (CSS_WIDE.has(lower)) return { valid: true, text: lower };
2795
+ if (hasSubstitution(trimmed)) return { valid: null, text: substitutionText(trimmed) };
2796
+ const comps = components(trimmed);
2797
+ if (!comps || !comps.length) return { valid: false };
2798
+ const grammar = LONGHAND_GRAMMARS.get(name);
2799
+ if (!grammar) return { valid: null, text: genericSpelling(name, comps) };
2800
+ const text = grammar[0](comps);
2801
+ if (text !== null) return { valid: true, text };
2802
+ return grammar[1] ? { valid: false } : { valid: null, text: genericSpelling(name, comps) };
2803
+ }