postcss-calc 11.1.2 → 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 (41) hide show
  1. package/README.md +40 -18
  2. package/package.json +16 -12
  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/functions.js +364 -0
  9. package/src/lib/limits.js +49 -0
  10. package/src/lib/node.js +63 -21
  11. package/src/lib/opaque.js +12 -32
  12. package/src/lib/parser.js +383 -328
  13. package/src/lib/print.js +75 -0
  14. package/src/lib/regex.js +4 -0
  15. package/src/lib/scan.js +61 -0
  16. package/src/lib/serialize.js +574 -215
  17. package/src/lib/simplify/call.js +5 -98
  18. package/src/lib/simplify/round.js +4 -4
  19. package/src/lib/simplify/sum.js +14 -5
  20. package/src/lib/simplify.js +20 -5
  21. package/src/reduce.js +46 -115
  22. package/types/index.d.ts +4 -0
  23. package/types/lib/analyze.d.ts +18 -0
  24. package/types/lib/block-index.d.ts +35 -0
  25. package/types/lib/calculation-type.d.ts +18 -0
  26. package/types/lib/compile.d.ts +37 -0
  27. package/types/lib/functions.d.ts +40 -0
  28. package/types/lib/limits.d.ts +8 -0
  29. package/types/lib/node.d.ts +19 -3
  30. package/types/lib/opaque.d.ts +8 -14
  31. package/types/lib/parser.d.ts +42 -48
  32. package/types/lib/print.d.ts +16 -0
  33. package/types/lib/regex.d.ts +2 -0
  34. package/types/lib/scan.d.ts +35 -0
  35. package/types/lib/serialize.d.ts +19 -2
  36. package/types/lib/simplify/call.d.ts +4 -18
  37. package/types/lib/simplify/round.d.ts +3 -1
  38. package/types/lib/simplify.d.ts +5 -2
  39. package/types/reduce.d.ts +47 -6
  40. package/src/lib/tokenizer.js +0 -10
  41. package/types/lib/tokenizer.d.ts +0 -3
package/src/lib/parser.js CHANGED
@@ -1,227 +1,138 @@
1
1
  // Pratt parser over native @csstools/css-tokenizer tokens.
2
2
  import { TokenType as CssType } from '@csstools/css-tokenizer';
3
3
  import { baseOf } from './convertUnits.js';
4
- import { mkSum, mkProduct, negate, num, dim, ident, call } from './node.js';
5
- import { setComponents } from './opaque.js';
6
- import { isSupportedMathFunction } from './simplify/call.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';
7
17
 
8
18
  /** @typedef {import('@csstools/css-tokenizer').CSSToken} CSSToken */
9
19
  /** @typedef {import('./node.js').Node} Node */
10
- /** @typedef {string | Node | Component[]} Component */
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 */
11
31
  /**
12
- * @typedef {object} Token
13
- * @property {'number' | 'dimension' | 'ident' | 'function' | 'punct' | 'eof'} type
14
- * @property {string | number} value
15
- * @property {string} raw
16
- * @property {string} [unit]
17
- * @property {string} [rawUnit]
18
- * @property {'+' | '-'} [signCharacter]
19
- * @property {number} pos
20
- * @property {boolean} ws
32
+ * Immutable bounds and shared block index for one parse range.
33
+ * @typedef {Readonly<{tokens: CSSToken[], end: number, index: BlockIndex}>} ParseInput
21
34
  */
22
- /** @typedef {(p: Parser, token: Token) => Node} PrefixParselet */
23
35
 
24
- const NUMERIC_RAW = /^[+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?/;
25
- const PUNCT_DELIMS = new Set(['+', '-', '*', '/']);
26
- const BLOCK_CLOSE = new Map([
27
- [CssType.Function, CssType.CloseParen],
28
- [CssType.OpenParen, CssType.CloseParen],
29
- [CssType.OpenSquare, CssType.CloseSquare],
30
- [CssType.OpenCurly, CssType.CloseCurly],
31
- ]);
36
+ /** @param {string} value @return {value is '+' | '-' | '*' | '/'} */
37
+ function isOperator(value) {
38
+ return value === '+' || value === '-' || value === '*' || value === '/';
39
+ }
40
+
41
+ /**
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}
48
+ */
49
+ function normalizeSourceZero(value) {
50
+ return value === 0 ? 0 : value;
51
+ }
52
+
32
53
  /** @param {string} raw @param {string} decoded */
33
54
  function sourceSpelling(raw, decoded) {
34
55
  return raw === decoded ? undefined : raw;
35
56
  }
36
57
 
37
- /** Bounded cursor that skips trivia but records whether it preceded a token. */
38
- class Parser {
39
- /** @type {CSSToken[]} */
40
- #tokens;
41
- /** @type {number} */
42
- #end;
43
- /** @type {Map<number, number>} */
44
- #ends;
45
- /** @type {number} */
46
- #i;
47
- /** @type {boolean} */
48
- #precededByWhitespace = true;
49
- /** @type {Token | null} */
50
- #lookahead = null;
51
-
52
- /**
53
- * @param {CSSToken[]} tokens
54
- * @param {number} start
55
- * @param {number} end
56
- * @param {Map<number, number>} [ends]
57
- */
58
- constructor(tokens, start, end, ends = blockEnds(tokens, start, end)) {
59
- this.#tokens = tokens;
60
- this.#end = end;
61
- this.#ends = ends;
62
- this.#i = start;
63
- this.#precededByWhitespace = true;
64
- this.#lookahead = null;
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;
65
73
  }
66
74
 
67
- /** @return {Map<number, number>} */
68
- get ends() {
69
- return this.#ends;
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;
70
81
  }
82
+ }
71
83
 
72
- /** @return {number} */
73
- eofPosition() {
74
- if (this.#i < this.#end && this.#tokens[this.#i][0] !== CssType.EOF) {
75
- return this.#tokens[this.#i][2];
76
- }
77
- if (
78
- this.#end < this.#tokens.length &&
79
- this.#tokens[this.#end][0] !== CssType.EOF
80
- ) {
81
- return this.#tokens[this.#end][2];
82
- }
83
- for (let i = Math.min(this.#end, this.#tokens.length) - 1; i >= 0; i--) {
84
- const token = this.#tokens[i];
85
- if (token[0] !== CssType.EOF) return token[3] + 1;
86
- }
87
- return 0;
88
- }
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
+ }
89
90
 
90
- /** @return {Token} */
91
- read() {
92
- while (this.#i < this.#end) {
93
- const native = this.#tokens[this.#i++];
94
- if (native[0] === CssType.Whitespace || native[0] === CssType.Comment) {
95
- this.#precededByWhitespace = true;
96
- continue;
97
- }
98
- if (native[0] === CssType.EOF) break;
99
- const ws = this.#precededByWhitespace;
100
- this.#precededByWhitespace = false;
101
- return normalizeToken(native, ws);
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;
102
109
  }
103
- return {
104
- type: 'eof',
105
- value: '',
106
- raw: '',
107
- pos: this.eofPosition(),
108
- ws: this.#precededByWhitespace,
109
- };
110
- }
111
-
112
- /** @return {Token} */
113
- peek() {
114
- if (this.#lookahead === null) this.#lookahead = this.read();
115
- return this.#lookahead;
116
- }
117
-
118
- /** @return {Token} */
119
- next() {
120
- const token = this.peek();
121
- this.#lookahead = null;
110
+ if (native[0] === CssType.EOF) break;
111
+ const token = normalizeToken(native, i, ws);
112
+ cursor.lookahead = token;
113
+ cursor.lookaheadNextIndex = i + 1;
122
114
  return token;
123
115
  }
124
-
125
- /** @return {{start: number, close: number, tokens: CSSToken[], ends: Map<number, number>}} */
126
- functionRange() {
127
- return {
128
- start: this.#i,
129
- close: this.#ends.get(this.#i - 1) ?? -1,
130
- tokens: this.#tokens,
131
- ends: this.#ends,
132
- };
133
- }
134
-
135
- /** @param {number} index */
136
- consumeThrough(index) {
137
- this.#i = index;
138
- this.#lookahead = null;
139
- }
140
-
141
- /** @param {string} value @param {string} [value2] @return {boolean} */
142
- isPunct(value, value2) {
143
- const t = this.peek();
144
- return (
145
- t.type === 'punct' &&
146
- (t.value === value || (value2 !== undefined && t.value === value2))
147
- );
148
- }
149
-
150
- /** @param {string} value @return {boolean} */
151
- matchPunct(value) {
152
- if (!this.isPunct(value)) return false;
153
- this.next();
154
- return true;
155
- }
156
-
157
- /** @param {string} value @return {Token} */
158
- expectPunct(value) {
159
- const t = this.next();
160
- if (t.type !== 'punct' || t.value !== value) {
161
- throw new Error(
162
- `Expected ${value} at position ${t.pos}, got "${t.value}"`
163
- );
164
- }
165
- return t;
166
- }
167
-
168
- /** @param {number} [minBp] @return {Node} */
169
- parseExpr(minBp = 0) {
170
- const t = this.next();
171
- const key = t.type === 'punct' ? String(t.value) : t.type;
172
- const prefix = PREFIX[key];
173
- if (!prefix)
174
- throw new Error(`Unexpected token "${t.raw}" at position ${t.pos}`);
175
- let left = prefix(this, t);
176
-
177
- while (true) {
178
- const nxt = this.peek();
179
- if (
180
- (nxt.type === 'number' || nxt.type === 'dimension') &&
181
- nxt.signCharacter !== undefined
182
- ) {
183
- throw new Error(
184
- `"${nxt.signCharacter}" must be surrounded by whitespace at position ${nxt.pos}`
185
- );
186
- }
187
- const infixKey = nxt.type === 'punct' ? String(nxt.value) : nxt.type;
188
- const rule = INFIX[infixKey];
189
- if (!rule || rule.lbp < minBp) break;
190
- if (infixKey === '+' || infixKey === '-') {
191
- /** @type {import('./node.js').SumTerm[]} */
192
- const terms = [{ sign: /** @type {1} */ (1), node: left }];
193
- do {
194
- const token = this.next();
195
- requireSurroundingWs(this, token);
196
- terms.push({
197
- sign: /** @type {1 | -1} */ (token.value === '+' ? 1 : -1),
198
- node: this.parseExpr(ADD_BP + 1),
199
- });
200
- } while (this.isPunct('+', '-'));
201
- left = mkSum(terms);
202
- continue;
203
- }
204
- if (infixKey === '*' || infixKey === '/') {
205
- /** @type {import('./node.js').ProductFactor[]} */
206
- const factors = [{ exponent: /** @type {1} */ (1), node: left }];
207
- do {
208
- const token = this.next();
209
- factors.push({
210
- exponent: /** @type {1 | -1} */ (token.value === '*' ? 1 : -1),
211
- node: this.parseExpr(MUL_BP + 1),
212
- });
213
- } while (this.isPunct('*', '/'));
214
- left = mkProduct(factors);
215
- continue;
216
- }
217
- break;
218
- }
219
- return left;
220
- }
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;
221
132
  }
222
133
 
223
- /** @param {CSSToken} t @param {boolean} ws @return {Token} */
224
- function normalizeToken(t, ws) {
134
+ /** @param {CSSToken} t @param {number} index @param {boolean} ws @return {Token} */
135
+ function normalizeToken(t, index, ws) {
225
136
  const [type, raw, pos, , detail] = t;
226
137
  switch (type) {
227
138
  case CssType.Number:
@@ -231,16 +142,18 @@ function normalizeToken(t, ws) {
231
142
  raw,
232
143
  pos,
233
144
  ws,
145
+ index,
234
146
  signCharacter: detail.signCharacter,
235
147
  };
236
148
  case CssType.Dimension: {
237
- const match = NUMERIC_RAW.exec(raw);
149
+ const match = CSS_NUMBER_PREFIX.exec(raw);
238
150
  return {
239
151
  type: 'dimension',
240
152
  value: detail.value,
241
153
  raw,
242
154
  pos,
243
155
  ws,
156
+ index,
244
157
  unit: detail.unit,
245
158
  rawUnit: match ? raw.slice(match[0].length) : detail.unit,
246
159
  signCharacter: detail.signCharacter,
@@ -253,6 +166,7 @@ function normalizeToken(t, ws) {
253
166
  raw,
254
167
  pos,
255
168
  ws,
169
+ index,
256
170
  unit: '%',
257
171
  rawUnit: '%',
258
172
  signCharacter: detail.signCharacter,
@@ -265,18 +179,180 @@ function normalizeToken(t, ws) {
265
179
  raw,
266
180
  pos,
267
181
  ws,
182
+ index,
268
183
  };
269
184
  case CssType.OpenParen:
185
+ return { type: 'punct', value: '(', raw, pos, ws, index };
270
186
  case CssType.CloseParen:
187
+ return { type: 'punct', value: ')', raw, pos, ws, index };
271
188
  case CssType.Comma:
272
- return { type: 'punct', value: raw, raw, pos, ws };
189
+ return { type: 'punct', value: ',', raw, pos, ws, index };
273
190
  case CssType.Delim:
274
- if (PUNCT_DELIMS.has(detail.value))
275
- return { type: 'punct', value: detail.value, raw, pos, ws };
191
+ if (isOperator(detail.value))
192
+ return {
193
+ type: 'punct',
194
+ value: detail.value,
195
+ raw,
196
+ pos,
197
+ ws,
198
+ index,
199
+ };
276
200
  }
277
201
  throw new Error(`Unexpected character "${raw[0] ?? ''}" at position ${pos}`);
278
202
  }
279
203
 
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];
208
+ }
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;
218
+ }
219
+ return 0;
220
+ }
221
+
222
+ /** @param {ParseInput} input @param {Cursor} cursor @return {Token} */
223
+ function peekToken(input, cursor) {
224
+ return cursor.lookahead ?? scanToken(input, cursor);
225
+ }
226
+
227
+ /**
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}
233
+ */
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
+ }
241
+
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}"`);
263
+ }
264
+ return t;
265
+ }
266
+
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
+ );
279
+ }
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;
295
+ }
296
+ case '-':
297
+ return negate(parseExpr(input, cursor, 7, depth + 1));
298
+ case '+':
299
+ return parseExpr(input, cursor, 7, depth + 1);
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;
337
+ }
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;
350
+ }
351
+ break;
352
+ }
353
+ return left;
354
+ }
355
+
280
356
  /** §10.9 — case-insensitive except for NaN. @param {string} name @return {Node | null} */
281
357
  function foldCalcKeyword(name) {
282
358
  if (name === 'NaN' || name === '-NaN') return num(Number.NaN);
@@ -295,41 +371,45 @@ function foldCalcKeyword(name) {
295
371
 
296
372
  const ADD_BP = 1;
297
373
  const MUL_BP = 3;
298
- const MATCH_CALC = /^(?:-(?:moz|webkit)-)?calc$/i;
299
-
300
- /** @param {Parser} p @param {string} name @param {string} rawName @return {Node} */
301
- function parseOpaqueCall(p, name, rawName) {
302
- const { start, close, tokens, ends } = p.functionRange();
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);
303
378
  if (close === -1)
304
- throw new Error(`Unclosed ${name}( at position ${p.eofPosition()}`);
305
- p.consumeThrough(close + 1);
306
- return setComponents(
307
- call(name, [], sourceSpelling(rawName, name)),
308
- componentTree(tokens, start, close, ends)
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)
309
387
  );
310
388
  }
311
389
 
312
- /** @param {Parser} p @param {Token} token */
313
- function requireSurroundingWs(p, token) {
314
- if (!token.ws || !p.peek().ws)
390
+ /** @param {ParseInput} input @param {Cursor} cursor @param {Token} token */
391
+ function requireSurroundingWs(input, cursor, token) {
392
+ if (!token.ws || !peekToken(input, cursor).ws)
315
393
  throw new Error(
316
394
  `"${token.value}" must be surrounded by whitespace at position ${token.pos}`
317
395
  );
318
396
  }
319
397
 
320
- /** @param {Parser} p @param {Token} t @return {Node} */
321
- function parseCall(p, t) {
322
- const name = String(t.value);
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;
323
401
  const rawName = t.raw.slice(0, -1);
324
- if (name.toLowerCase() === 'var') return parseVar(p, name, rawName);
325
- if (!MATCH_CALC.test(name) && !isSupportedMathFunction(name))
326
- return parseOpaqueCall(p, name, rawName);
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);
327
406
  /** @type {Node[]} */ const args = [];
328
- if (!p.isPunct(')')) {
329
- args.push(p.parseExpr(0));
330
- while (p.matchPunct(',')) args.push(p.parseExpr(0));
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));
331
411
  }
332
- p.expectPunct(')');
412
+ expectPunct(input, cursor, ')');
333
413
  return call(name, args, sourceSpelling(rawName, name));
334
414
  }
335
415
 
@@ -339,37 +419,6 @@ function rawTokens(tokens, start, end) {
339
419
  for (let i = start; i < end; i++) raw += tokens[i][1];
340
420
  return raw;
341
421
  }
342
- /** @param {CSSToken[]} tokens @param {number} start @param {number} end @param {Map<number, number>} ends @return {number} */
343
- function firstComma(tokens, start, end, ends) {
344
- for (let i = start; i < end; i++) {
345
- const close = ends.get(i);
346
- if (close !== undefined) {
347
- i = close;
348
- continue;
349
- }
350
- if (tokens[i][0] === CssType.Comma) return i;
351
- }
352
- return -1;
353
- }
354
- /** @param {CSSToken[]} tokens @param {number} start @param {number} end */
355
- function blockEnds(tokens, start, end) {
356
- /** @type {{index: number, close: import('@csstools/css-tokenizer').TokenType}[]} */
357
- const stack = [];
358
- /** @type {Map<number, number>} */ const ends = new Map();
359
- for (let i = start; i < end; i++) {
360
- const close = BLOCK_CLOSE.get(tokens[i][0]);
361
- if (close !== undefined) {
362
- stack.push({ index: i, close });
363
- continue;
364
- }
365
- const open = stack.at(-1);
366
- if (open !== undefined && tokens[i][0] === open.close) {
367
- stack.pop();
368
- ends.set(open.index, i);
369
- }
370
- }
371
- return ends;
372
- }
373
422
  /** @param {CSSToken[]} tokens @param {number} start @param {number} end */
374
423
  function customProperty(tokens, start, end) {
375
424
  /** @type {CSSToken | null} */
@@ -388,88 +437,88 @@ function customProperty(tokens, start, end) {
388
437
  ? { decoded, raw: found[1], index: foundIndex }
389
438
  : null;
390
439
  }
391
- /** @param {CSSToken[]} tokens @param {number} start @param {number} end @param {Map<number, number>} ends @return {Component[]} */
392
- function componentTree(tokens, start, end, ends) {
393
- /** @type {Component[]} */ const tree = [];
394
- /** @param {Component} part */
395
- const push = (part) => {
396
- if (typeof part === 'string' && typeof tree.at(-1) === 'string')
397
- tree[tree.length - 1] += part;
398
- else tree.push(part);
399
- };
400
- for (let i = start; i < end; i++) {
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
+ }
463
+
401
464
  const token = tokens[i];
402
- const close = ends.get(i) ?? -1;
465
+ const close = input.index.closeOf(i, end);
403
466
  if (close === -1) {
404
- push(token[1]);
467
+ pushComponent(tree, token[1]);
468
+ i++;
405
469
  continue;
406
470
  }
471
+
407
472
  const isMathFunction =
408
473
  token[0] === CssType.Function &&
409
- (MATCH_CALC.test(token[4].value) ||
474
+ (isCalculationFunction(token[4].value) ||
410
475
  isSupportedMathFunction(token[4].value));
411
476
  if (isMathFunction) {
412
477
  try {
413
- push(parseRange(tokens, i, close + 1, ends));
478
+ pushComponent(tree, parseRange(input, i, close + 1));
414
479
  } catch {
415
- push(rawTokens(tokens, i, close + 1));
480
+ pushComponent(tree, rawTokens(tokens, i, close + 1));
416
481
  }
417
- } else {
418
- push(token[1]);
419
- push(componentTree(tokens, i + 1, close, ends));
420
- push(tokens[close][1]);
482
+ i = close + 1;
483
+ continue;
421
484
  }
422
- i = close;
485
+
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++;
423
494
  }
424
- return tree;
495
+ return root;
425
496
  }
426
- /** @param {Parser} p @param {string} name @param {string} rawName @return {Node} */
427
- function parseVar(p, name, rawName) {
428
- const { start, close, tokens, ends } = p.functionRange();
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);
429
502
  if (close === -1)
430
- throw new Error(`Unclosed ${name}( at position ${p.eofPosition()}`);
431
- const comma = firstComma(tokens, start, close, ends);
503
+ throw new Error(
504
+ `Unclosed ${name}( at position ${eofPositionAt(input, cursor.index)}`
505
+ );
506
+ const comma = input.index.firstTopLevelComma(start, close);
432
507
  const property = customProperty(tokens, start, comma === -1 ? close : comma);
433
508
  if (!property)
434
509
  throw new Error(
435
- `Invalid custom property in ${name}() at position ${tokens[start]?.[2] ?? p.eofPosition()}`
510
+ `Invalid custom property in ${name}() at position ${tokens[start]?.[2] ?? eofPositionAt(input, cursor.index)}`
436
511
  );
437
- p.consumeThrough(close + 1);
438
- const node = call(
439
- name,
440
- [ident(property.decoded, sourceSpelling(property.raw, property.decoded))],
441
- sourceSpelling(rawName, name)
442
- );
512
+ cursor.skipTo(close + 1);
513
+ /** @type {OpaqueComponent[]} */
514
+ const components = [
515
+ ident(property.decoded, sourceSpelling(property.raw, property.decoded)),
516
+ ];
443
517
  if (comma !== -1)
444
- setComponents(node, componentTree(tokens, property.index + 1, close, ends));
445
- return node;
518
+ components.push(...componentTree(input, property.index + 1, close));
519
+ return opaqueCall(name, components, sourceSpelling(rawName, name));
446
520
  }
447
521
 
448
- /** @type {Record<string, PrefixParselet>} */
449
- const PREFIX = {
450
- number: (_p, t) => num(/** @type {number} */ (t.value)),
451
- dimension: (_p, t) => {
452
- const unit = /** @type {string} */ (t.unit).toLowerCase();
453
- return dim(
454
- /** @type {number} */ (t.value),
455
- unit,
456
- baseOf(unit) || t.rawUnit === unit ? undefined : t.rawUnit
457
- );
458
- },
459
- ident: (_p, t) => {
460
- const name = String(t.value);
461
- return foldCalcKeyword(name) ?? ident(name, sourceSpelling(t.raw, name));
462
- },
463
- function: parseCall,
464
- '(': (p) => {
465
- const e = p.parseExpr(0);
466
- p.expectPunct(')');
467
- return e.type === 'Sum' ? { ...e, grouped: true } : e;
468
- },
469
- '-': (p) => negate(p.parseExpr(7)),
470
- '+': (p) => p.parseExpr(7),
471
- };
472
-
473
522
  /** @type {Record<string, {lbp: number}>} */
474
523
  const INFIX = {
475
524
  '+': { lbp: ADD_BP },
@@ -479,16 +528,23 @@ const INFIX = {
479
528
  };
480
529
 
481
530
  /**
482
- * @param {CSSToken[]} tokens
531
+ * @param {ParseInput} input
483
532
  * @param {number} start
484
533
  * @param {number} end
485
- * @param {Map<number, number>} ends
486
534
  * @return {Node}
487
535
  */
488
- function parseRange(tokens, start, end, ends) {
489
- const p = new Parser(tokens, start, end, ends);
490
- const ast = p.parseExpr(0);
491
- const trailing = p.peek();
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);
492
548
  if (trailing.type !== 'eof')
493
549
  throw new Error(
494
550
  `Unexpected token "${trailing.raw}" at position ${trailing.pos}`
@@ -496,10 +552,9 @@ function parseRange(tokens, start, end, ends) {
496
552
  return ast;
497
553
  }
498
554
 
499
- /** @param {CSSToken[]} tokens @param {number} [start] @param {number} [end] @return {Node} */
500
- function parse(tokens, start = 0, end = tokens.length) {
501
- const ends = blockEnds(tokens, start, end);
502
- return parseRange(tokens, start, end, ends);
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);
503
558
  }
504
559
 
505
560
  export { parse };