postcss-calc 11.1.1 → 11.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +40 -18
  2. package/package.json +16 -11
  3. package/src/index.js +8 -5
  4. package/src/lib/analyze.js +228 -0
  5. package/src/lib/block-index.js +118 -0
  6. package/src/lib/calculation-type.js +25 -0
  7. package/src/lib/compile.js +80 -0
  8. package/src/lib/convertUnits.js +15 -1
  9. package/src/lib/functions.js +364 -0
  10. package/src/lib/limits.js +49 -0
  11. package/src/lib/node.js +85 -39
  12. package/src/lib/opaque.js +20 -0
  13. package/src/lib/parser.js +493 -257
  14. package/src/lib/print.js +75 -0
  15. package/src/lib/regex.js +4 -0
  16. package/src/lib/scan.js +61 -0
  17. package/src/lib/serialize.js +574 -207
  18. package/src/lib/simplify/abs.js +1 -1
  19. package/src/lib/simplify/bucket.js +18 -23
  20. package/src/lib/simplify/call.js +6 -92
  21. package/src/lib/simplify/product.js +7 -4
  22. package/src/lib/simplify/round.js +4 -4
  23. package/src/lib/simplify/sum.js +22 -10
  24. package/src/lib/simplify.js +20 -5
  25. package/src/reduce.js +46 -118
  26. package/types/index.d.ts +4 -0
  27. package/types/lib/analyze.d.ts +18 -0
  28. package/types/lib/block-index.d.ts +35 -0
  29. package/types/lib/calculation-type.d.ts +18 -0
  30. package/types/lib/compile.d.ts +37 -0
  31. package/types/lib/convertUnits.d.ts +8 -1
  32. package/types/lib/functions.d.ts +40 -0
  33. package/types/lib/limits.d.ts +8 -0
  34. package/types/lib/node.d.ts +31 -9
  35. package/types/lib/opaque.d.ts +9 -0
  36. package/types/lib/parser.d.ts +47 -49
  37. package/types/lib/print.d.ts +16 -0
  38. package/types/lib/regex.d.ts +2 -0
  39. package/types/lib/scan.d.ts +35 -0
  40. package/types/lib/serialize.d.ts +19 -2
  41. package/types/lib/simplify/bucket.d.ts +2 -2
  42. package/types/lib/simplify/call.d.ts +4 -18
  43. package/types/lib/simplify/round.d.ts +3 -1
  44. package/types/lib/simplify.d.ts +5 -2
  45. package/types/reduce.d.ts +47 -6
  46. package/src/lib/tokenizer.js +0 -139
  47. package/types/lib/tokenizer.d.ts +0 -32
package/src/lib/parser.js CHANGED
@@ -1,301 +1,523 @@
1
- // Pratt parser. +/- emit Sum nodes; */÷ emit Product nodes. node.js
2
- // constructors flatten and normalize on construction, while parenthesized
3
- // sums retain a grouping marker for the opaque-subtraction invariant.
4
- import { mkSum, mkProduct, negate, num, dim, ident, call } from './node.js';
1
+ // Pratt parser over native @csstools/css-tokenizer tokens.
2
+ import { TokenType as CssType } from '@csstools/css-tokenizer';
3
+ import { baseOf } from './convertUnits.js';
4
+ import {
5
+ call,
6
+ dim,
7
+ ident,
8
+ mkProduct,
9
+ mkSum,
10
+ negate,
11
+ num,
12
+ opaqueCall,
13
+ } from './node.js';
14
+ import { isCalculationFunction, isSupportedMathFunction } from './functions.js';
15
+ import { assertDepth } from './limits.js';
16
+ import { CSS_NUMBER_PREFIX } from './regex.js';
5
17
 
18
+ /** @typedef {import('@csstools/css-tokenizer').CSSToken} CSSToken */
19
+ /** @typedef {import('./node.js').Node} Node */
20
+ /** @typedef {import('./node.js').OpaqueComponent} OpaqueComponent */
21
+ /** @typedef {ReturnType<typeof import('./block-index.js').indexBlocks>} BlockIndex */
22
+ /** @typedef {{raw: string, pos: number, ws: boolean, index: number}} TokenBase */
23
+ /** @typedef {TokenBase & {type: 'number', value: number, signCharacter?: '+' | '-'}} NumberToken */
24
+ /** @typedef {TokenBase & {type: 'dimension', value: number, unit: string, rawUnit: string, signCharacter?: '+' | '-'}} DimensionToken */
25
+ /** @typedef {TokenBase & {type: 'ident', value: string}} IdentToken */
26
+ /** @typedef {TokenBase & {type: 'function', value: string}} FunctionToken */
27
+ /** @typedef {'(' | ')' | ',' | '+' | '-' | '*' | '/'} Punctuator */
28
+ /** @typedef {TokenBase & {type: 'punct', value: Punctuator}} PunctToken */
29
+ /** @typedef {TokenBase & {type: 'eof', value: '', raw: ''}} EofToken */
30
+ /** @typedef {NumberToken | DimensionToken | IdentToken | FunctionToken | PunctToken | EofToken} Token */
6
31
  /**
7
- * @typedef {import('./tokenizer.js').Token} Token
8
- * @typedef {import('./tokenizer.js').TokenType} TokenType
9
- * @typedef {import('./node.js').Node} Node
10
- * @typedef {(p: Parser, token: Token) => Node} PrefixParselet
32
+ * Immutable bounds and shared block index for one parse range.
33
+ * @typedef {Readonly<{tokens: CSSToken[], end: number, index: BlockIndex}>} ParseInput
11
34
  */
12
35
 
36
+ /** @param {string} value @return {value is '+' | '-' | '*' | '/'} */
37
+ function isOperator(value) {
38
+ return value === '+' || value === '-' || value === '*' || value === '/';
39
+ }
40
+
13
41
  /**
14
- * §10.9 — case-insensitive except for NaN.
15
- * @param {string} name
16
- * @return {Node | null}
42
+ * CSS numeric tokens do not retain a signed-zero distinction. Keep that
43
+ * normalization at the source boundary so a later IEEE-754 `-0` can only
44
+ * have been introduced by calculation evaluation.
45
+ *
46
+ * @param {number} value
47
+ * @return {number}
17
48
  */
18
- function foldCalcKeyword(name) {
19
- // `NaN` and `-NaN` are spec-defined math constants (§10.7.1). The signed
20
- // form arrives as a single ident because CSS Syntax tokenizes leading
21
- // `-` + ident-start as one ident-token.
22
- if (name === 'NaN' || name === '-NaN') {
23
- return num(Number.NaN);
24
- }
25
- switch (name.toLowerCase()) {
26
- case 'pi':
27
- return num(Math.PI);
28
- case 'e':
29
- return num(Math.E);
30
- case 'infinity':
31
- return num(Infinity);
32
- case '-infinity':
33
- return num(-Infinity);
34
- }
35
- return null;
49
+ function normalizeSourceZero(value) {
50
+ return value === 0 ? 0 : value;
36
51
  }
37
52
 
38
- class Parser {
39
- /**
40
- * @param {Token[]} tokens
41
- */
42
- constructor(tokens) {
43
- /** @private */
44
- this.i = 0;
45
- /** @private @readonly */
46
- this.tokens = tokens;
47
- }
53
+ /** @param {string} raw @param {string} decoded */
54
+ function sourceSpelling(raw, decoded) {
55
+ return raw === decoded ? undefined : raw;
56
+ }
48
57
 
49
- /** @return {Token} */
50
- peek() {
51
- return this.tokens[this.i];
58
+ /**
59
+ * Mutable navigation state only. `index` is always the next native token
60
+ * position; trivia is intentionally left visible to `scanToken`.
61
+ */
62
+ class Cursor {
63
+ /** @param {number} start */
64
+ constructor(start) {
65
+ /** @type {number} */
66
+ this.index = start;
67
+ /** @type {boolean} */
68
+ this.firstToken = true;
69
+ /** @type {Token | null} */
70
+ this.lookahead = null;
71
+ /** @type {number} */
72
+ this.lookaheadNextIndex = start;
52
73
  }
53
74
 
54
- /** @return {Token} */
55
- next() {
56
- return this.tokens[this.i++];
75
+ /** @param {number} index @return {void} */
76
+ skipTo(index) {
77
+ this.index = index;
78
+ this.lookaheadNextIndex = index;
79
+ this.firstToken = false;
80
+ this.lookahead = null;
57
81
  }
82
+ }
58
83
 
59
- /**
60
- * @param {TokenType} type
61
- * @param {string} [value]
62
- * @return {Token}
63
- */
64
- expect(type, value) {
65
- const t = this.next();
66
- if (t.type !== type || (value !== undefined && t.value !== value)) {
67
- const want = value ?? type;
68
- throw new Error(
69
- `Expected ${want} at position ${t.pos}, got "${t.value}"`
70
- );
71
- }
72
- return t;
73
- }
84
+ /** @param {OpaqueComponent[]} target @param {OpaqueComponent} part */
85
+ function pushComponent(target, part) {
86
+ if (typeof part === 'string' && typeof target.at(-1) === 'string')
87
+ target[target.length - 1] += part;
88
+ else target.push(part);
89
+ }
74
90
 
75
- /**
76
- * @param {string} value
77
- * @param {string} [value2]
78
- * @return {boolean}
79
- */
80
- isPunct(value, value2) {
81
- const t = this.peek();
82
- return (
83
- t.type === 'punct' &&
84
- (t.value === value || (value2 !== undefined && t.value === value2))
85
- );
91
+ /**
92
+ * Scan one token without consuming it. `firstToken` supplies the virtual
93
+ * leading trivia at a bounded parse boundary; all later whitespace state is
94
+ * derived from the native tokens encountered in this scan.
95
+ *
96
+ * @param {ParseInput} input
97
+ * @param {Cursor} cursor
98
+ * @return {Token}
99
+ */
100
+ function scanToken(input, cursor) {
101
+ let i = cursor.index;
102
+ let ws = cursor.firstToken;
103
+ while (i < input.end) {
104
+ const native = input.tokens[i];
105
+ if (native[0] === CssType.Whitespace || native[0] === CssType.Comment) {
106
+ ws = true;
107
+ i++;
108
+ continue;
109
+ }
110
+ if (native[0] === CssType.EOF) break;
111
+ const token = normalizeToken(native, i, ws);
112
+ cursor.lookahead = token;
113
+ cursor.lookaheadNextIndex = i + 1;
114
+ return token;
86
115
  }
116
+ /** @type {EofToken} */
117
+ const token = {
118
+ type: 'eof',
119
+ value: '',
120
+ raw: '',
121
+ pos: eofPositionAt(input, i),
122
+ ws,
123
+ index: i,
124
+ };
125
+ cursor.lookahead = token;
126
+ // Native EOF is a real token and is consumed past its array index. When
127
+ // the bounded range ends before native EOF, this is a virtual EOF and must
128
+ // remain at the range boundary.
129
+ cursor.lookaheadNextIndex =
130
+ i < input.end && input.tokens[i][0] === CssType.EOF ? i + 1 : input.end;
131
+ return token;
132
+ }
87
133
 
88
- /**
89
- * @param {string} value
90
- * @return {boolean}
91
- */
92
- matchPunct(value) {
93
- if (this.isPunct(value)) {
94
- this.next();
95
- return true;
134
+ /** @param {CSSToken} t @param {number} index @param {boolean} ws @return {Token} */
135
+ function normalizeToken(t, index, ws) {
136
+ const [type, raw, pos, , detail] = t;
137
+ switch (type) {
138
+ case CssType.Number:
139
+ return {
140
+ type: 'number',
141
+ value: detail.value,
142
+ raw,
143
+ pos,
144
+ ws,
145
+ index,
146
+ signCharacter: detail.signCharacter,
147
+ };
148
+ case CssType.Dimension: {
149
+ const match = CSS_NUMBER_PREFIX.exec(raw);
150
+ return {
151
+ type: 'dimension',
152
+ value: detail.value,
153
+ raw,
154
+ pos,
155
+ ws,
156
+ index,
157
+ unit: detail.unit,
158
+ rawUnit: match ? raw.slice(match[0].length) : detail.unit,
159
+ signCharacter: detail.signCharacter,
160
+ };
96
161
  }
97
- return false;
162
+ case CssType.Percentage:
163
+ return {
164
+ type: 'dimension',
165
+ value: detail.value,
166
+ raw,
167
+ pos,
168
+ ws,
169
+ index,
170
+ unit: '%',
171
+ rawUnit: '%',
172
+ signCharacter: detail.signCharacter,
173
+ };
174
+ case CssType.Ident:
175
+ case CssType.Function:
176
+ return {
177
+ type: type === CssType.Ident ? 'ident' : 'function',
178
+ value: detail.value,
179
+ raw,
180
+ pos,
181
+ ws,
182
+ index,
183
+ };
184
+ case CssType.OpenParen:
185
+ return { type: 'punct', value: '(', raw, pos, ws, index };
186
+ case CssType.CloseParen:
187
+ return { type: 'punct', value: ')', raw, pos, ws, index };
188
+ case CssType.Comma:
189
+ return { type: 'punct', value: ',', raw, pos, ws, index };
190
+ case CssType.Delim:
191
+ if (isOperator(detail.value))
192
+ return {
193
+ type: 'punct',
194
+ value: detail.value,
195
+ raw,
196
+ pos,
197
+ ws,
198
+ index,
199
+ };
98
200
  }
201
+ throw new Error(`Unexpected character "${raw[0] ?? ''}" at position ${pos}`);
202
+ }
99
203
 
100
- /**
101
- * @param {string} value
102
- * @return {Token}
103
- */
104
- expectPunct(value) {
105
- return this.expect('punct', value);
204
+ /** @param {ParseInput} input @param {number} index @return {number} */
205
+ function eofPositionAt(input, index) {
206
+ if (index < input.end && input.tokens[index][0] !== CssType.EOF) {
207
+ return input.tokens[index][2];
106
208
  }
107
-
108
- /**
109
- * @param {number} [minBp]
110
- * @return {Node}
111
- */
112
- parseExpr(minBp = 0) {
113
- const t = this.next();
114
- const key = t.type === 'punct' ? t.value : t.type;
115
- const prefix = PREFIX[key];
116
- if (!prefix) {
117
- throw new Error(`Unexpected token "${t.value}" at position ${t.pos}`);
118
- }
119
- let left = prefix(this, t);
120
-
121
- while (true) {
122
- const nxt = this.peek();
123
- const infixKey = nxt.type === 'punct' ? nxt.value : nxt.type;
124
- const rule = INFIX[infixKey];
125
- if (!rule || rule.lbp < minBp) {
126
- break;
127
- }
128
- if (infixKey === '+' || infixKey === '-') {
129
- /** @type {import('./node.js').SumTerm[]} */
130
- const terms = [{ sign: /** @type {1} */ (1), node: left }];
131
- do {
132
- const token = this.next();
133
- requireSurroundingWs(this, token);
134
- terms.push({
135
- sign: /** @type {1 | -1} */ (token.value === '+' ? 1 : -1),
136
- node: this.parseExpr(ADD_BP + 1),
137
- });
138
- } while (this.isPunct('+', '-'));
139
- left = mkSum(terms);
140
- continue;
141
- }
142
-
143
- if (infixKey === '*' || infixKey === '/') {
144
- /** @type {import('./node.js').ProductFactor[]} */
145
- const factors = [{ exponent: /** @type {1} */ (1), node: left }];
146
- do {
147
- const token = this.next();
148
- factors.push({
149
- exponent: /** @type {1 | -1} */ (token.value === '*' ? 1 : -1),
150
- node: this.parseExpr(MUL_BP + 1),
151
- });
152
- } while (this.isPunct('*', '/'));
153
- left = mkProduct(factors);
154
- continue;
155
- }
156
-
157
- // Every infix operator is handled above. Keep this defensive exit in
158
- // case a future parselet is added without a chain implementation.
159
- break;
160
- }
161
-
162
- return left;
209
+ if (
210
+ input.end < input.tokens.length &&
211
+ input.tokens[input.end][0] !== CssType.EOF
212
+ ) {
213
+ return input.tokens[input.end][2];
214
+ }
215
+ for (let i = Math.min(input.end, input.tokens.length) - 1; i >= 0; i--) {
216
+ const token = input.tokens[i];
217
+ if (token[0] !== CssType.EOF) return token[3] + 1;
163
218
  }
219
+ return 0;
164
220
  }
165
221
 
166
- const ADD_BP = 1;
167
- const MUL_BP = 3;
168
- const UNARY_BP = 7;
222
+ /** @param {ParseInput} input @param {Cursor} cursor @return {Token} */
223
+ function peekToken(input, cursor) {
224
+ return cursor.lookahead ?? scanToken(input, cursor);
225
+ }
169
226
 
170
227
  /**
171
- * Functions whose argument list isn't a comma-separated list of calc
172
- * expressions. Their bodies are slurped as opaque space-separated tokens
173
- * and round-tripped verbatim. anchor() / anchor-size() use the
174
- * `<anchor-name> <anchor-side>` shape (CSS Anchor Positioning).
228
+ * Consume the cached token and advance to its native next index. This is one
229
+ * of the only two operations allowed to advance `cursor.index`.
230
+ * @param {ParseInput} input
231
+ * @param {Cursor} cursor
232
+ * @return {Token}
175
233
  */
176
- const OPAQUE_ARG_FUNCTIONS = new Set(['anchor', 'anchor-size']);
234
+ function takeToken(input, cursor) {
235
+ const token = peekToken(input, cursor);
236
+ cursor.index = cursor.lookaheadNextIndex;
237
+ cursor.firstToken = false;
238
+ cursor.lookahead = null;
239
+ return token;
240
+ }
177
241
 
178
- /**
179
- * Reconstruct a token's source text for opaque-arg slurping.
180
- * @param {Token} t
181
- * @return {string}
182
- */
183
- function tokenText(t) {
184
- if (t.type === 'dimension') {
185
- return `${t.value}${t.unit ?? ''}`;
242
+ /** @param {ParseInput} input @param {Cursor} cursor @param {Punctuator} value @param {Punctuator} [value2] @return {boolean} */
243
+ function isPunct(input, cursor, value, value2) {
244
+ const t = peekToken(input, cursor);
245
+ return (
246
+ t.type === 'punct' &&
247
+ (t.value === value || (value2 !== undefined && t.value === value2))
248
+ );
249
+ }
250
+
251
+ /** @param {ParseInput} input @param {Cursor} cursor @param {Punctuator} value @return {boolean} */
252
+ function matchPunct(input, cursor, value) {
253
+ if (!isPunct(input, cursor, value)) return false;
254
+ takeToken(input, cursor);
255
+ return true;
256
+ }
257
+
258
+ /** @param {ParseInput} input @param {Cursor} cursor @param {Punctuator} value @return {PunctToken} */
259
+ function expectPunct(input, cursor, value) {
260
+ const t = takeToken(input, cursor);
261
+ if (t.type !== 'punct' || t.value !== value) {
262
+ throw new Error(`Expected ${value} at position ${t.pos}, got "${t.value}"`);
186
263
  }
187
- return t.value;
264
+ return t;
188
265
  }
189
266
 
190
- /**
191
- * Parse the body of an opaque-arg call, with `(` already consumed.
192
- * @param {Parser} p
193
- * @param {string} name
194
- * @return {Node}
195
- */
196
- function parseOpaqueCall(p, name) {
197
- /** @type {Node[]} */
198
- const args = [];
199
- let buf = '';
200
- let depth = 1;
201
- const flush = () => {
202
- const trimmed = buf.trim();
203
- if (trimmed) {
204
- args.push(ident(trimmed));
205
- }
206
- buf = '';
207
- };
208
- while (true) {
209
- const tk = p.peek();
210
- if (tk.type === 'eof') {
211
- throw new Error(`Unclosed ${name}( at position ${tk.pos}`);
267
+ /** @param {ParseInput} input @param {Cursor} cursor @param {Token} token @param {number} depth @return {Node} */
268
+ function parsePrefix(input, cursor, token, depth) {
269
+ switch (token.type) {
270
+ case 'number':
271
+ return num(normalizeSourceZero(token.value));
272
+ case 'dimension': {
273
+ const unit = token.unit.toLowerCase();
274
+ return dim(
275
+ normalizeSourceZero(token.value),
276
+ unit,
277
+ baseOf(unit) || token.rawUnit === unit ? undefined : token.rawUnit
278
+ );
212
279
  }
213
- if (tk.type === 'punct') {
214
- if (tk.value === '(') {
215
- depth++;
216
- } else if (tk.value === ')') {
217
- depth--;
218
- if (depth === 0) {
219
- p.next();
220
- flush();
221
- return call(name, args);
280
+ case 'ident':
281
+ return (
282
+ foldCalcKeyword(token.value) ??
283
+ ident(token.value, sourceSpelling(token.raw, token.value))
284
+ );
285
+ case 'function':
286
+ return parseCall(input, cursor, token, depth);
287
+ case 'punct':
288
+ switch (token.value) {
289
+ case '(': {
290
+ const expression = parseExpr(input, cursor, 0, depth + 1);
291
+ expectPunct(input, cursor, ')');
292
+ return expression.type === 'Sum'
293
+ ? { ...expression, grouped: true }
294
+ : expression;
222
295
  }
223
- } else if (tk.value === ',' && depth === 1) {
224
- p.next();
225
- flush();
226
- continue;
296
+ case '-':
297
+ return negate(parseExpr(input, cursor, 7, depth + 1));
298
+ case '+':
299
+ return parseExpr(input, cursor, 7, depth + 1);
227
300
  }
301
+ }
302
+ throw new Error(`Unexpected token "${token.raw}" at position ${token.pos}`);
303
+ }
304
+
305
+ /** @param {ParseInput} input @param {Cursor} cursor @param {number} minBp @param {number} depth @return {Node} */
306
+ function parseExpr(input, cursor, minBp = 0, depth = 0) {
307
+ assertDepth(depth);
308
+ const t = takeToken(input, cursor);
309
+ let left = parsePrefix(input, cursor, t, depth);
310
+
311
+ while (true) {
312
+ const nxt = peekToken(input, cursor);
313
+ if (
314
+ (nxt.type === 'number' || nxt.type === 'dimension') &&
315
+ nxt.signCharacter !== undefined
316
+ ) {
317
+ throw new Error(
318
+ `"${nxt.signCharacter}" must be surrounded by whitespace at position ${nxt.pos}`
319
+ );
320
+ }
321
+ const infixKey = nxt.type === 'punct' ? String(nxt.value) : nxt.type;
322
+ const rule = INFIX[infixKey];
323
+ if (!rule || rule.lbp < minBp) break;
324
+ if (infixKey === '+' || infixKey === '-') {
325
+ /** @type {import('./node.js').SumTerm[]} */
326
+ const terms = [{ sign: /** @type {1} */ (1), node: left }];
327
+ do {
328
+ const token = takeToken(input, cursor);
329
+ requireSurroundingWs(input, cursor, token);
330
+ terms.push({
331
+ sign: /** @type {1 | -1} */ (token.value === '+' ? 1 : -1),
332
+ node: parseExpr(input, cursor, ADD_BP + 1, depth),
333
+ });
334
+ } while (isPunct(input, cursor, '+', '-'));
335
+ left = mkSum(terms);
336
+ continue;
228
337
  }
229
- if (tk.ws && buf) {
230
- buf += ' ';
338
+ if (infixKey === '*' || infixKey === '/') {
339
+ /** @type {import('./node.js').ProductFactor[]} */
340
+ const factors = [{ exponent: /** @type {1} */ (1), node: left }];
341
+ do {
342
+ const token = takeToken(input, cursor);
343
+ factors.push({
344
+ exponent: /** @type {1 | -1} */ (token.value === '*' ? 1 : -1),
345
+ node: parseExpr(input, cursor, MUL_BP + 1, depth),
346
+ });
347
+ } while (isPunct(input, cursor, '*', '/'));
348
+ left = mkProduct(factors);
349
+ continue;
231
350
  }
232
- buf += tokenText(tk);
233
- p.next();
351
+ break;
234
352
  }
353
+ return left;
235
354
  }
236
355
 
237
- /**
238
- * §10.1 requires whitespace on both sides of a binary `+` / `-`. Without
239
- * it, CSS tokenization treats `1px+2px` as two tokens with no operator
240
- * between them (browsers reject this). We enforce the rule here by
241
- * checking the token's `ws` flag (whitespace before the `+`/`-`) and the
242
- * following token's flag (whitespace after).
243
- * @param {Parser} p
244
- * @param {Token} token
245
- * @return {void}
246
- */
247
- function requireSurroundingWs(p, token) {
248
- const next = p.peek();
249
- if (!token.ws || !next.ws) {
356
+ /** §10.9 — case-insensitive except for NaN. @param {string} name @return {Node | null} */
357
+ function foldCalcKeyword(name) {
358
+ if (name === 'NaN' || name === '-NaN') return num(Number.NaN);
359
+ switch (name.toLowerCase()) {
360
+ case 'pi':
361
+ return num(Math.PI);
362
+ case 'e':
363
+ return num(Math.E);
364
+ case 'infinity':
365
+ return num(Infinity);
366
+ case '-infinity':
367
+ return num(-Infinity);
368
+ }
369
+ return null;
370
+ }
371
+
372
+ const ADD_BP = 1;
373
+ const MUL_BP = 3;
374
+ /** @param {ParseInput} input @param {Cursor} cursor @param {FunctionToken} token @param {string} name @param {string} rawName @return {Node} */
375
+ function parseOpaqueCall(input, cursor, token, name, rawName) {
376
+ const start = token.index + 1;
377
+ const close = input.index.closeOf(token.index, input.end);
378
+ if (close === -1)
379
+ throw new Error(
380
+ `Unclosed ${name}( at position ${eofPositionAt(input, cursor.index)}`
381
+ );
382
+ cursor.skipTo(close + 1);
383
+ return opaqueCall(
384
+ name,
385
+ componentTree(input, start, close),
386
+ sourceSpelling(rawName, name)
387
+ );
388
+ }
389
+
390
+ /** @param {ParseInput} input @param {Cursor} cursor @param {Token} token */
391
+ function requireSurroundingWs(input, cursor, token) {
392
+ if (!token.ws || !peekToken(input, cursor).ws)
250
393
  throw new Error(
251
394
  `"${token.value}" must be surrounded by whitespace at position ${token.pos}`
252
395
  );
396
+ }
397
+
398
+ /** @param {ParseInput} input @param {Cursor} cursor @param {FunctionToken} t @param {number} depth @return {Node} */
399
+ function parseCall(input, cursor, t, depth) {
400
+ const name = t.value;
401
+ const rawName = t.raw.slice(0, -1);
402
+ if (name.toLowerCase() === 'var')
403
+ return parseVar(input, cursor, t, name, rawName);
404
+ if (!isCalculationFunction(name) && !isSupportedMathFunction(name))
405
+ return parseOpaqueCall(input, cursor, t, name, rawName);
406
+ /** @type {Node[]} */ const args = [];
407
+ if (!isPunct(input, cursor, ')')) {
408
+ args.push(parseExpr(input, cursor, 0, depth + 1));
409
+ while (matchPunct(input, cursor, ','))
410
+ args.push(parseExpr(input, cursor, 0, depth + 1));
253
411
  }
412
+ expectPunct(input, cursor, ')');
413
+ return call(name, args, sourceSpelling(rawName, name));
254
414
  }
255
415
 
256
- /** @type {Record<string, PrefixParselet>} */
257
- const PREFIX = {
258
- number: (_p, t) => num(Number.parseFloat(t.value)),
416
+ /** @param {CSSToken[]} tokens @param {number} start @param {number} end */
417
+ function rawTokens(tokens, start, end) {
418
+ let raw = '';
419
+ for (let i = start; i < end; i++) raw += tokens[i][1];
420
+ return raw;
421
+ }
422
+ /** @param {CSSToken[]} tokens @param {number} start @param {number} end */
423
+ function customProperty(tokens, start, end) {
424
+ /** @type {CSSToken | null} */
425
+ let found = null;
426
+ let foundIndex = -1;
427
+ for (let i = start; i < end; i++) {
428
+ const type = tokens[i][0];
429
+ if (type === CssType.Whitespace || type === CssType.Comment) continue;
430
+ if (found !== null) return null;
431
+ found = tokens[i];
432
+ foundIndex = i;
433
+ }
434
+ if (!found || found[0] !== CssType.Ident) return null;
435
+ const decoded = found[4].value;
436
+ return decoded.startsWith('--') && decoded !== '--'
437
+ ? { decoded, raw: found[1], index: foundIndex }
438
+ : null;
439
+ }
440
+ /** @param {ParseInput} input @param {number} start @param {number} end @param {number} [depth] @return {OpaqueComponent[]} */
441
+ function componentTree(input, start, end, depth = 0) {
442
+ assertDepth(depth);
443
+ /** @type {OpaqueComponent[]} */ const root = [];
444
+ /** @type {OpaqueComponent[]} */ let tree = root;
445
+ /** @type {{parent: OpaqueComponent[], tree: OpaqueComponent[], close: number, end: number}[]} */
446
+ const frames = [];
447
+ const { tokens } = input;
448
+ let i = start;
449
+ while (true) {
450
+ if (i >= end) {
451
+ if (frames.length === 0) break;
452
+ const frame =
453
+ /** @type {{parent: OpaqueComponent[], tree: OpaqueComponent[], close: number, end: number}} */ (
454
+ frames.pop()
455
+ );
456
+ tree = frame.parent;
457
+ pushComponent(tree, frame.tree);
458
+ pushComponent(tree, tokens[frame.close][1]);
459
+ end = frame.end;
460
+ i = frame.close + 1;
461
+ continue;
462
+ }
259
463
 
260
- // Unit case normalization per §10.12: `1PX` serializes as `1px`.
261
- dimension: (_p, t) =>
262
- dim(
263
- Number.parseFloat(t.value),
264
- t.unit === '%' ? '%' : /** @type {string} */ (t.unit).toLowerCase()
265
- ),
464
+ const token = tokens[i];
465
+ const close = input.index.closeOf(i, end);
466
+ if (close === -1) {
467
+ pushComponent(tree, token[1]);
468
+ i++;
469
+ continue;
470
+ }
266
471
 
267
- ident: (p, t) => {
268
- if (p.matchPunct('(')) {
269
- if (OPAQUE_ARG_FUNCTIONS.has(t.value.toLowerCase())) {
270
- return parseOpaqueCall(p, t.value);
271
- }
272
- /** @type {Node[]} */
273
- const args = [];
274
- if (!p.isPunct(')')) {
275
- args.push(p.parseExpr(0));
276
- while (p.matchPunct(',')) {
277
- args.push(p.parseExpr(0));
278
- }
472
+ const isMathFunction =
473
+ token[0] === CssType.Function &&
474
+ (isCalculationFunction(token[4].value) ||
475
+ isSupportedMathFunction(token[4].value));
476
+ if (isMathFunction) {
477
+ try {
478
+ pushComponent(tree, parseRange(input, i, close + 1));
479
+ } catch {
480
+ pushComponent(tree, rawTokens(tokens, i, close + 1));
279
481
  }
280
- p.expectPunct(')');
281
- return call(t.value, args);
282
- }
283
- const kw = foldCalcKeyword(t.value);
284
- if (kw) {
285
- return kw;
482
+ i = close + 1;
483
+ continue;
286
484
  }
287
- return ident(t.value);
288
- },
289
485
 
290
- '(': (p) => {
291
- const e = p.parseExpr(0);
292
- p.expectPunct(')');
293
- return e.type === 'Sum' ? { ...e, grouped: true } : e;
294
- },
295
-
296
- '-': (p) => negate(p.parseExpr(UNARY_BP)),
297
- '+': (p) => p.parseExpr(UNARY_BP),
298
- };
486
+ pushComponent(tree, token[1]);
487
+ assertDepth(depth + frames.length + 1);
488
+ /** @type {OpaqueComponent[]} */
489
+ const child = [];
490
+ frames.push({ parent: tree, tree: child, close, end });
491
+ tree = child;
492
+ end = close;
493
+ i++;
494
+ }
495
+ return root;
496
+ }
497
+ /** @param {ParseInput} input @param {Cursor} cursor @param {FunctionToken} token @param {string} name @param {string} rawName @return {Node} */
498
+ function parseVar(input, cursor, token, name, rawName) {
499
+ const { tokens } = input;
500
+ const start = token.index + 1;
501
+ const close = input.index.closeOf(token.index, input.end);
502
+ if (close === -1)
503
+ throw new Error(
504
+ `Unclosed ${name}( at position ${eofPositionAt(input, cursor.index)}`
505
+ );
506
+ const comma = input.index.firstTopLevelComma(start, close);
507
+ const property = customProperty(tokens, start, comma === -1 ? close : comma);
508
+ if (!property)
509
+ throw new Error(
510
+ `Invalid custom property in ${name}() at position ${tokens[start]?.[2] ?? eofPositionAt(input, cursor.index)}`
511
+ );
512
+ cursor.skipTo(close + 1);
513
+ /** @type {OpaqueComponent[]} */
514
+ const components = [
515
+ ident(property.decoded, sourceSpelling(property.raw, property.decoded)),
516
+ ];
517
+ if (comma !== -1)
518
+ components.push(...componentTree(input, property.index + 1, close));
519
+ return opaqueCall(name, components, sourceSpelling(rawName, name));
520
+ }
299
521
 
300
522
  /** @type {Record<string, {lbp: number}>} */
301
523
  const INFIX = {
@@ -306,19 +528,33 @@ const INFIX = {
306
528
  };
307
529
 
308
530
  /**
309
- * @param {Token[]} tokens
531
+ * @param {ParseInput} input
532
+ * @param {number} start
533
+ * @param {number} end
310
534
  * @return {Node}
311
535
  */
312
- function parse(tokens) {
313
- const p = new Parser(tokens);
314
- const ast = p.parseExpr(0);
315
- const trailing = p.peek();
316
- if (trailing.type !== 'eof') {
536
+ function parseRange(input, start, end) {
537
+ const bounded =
538
+ input.end === end
539
+ ? input
540
+ : /** @type {ParseInput} */ ({
541
+ tokens: input.tokens,
542
+ end,
543
+ index: input.index,
544
+ });
545
+ const cursor = new Cursor(start);
546
+ const ast = parseExpr(bounded, cursor, 0, 0);
547
+ const trailing = peekToken(bounded, cursor);
548
+ if (trailing.type !== 'eof')
317
549
  throw new Error(
318
- `Unexpected token "${trailing.value}" at position ${trailing.pos}`
550
+ `Unexpected token "${trailing.raw}" at position ${trailing.pos}`
319
551
  );
320
- }
321
552
  return ast;
322
553
  }
323
554
 
555
+ /** @param {CSSToken[]} tokens @param {number} start @param {number} end @param {BlockIndex} index @return {Node} */
556
+ function parse(tokens, start, end, index) {
557
+ return parseRange({ tokens, end, index }, start, end);
558
+ }
559
+
324
560
  export { parse };