postcss-calc 11.1.2 → 11.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/lib/parser.js CHANGED
@@ -1,227 +1,129 @@
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 { call, dim, ident, mkProduct, mkSum, num, opaqueCall } from './node.js';
5
+ import { isCalculationFunction, isSupportedMathFunction } from './functions.js';
6
+ import { assertDepth } from './limits.js';
7
+ import { CSS_NUMBER_PREFIX } from './regex.js';
7
8
 
8
9
  /** @typedef {import('@csstools/css-tokenizer').CSSToken} CSSToken */
9
10
  /** @typedef {import('./node.js').Node} Node */
10
- /** @typedef {string | Node | Component[]} Component */
11
+ /** @typedef {import('./node.js').OpaqueComponent} OpaqueComponent */
12
+ /** @typedef {ReturnType<typeof import('./block-index.js').indexBlocks>} BlockIndex */
13
+ /** @typedef {{raw: string, pos: number, ws: boolean, index: number}} TokenBase */
14
+ /** @typedef {TokenBase & {type: 'number', value: number, signCharacter?: '+' | '-'}} NumberToken */
15
+ /** @typedef {TokenBase & {type: 'dimension', value: number, unit: string, rawUnit: string, signCharacter?: '+' | '-'}} DimensionToken */
16
+ /** @typedef {TokenBase & {type: 'ident', value: string}} IdentToken */
17
+ /** @typedef {TokenBase & {type: 'function', value: string}} FunctionToken */
18
+ /** @typedef {'(' | ')' | ',' | '+' | '-' | '*' | '/'} Punctuator */
19
+ /** @typedef {TokenBase & {type: 'punct', value: Punctuator}} PunctToken */
20
+ /** @typedef {TokenBase & {type: 'eof', value: '', raw: ''}} EofToken */
21
+ /** @typedef {NumberToken | DimensionToken | IdentToken | FunctionToken | PunctToken | EofToken} Token */
11
22
  /**
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
23
+ * Immutable bounds and shared block index for one parse range.
24
+ * @typedef {Readonly<{tokens: CSSToken[], end: number, index: BlockIndex}>} ParseInput
21
25
  */
22
- /** @typedef {(p: Parser, token: Token) => Node} PrefixParselet */
23
26
 
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
- ]);
27
+ /** @param {string} value @return {value is '+' | '-' | '*' | '/'} */
28
+ function isOperator(value) {
29
+ return value === '+' || value === '-' || value === '*' || value === '/';
30
+ }
31
+
32
+ /**
33
+ * CSS numeric tokens do not retain a signed-zero distinction. Keep that
34
+ * normalization at the source boundary so a later IEEE-754 `-0` can only
35
+ * have been introduced by calculation evaluation.
36
+ *
37
+ * @param {number} value
38
+ * @return {number}
39
+ */
40
+ function normalizeSourceZero(value) {
41
+ return value === 0 ? 0 : value;
42
+ }
43
+
32
44
  /** @param {string} raw @param {string} decoded */
33
45
  function sourceSpelling(raw, decoded) {
34
46
  return raw === decoded ? undefined : raw;
35
47
  }
36
48
 
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;
49
+ /**
50
+ * Mutable navigation state only. `index` is always the next native token
51
+ * position; trivia is intentionally left visible to `scanToken`.
52
+ */
53
+ class Cursor {
54
+ /** @param {number} start */
55
+ constructor(start) {
56
+ /** @type {number} */
57
+ this.index = start;
58
+ /** @type {boolean} */
59
+ this.firstToken = true;
60
+ /** @type {Token | null} */
61
+ this.lookahead = null;
62
+ /** @type {number} */
63
+ this.lookaheadNextIndex = start;
65
64
  }
66
65
 
67
- /** @return {Map<number, number>} */
68
- get ends() {
69
- return this.#ends;
66
+ /** @param {number} index @return {void} */
67
+ skipTo(index) {
68
+ this.index = index;
69
+ this.lookaheadNextIndex = index;
70
+ this.firstToken = false;
71
+ this.lookahead = null;
70
72
  }
73
+ }
71
74
 
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
- }
75
+ /** @param {OpaqueComponent[]} target @param {OpaqueComponent} part */
76
+ function pushComponent(target, part) {
77
+ if (typeof part === 'string' && typeof target.at(-1) === 'string')
78
+ target[target.length - 1] += part;
79
+ else target.push(part);
80
+ }
89
81
 
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);
82
+ /**
83
+ * Scan one token without consuming it. `firstToken` supplies the virtual
84
+ * leading trivia at a bounded parse boundary; all later whitespace state is
85
+ * derived from the native tokens encountered in this scan.
86
+ *
87
+ * @param {ParseInput} input
88
+ * @param {Cursor} cursor
89
+ * @return {Token}
90
+ */
91
+ function scanToken(input, cursor) {
92
+ let i = cursor.index;
93
+ let ws = cursor.firstToken;
94
+ while (i < input.end) {
95
+ const native = input.tokens[i];
96
+ if (native[0] === CssType.Whitespace || native[0] === CssType.Comment) {
97
+ ws = true;
98
+ i++;
99
+ continue;
102
100
  }
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;
101
+ if (native[0] === CssType.EOF) break;
102
+ const token = normalizeToken(native, i, ws);
103
+ cursor.lookahead = token;
104
+ cursor.lookaheadNextIndex = i + 1;
122
105
  return token;
123
106
  }
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
- }
107
+ /** @type {EofToken} */
108
+ const token = {
109
+ type: 'eof',
110
+ value: '',
111
+ raw: '',
112
+ pos: eofPositionAt(input, i),
113
+ ws,
114
+ index: i,
115
+ };
116
+ cursor.lookahead = token;
117
+ // Native EOF is a real token and is consumed past its array index. When
118
+ // the bounded range ends before native EOF, this is a virtual EOF and must
119
+ // remain at the range boundary.
120
+ cursor.lookaheadNextIndex =
121
+ i < input.end && input.tokens[i][0] === CssType.EOF ? i + 1 : input.end;
122
+ return token;
221
123
  }
222
124
 
223
- /** @param {CSSToken} t @param {boolean} ws @return {Token} */
224
- function normalizeToken(t, ws) {
125
+ /** @param {CSSToken} t @param {number} index @param {boolean} ws @return {Token} */
126
+ function normalizeToken(t, index, ws) {
225
127
  const [type, raw, pos, , detail] = t;
226
128
  switch (type) {
227
129
  case CssType.Number:
@@ -231,16 +133,18 @@ function normalizeToken(t, ws) {
231
133
  raw,
232
134
  pos,
233
135
  ws,
136
+ index,
234
137
  signCharacter: detail.signCharacter,
235
138
  };
236
139
  case CssType.Dimension: {
237
- const match = NUMERIC_RAW.exec(raw);
140
+ const match = CSS_NUMBER_PREFIX.exec(raw);
238
141
  return {
239
142
  type: 'dimension',
240
143
  value: detail.value,
241
144
  raw,
242
145
  pos,
243
146
  ws,
147
+ index,
244
148
  unit: detail.unit,
245
149
  rawUnit: match ? raw.slice(match[0].length) : detail.unit,
246
150
  signCharacter: detail.signCharacter,
@@ -253,6 +157,7 @@ function normalizeToken(t, ws) {
253
157
  raw,
254
158
  pos,
255
159
  ws,
160
+ index,
256
161
  unit: '%',
257
162
  rawUnit: '%',
258
163
  signCharacter: detail.signCharacter,
@@ -265,18 +170,179 @@ function normalizeToken(t, ws) {
265
170
  raw,
266
171
  pos,
267
172
  ws,
173
+ index,
268
174
  };
269
175
  case CssType.OpenParen:
176
+ return { type: 'punct', value: '(', raw, pos, ws, index };
270
177
  case CssType.CloseParen:
178
+ return { type: 'punct', value: ')', raw, pos, ws, index };
271
179
  case CssType.Comma:
272
- return { type: 'punct', value: raw, raw, pos, ws };
180
+ return { type: 'punct', value: ',', raw, pos, ws, index };
273
181
  case CssType.Delim:
274
- if (PUNCT_DELIMS.has(detail.value))
275
- return { type: 'punct', value: detail.value, raw, pos, ws };
182
+ if (isOperator(detail.value))
183
+ return {
184
+ type: 'punct',
185
+ value: detail.value,
186
+ raw,
187
+ pos,
188
+ ws,
189
+ index,
190
+ };
276
191
  }
277
192
  throw new Error(`Unexpected character "${raw[0] ?? ''}" at position ${pos}`);
278
193
  }
279
194
 
195
+ /** @param {ParseInput} input @param {number} index @return {number} */
196
+ function eofPositionAt(input, index) {
197
+ if (index < input.end && input.tokens[index][0] !== CssType.EOF) {
198
+ return input.tokens[index][2];
199
+ }
200
+ if (
201
+ input.end < input.tokens.length &&
202
+ input.tokens[input.end][0] !== CssType.EOF
203
+ ) {
204
+ return input.tokens[input.end][2];
205
+ }
206
+ for (let i = Math.min(input.end, input.tokens.length) - 1; i >= 0; i--) {
207
+ const token = input.tokens[i];
208
+ if (token[0] !== CssType.EOF) return token[3] + 1;
209
+ }
210
+ return 0;
211
+ }
212
+
213
+ /** @param {ParseInput} input @param {Cursor} cursor @return {Token} */
214
+ function peekToken(input, cursor) {
215
+ return cursor.lookahead ?? scanToken(input, cursor);
216
+ }
217
+
218
+ /**
219
+ * Consume the cached token and advance to its native next index. This is one
220
+ * of the only two operations allowed to advance `cursor.index`.
221
+ * @param {ParseInput} input
222
+ * @param {Cursor} cursor
223
+ * @return {Token}
224
+ */
225
+ function takeToken(input, cursor) {
226
+ const token = peekToken(input, cursor);
227
+ cursor.index = cursor.lookaheadNextIndex;
228
+ cursor.firstToken = false;
229
+ cursor.lookahead = null;
230
+ return token;
231
+ }
232
+
233
+ /** @param {ParseInput} input @param {Cursor} cursor @param {Punctuator} value @param {Punctuator} [value2] @return {boolean} */
234
+ function isPunct(input, cursor, value, value2) {
235
+ const t = peekToken(input, cursor);
236
+ return (
237
+ t.type === 'punct' &&
238
+ (t.value === value || (value2 !== undefined && t.value === value2))
239
+ );
240
+ }
241
+
242
+ /** @param {ParseInput} input @param {Cursor} cursor @param {Punctuator} value @return {boolean} */
243
+ function matchPunct(input, cursor, value) {
244
+ if (!isPunct(input, cursor, value)) return false;
245
+ takeToken(input, cursor);
246
+ return true;
247
+ }
248
+
249
+ /** @param {ParseInput} input @param {Cursor} cursor @param {Punctuator} value @return {PunctToken} */
250
+ function expectPunct(input, cursor, value) {
251
+ const t = takeToken(input, cursor);
252
+ if (t.type !== 'punct' || t.value !== value) {
253
+ throw new Error(`Expected ${value} at position ${t.pos}, got "${t.value}"`);
254
+ }
255
+ return t;
256
+ }
257
+
258
+ /** @param {ParseInput} input @param {Cursor} cursor @param {Token} token @param {number} depth @return {Node} */
259
+ function parsePrefix(input, cursor, token, depth) {
260
+ switch (token.type) {
261
+ case 'number':
262
+ return num(normalizeSourceZero(token.value));
263
+ case 'dimension': {
264
+ const unit = token.unit.toLowerCase();
265
+ return dim(
266
+ normalizeSourceZero(token.value),
267
+ unit,
268
+ baseOf(unit) || token.rawUnit === unit ? undefined : token.rawUnit
269
+ );
270
+ }
271
+ case 'ident':
272
+ return (
273
+ foldCalcKeyword(token.value) ??
274
+ ident(token.value, sourceSpelling(token.raw, token.value))
275
+ );
276
+ case 'function':
277
+ return parseCall(input, cursor, token, depth);
278
+ case 'punct':
279
+ switch (token.value) {
280
+ case '(': {
281
+ const expression = parseExpr(input, cursor, 0, depth + 1);
282
+ expectPunct(input, cursor, ')');
283
+ return expression.type === 'Sum'
284
+ ? { ...expression, grouped: true }
285
+ : expression;
286
+ }
287
+ }
288
+ // No unary `+`/`-` production exists in the <calc-value> grammar; a
289
+ // sign is only valid inside a number/dimension token or as a binary
290
+ // operator. `-(...)` therefore fails to parse and is preserved.
291
+ }
292
+ throw new Error(`Unexpected token "${token.raw}" at position ${token.pos}`);
293
+ }
294
+
295
+ /** @param {ParseInput} input @param {Cursor} cursor @param {number} minBp @param {number} depth @return {Node} */
296
+ function parseExpr(input, cursor, minBp = 0, depth = 0) {
297
+ assertDepth(depth);
298
+ const t = takeToken(input, cursor);
299
+ let left = parsePrefix(input, cursor, t, depth);
300
+
301
+ while (true) {
302
+ const nxt = peekToken(input, cursor);
303
+ if (
304
+ (nxt.type === 'number' || nxt.type === 'dimension') &&
305
+ nxt.signCharacter !== undefined
306
+ ) {
307
+ throw new Error(
308
+ `"${nxt.signCharacter}" must be surrounded by whitespace at position ${nxt.pos}`
309
+ );
310
+ }
311
+ const infixKey = nxt.type === 'punct' ? String(nxt.value) : nxt.type;
312
+ const rule = INFIX[infixKey];
313
+ if (!rule || rule.lbp < minBp) break;
314
+ if (infixKey === '+' || infixKey === '-') {
315
+ /** @type {import('./node.js').SumTerm[]} */
316
+ const terms = [{ sign: /** @type {1} */ (1), node: left }];
317
+ do {
318
+ const token = takeToken(input, cursor);
319
+ requireSurroundingWs(input, cursor, token);
320
+ terms.push({
321
+ sign: /** @type {1 | -1} */ (token.value === '+' ? 1 : -1),
322
+ node: parseExpr(input, cursor, ADD_BP + 1, depth),
323
+ });
324
+ } while (isPunct(input, cursor, '+', '-'));
325
+ left = mkSum(terms);
326
+ continue;
327
+ }
328
+ if (infixKey === '*' || infixKey === '/') {
329
+ /** @type {import('./node.js').ProductFactor[]} */
330
+ const factors = [{ exponent: /** @type {1} */ (1), node: left }];
331
+ do {
332
+ const token = takeToken(input, cursor);
333
+ factors.push({
334
+ exponent: /** @type {1 | -1} */ (token.value === '*' ? 1 : -1),
335
+ node: parseExpr(input, cursor, MUL_BP + 1, depth),
336
+ });
337
+ } while (isPunct(input, cursor, '*', '/'));
338
+ left = mkProduct(factors);
339
+ continue;
340
+ }
341
+ break;
342
+ }
343
+ return left;
344
+ }
345
+
280
346
  /** §10.9 — case-insensitive except for NaN. @param {string} name @return {Node | null} */
281
347
  function foldCalcKeyword(name) {
282
348
  if (name === 'NaN' || name === '-NaN') return num(Number.NaN);
@@ -295,41 +361,45 @@ function foldCalcKeyword(name) {
295
361
 
296
362
  const ADD_BP = 1;
297
363
  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();
364
+ /** @param {ParseInput} input @param {Cursor} cursor @param {FunctionToken} token @param {string} name @param {string} rawName @return {Node} */
365
+ function parseOpaqueCall(input, cursor, token, name, rawName) {
366
+ const start = token.index + 1;
367
+ const close = input.index.closeOf(token.index, input.end);
303
368
  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)
369
+ throw new Error(
370
+ `Unclosed ${name}( at position ${eofPositionAt(input, cursor.index)}`
371
+ );
372
+ cursor.skipTo(close + 1);
373
+ return opaqueCall(
374
+ name,
375
+ componentTree(input, start, close),
376
+ sourceSpelling(rawName, name)
309
377
  );
310
378
  }
311
379
 
312
- /** @param {Parser} p @param {Token} token */
313
- function requireSurroundingWs(p, token) {
314
- if (!token.ws || !p.peek().ws)
380
+ /** @param {ParseInput} input @param {Cursor} cursor @param {Token} token */
381
+ function requireSurroundingWs(input, cursor, token) {
382
+ if (!token.ws || !peekToken(input, cursor).ws)
315
383
  throw new Error(
316
384
  `"${token.value}" must be surrounded by whitespace at position ${token.pos}`
317
385
  );
318
386
  }
319
387
 
320
- /** @param {Parser} p @param {Token} t @return {Node} */
321
- function parseCall(p, t) {
322
- const name = String(t.value);
388
+ /** @param {ParseInput} input @param {Cursor} cursor @param {FunctionToken} t @param {number} depth @return {Node} */
389
+ function parseCall(input, cursor, t, depth) {
390
+ const name = t.value;
323
391
  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);
392
+ if (name.toLowerCase() === 'var')
393
+ return parseVar(input, cursor, t, name, rawName);
394
+ if (!isCalculationFunction(name) && !isSupportedMathFunction(name))
395
+ return parseOpaqueCall(input, cursor, t, name, rawName);
327
396
  /** @type {Node[]} */ const args = [];
328
- if (!p.isPunct(')')) {
329
- args.push(p.parseExpr(0));
330
- while (p.matchPunct(',')) args.push(p.parseExpr(0));
397
+ if (!isPunct(input, cursor, ')')) {
398
+ args.push(parseExpr(input, cursor, 0, depth + 1));
399
+ while (matchPunct(input, cursor, ','))
400
+ args.push(parseExpr(input, cursor, 0, depth + 1));
331
401
  }
332
- p.expectPunct(')');
402
+ expectPunct(input, cursor, ')');
333
403
  return call(name, args, sourceSpelling(rawName, name));
334
404
  }
335
405
 
@@ -339,37 +409,6 @@ function rawTokens(tokens, start, end) {
339
409
  for (let i = start; i < end; i++) raw += tokens[i][1];
340
410
  return raw;
341
411
  }
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
412
  /** @param {CSSToken[]} tokens @param {number} start @param {number} end */
374
413
  function customProperty(tokens, start, end) {
375
414
  /** @type {CSSToken | null} */
@@ -388,88 +427,88 @@ function customProperty(tokens, start, end) {
388
427
  ? { decoded, raw: found[1], index: foundIndex }
389
428
  : null;
390
429
  }
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++) {
430
+ /** @param {ParseInput} input @param {number} start @param {number} end @param {number} [depth] @return {OpaqueComponent[]} */
431
+ function componentTree(input, start, end, depth = 0) {
432
+ assertDepth(depth);
433
+ /** @type {OpaqueComponent[]} */ const root = [];
434
+ /** @type {OpaqueComponent[]} */ let tree = root;
435
+ /** @type {{parent: OpaqueComponent[], tree: OpaqueComponent[], close: number, end: number}[]} */
436
+ const frames = [];
437
+ const { tokens } = input;
438
+ let i = start;
439
+ while (true) {
440
+ if (i >= end) {
441
+ if (frames.length === 0) break;
442
+ const frame =
443
+ /** @type {{parent: OpaqueComponent[], tree: OpaqueComponent[], close: number, end: number}} */ (
444
+ frames.pop()
445
+ );
446
+ tree = frame.parent;
447
+ pushComponent(tree, frame.tree);
448
+ pushComponent(tree, tokens[frame.close][1]);
449
+ end = frame.end;
450
+ i = frame.close + 1;
451
+ continue;
452
+ }
453
+
401
454
  const token = tokens[i];
402
- const close = ends.get(i) ?? -1;
455
+ const close = input.index.closeOf(i, end);
403
456
  if (close === -1) {
404
- push(token[1]);
457
+ pushComponent(tree, token[1]);
458
+ i++;
405
459
  continue;
406
460
  }
461
+
407
462
  const isMathFunction =
408
463
  token[0] === CssType.Function &&
409
- (MATCH_CALC.test(token[4].value) ||
464
+ (isCalculationFunction(token[4].value) ||
410
465
  isSupportedMathFunction(token[4].value));
411
466
  if (isMathFunction) {
412
467
  try {
413
- push(parseRange(tokens, i, close + 1, ends));
468
+ pushComponent(tree, parseRange(input, i, close + 1));
414
469
  } catch {
415
- push(rawTokens(tokens, i, close + 1));
470
+ pushComponent(tree, rawTokens(tokens, i, close + 1));
416
471
  }
417
- } else {
418
- push(token[1]);
419
- push(componentTree(tokens, i + 1, close, ends));
420
- push(tokens[close][1]);
472
+ i = close + 1;
473
+ continue;
421
474
  }
422
- i = close;
475
+
476
+ pushComponent(tree, token[1]);
477
+ assertDepth(depth + frames.length + 1);
478
+ /** @type {OpaqueComponent[]} */
479
+ const child = [];
480
+ frames.push({ parent: tree, tree: child, close, end });
481
+ tree = child;
482
+ end = close;
483
+ i++;
423
484
  }
424
- return tree;
485
+ return root;
425
486
  }
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();
487
+ /** @param {ParseInput} input @param {Cursor} cursor @param {FunctionToken} token @param {string} name @param {string} rawName @return {Node} */
488
+ function parseVar(input, cursor, token, name, rawName) {
489
+ const { tokens } = input;
490
+ const start = token.index + 1;
491
+ const close = input.index.closeOf(token.index, input.end);
429
492
  if (close === -1)
430
- throw new Error(`Unclosed ${name}( at position ${p.eofPosition()}`);
431
- const comma = firstComma(tokens, start, close, ends);
493
+ throw new Error(
494
+ `Unclosed ${name}( at position ${eofPositionAt(input, cursor.index)}`
495
+ );
496
+ const comma = input.index.firstTopLevelComma(start, close);
432
497
  const property = customProperty(tokens, start, comma === -1 ? close : comma);
433
498
  if (!property)
434
499
  throw new Error(
435
- `Invalid custom property in ${name}() at position ${tokens[start]?.[2] ?? p.eofPosition()}`
500
+ `Invalid custom property in ${name}() at position ${tokens[start]?.[2] ?? eofPositionAt(input, cursor.index)}`
436
501
  );
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
- );
502
+ cursor.skipTo(close + 1);
503
+ /** @type {OpaqueComponent[]} */
504
+ const components = [
505
+ ident(property.decoded, sourceSpelling(property.raw, property.decoded)),
506
+ ];
443
507
  if (comma !== -1)
444
- setComponents(node, componentTree(tokens, property.index + 1, close, ends));
445
- return node;
508
+ components.push(...componentTree(input, property.index + 1, close));
509
+ return opaqueCall(name, components, sourceSpelling(rawName, name));
446
510
  }
447
511
 
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
512
  /** @type {Record<string, {lbp: number}>} */
474
513
  const INFIX = {
475
514
  '+': { lbp: ADD_BP },
@@ -479,16 +518,23 @@ const INFIX = {
479
518
  };
480
519
 
481
520
  /**
482
- * @param {CSSToken[]} tokens
521
+ * @param {ParseInput} input
483
522
  * @param {number} start
484
523
  * @param {number} end
485
- * @param {Map<number, number>} ends
486
524
  * @return {Node}
487
525
  */
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();
526
+ function parseRange(input, start, end) {
527
+ const bounded =
528
+ input.end === end
529
+ ? input
530
+ : /** @type {ParseInput} */ ({
531
+ tokens: input.tokens,
532
+ end,
533
+ index: input.index,
534
+ });
535
+ const cursor = new Cursor(start);
536
+ const ast = parseExpr(bounded, cursor, 0, 0);
537
+ const trailing = peekToken(bounded, cursor);
492
538
  if (trailing.type !== 'eof')
493
539
  throw new Error(
494
540
  `Unexpected token "${trailing.raw}" at position ${trailing.pos}`
@@ -496,10 +542,9 @@ function parseRange(tokens, start, end, ends) {
496
542
  return ast;
497
543
  }
498
544
 
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);
545
+ /** @param {CSSToken[]} tokens @param {number} start @param {number} end @param {BlockIndex} index @return {Node} */
546
+ function parse(tokens, start, end, index) {
547
+ return parseRange({ tokens, end, index }, start, end);
503
548
  }
504
549
 
505
550
  export { parse };