postcss-calc 11.2.1 → 11.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,242 @@
1
+ // Token scanning and cursor navigation for the Pratt parser.
2
+ import { TokenType as CssType } from '@csstools/css-tokenizer';
3
+ import { CSS_NUMBER_PREFIX } from '../regex.js';
4
+
5
+ /** @typedef {import('@csstools/css-tokenizer').CSSToken} CSSToken */
6
+ /** @typedef {ReturnType<typeof import('../block-index.js').indexBlocks>} BlockIndex */
7
+ /** @typedef {{raw: string, pos: number, ws: boolean, index: number}} TokenBase */
8
+ /** @typedef {TokenBase & {type: 'number', value: number, signCharacter?: '+' | '-'}} NumberToken */
9
+ /** @typedef {TokenBase & {type: 'dimension', value: number, unit: string, rawUnit: string, signCharacter?: '+' | '-'}} DimensionToken */
10
+ /** @typedef {TokenBase & {type: 'ident', value: string}} IdentToken */
11
+ /** @typedef {TokenBase & {type: 'function', value: string}} FunctionToken */
12
+ /** @typedef {'(' | ')' | ',' | '+' | '-' | '*' | '/'} Punctuator */
13
+ /** @typedef {TokenBase & {type: 'punct', value: Punctuator}} PunctToken */
14
+ /** @typedef {TokenBase & {type: 'eof', value: '', raw: ''}} EofToken */
15
+ /** @typedef {NumberToken | DimensionToken | IdentToken | FunctionToken | PunctToken | EofToken} Token */
16
+ /**
17
+ * Immutable bounds and shared block index for one parse range.
18
+ * @typedef {Readonly<{tokens: CSSToken[], end: number, index: BlockIndex}>} ParseInput
19
+ */
20
+
21
+ /** @param {string} value @return {value is '+' | '-' | '*' | '/'} */
22
+ function isOperator(value) {
23
+ return value === '+' || value === '-' || value === '*' || value === '/';
24
+ }
25
+
26
+ /** @param {string} raw @param {string} decoded */
27
+ function sourceSpelling(raw, decoded) {
28
+ return raw === decoded ? undefined : raw;
29
+ }
30
+
31
+ /**
32
+ * Mutable navigation state only. `index` is always the next native token
33
+ * position; trivia is intentionally left visible to `scanToken`.
34
+ */
35
+ class Cursor {
36
+ /** @param {number} start */
37
+ constructor(start) {
38
+ /** @type {number} */
39
+ this.index = start;
40
+ /** @type {boolean} */
41
+ this.firstToken = true;
42
+ /** @type {Token | null} */
43
+ this.lookahead = null;
44
+ /** @type {number} */
45
+ this.lookaheadNextIndex = start;
46
+ }
47
+
48
+ /** @param {number} index @return {void} */
49
+ skipTo(index) {
50
+ this.index = index;
51
+ this.lookaheadNextIndex = index;
52
+ this.firstToken = false;
53
+ this.lookahead = null;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Scan one token without consuming it. `firstToken` supplies the virtual
59
+ * leading trivia at a bounded parse boundary; all later whitespace state is
60
+ * derived from the native tokens encountered in this scan.
61
+ *
62
+ * @param {ParseInput} input
63
+ * @param {Cursor} cursor
64
+ * @return {Token}
65
+ */
66
+ function scanToken(input, cursor) {
67
+ let i = cursor.index;
68
+ let ws = cursor.firstToken;
69
+ while (i < input.end) {
70
+ const native = input.tokens[i];
71
+ if (native[0] === CssType.Whitespace || native[0] === CssType.Comment) {
72
+ ws = true;
73
+ i++;
74
+ continue;
75
+ }
76
+ if (native[0] === CssType.EOF) break;
77
+ const token = normalizeToken(native, i, ws);
78
+ cursor.lookahead = token;
79
+ cursor.lookaheadNextIndex = i + 1;
80
+ return token;
81
+ }
82
+ /** @type {EofToken} */
83
+ const token = {
84
+ type: 'eof',
85
+ value: '',
86
+ raw: '',
87
+ pos: eofPositionAt(input, i),
88
+ ws,
89
+ index: i,
90
+ };
91
+ cursor.lookahead = token;
92
+ // Native EOF is a real token and is consumed past its array index. When
93
+ // the bounded range ends before native EOF, this is a virtual EOF and must
94
+ // remain at the range boundary.
95
+ cursor.lookaheadNextIndex =
96
+ i < input.end && input.tokens[i][0] === CssType.EOF ? i + 1 : input.end;
97
+ return token;
98
+ }
99
+
100
+ /** @param {CSSToken} t @param {number} index @param {boolean} ws @return {Token} */
101
+ function normalizeToken(t, index, ws) {
102
+ const [type, raw, pos, , detail] = t;
103
+ switch (type) {
104
+ case CssType.Number:
105
+ return {
106
+ type: 'number',
107
+ value: detail.value,
108
+ raw,
109
+ pos,
110
+ ws,
111
+ index,
112
+ signCharacter: detail.signCharacter,
113
+ };
114
+ case CssType.Dimension: {
115
+ const match = CSS_NUMBER_PREFIX.exec(raw);
116
+ return {
117
+ type: 'dimension',
118
+ value: detail.value,
119
+ raw,
120
+ pos,
121
+ ws,
122
+ index,
123
+ unit: detail.unit,
124
+ rawUnit: match ? raw.slice(match[0].length) : detail.unit,
125
+ signCharacter: detail.signCharacter,
126
+ };
127
+ }
128
+ case CssType.Percentage:
129
+ return {
130
+ type: 'dimension',
131
+ value: detail.value,
132
+ raw,
133
+ pos,
134
+ ws,
135
+ index,
136
+ unit: '%',
137
+ rawUnit: '%',
138
+ signCharacter: detail.signCharacter,
139
+ };
140
+ case CssType.Ident:
141
+ case CssType.Function:
142
+ return {
143
+ type: type === CssType.Ident ? 'ident' : 'function',
144
+ value: detail.value,
145
+ raw,
146
+ pos,
147
+ ws,
148
+ index,
149
+ };
150
+ case CssType.OpenParen:
151
+ return { type: 'punct', value: '(', raw, pos, ws, index };
152
+ case CssType.CloseParen:
153
+ return { type: 'punct', value: ')', raw, pos, ws, index };
154
+ case CssType.Comma:
155
+ return { type: 'punct', value: ',', raw, pos, ws, index };
156
+ case CssType.Delim:
157
+ if (isOperator(detail.value))
158
+ return {
159
+ type: 'punct',
160
+ value: detail.value,
161
+ raw,
162
+ pos,
163
+ ws,
164
+ index,
165
+ };
166
+ }
167
+ throw new Error(`Unexpected character "${raw[0] ?? ''}" at position ${pos}`);
168
+ }
169
+
170
+ /** @param {ParseInput} input @param {number} index @return {number} */
171
+ function eofPositionAt(input, index) {
172
+ if (index < input.end && input.tokens[index][0] !== CssType.EOF) {
173
+ return input.tokens[index][2];
174
+ }
175
+ if (
176
+ input.end < input.tokens.length &&
177
+ input.tokens[input.end][0] !== CssType.EOF
178
+ ) {
179
+ return input.tokens[input.end][2];
180
+ }
181
+ for (let i = Math.min(input.end, input.tokens.length) - 1; i >= 0; i--) {
182
+ const token = input.tokens[i];
183
+ if (token[0] !== CssType.EOF) return token[3] + 1;
184
+ }
185
+ return 0;
186
+ }
187
+
188
+ /** @param {ParseInput} input @param {Cursor} cursor @return {Token} */
189
+ function peekToken(input, cursor) {
190
+ return cursor.lookahead ?? scanToken(input, cursor);
191
+ }
192
+
193
+ /**
194
+ * Consume the cached token and advance to its native next index. This is one
195
+ * of the only two operations allowed to advance `cursor.index`.
196
+ * @param {ParseInput} input
197
+ * @param {Cursor} cursor
198
+ * @return {Token}
199
+ */
200
+ function takeToken(input, cursor) {
201
+ const token = peekToken(input, cursor);
202
+ cursor.index = cursor.lookaheadNextIndex;
203
+ cursor.firstToken = false;
204
+ cursor.lookahead = null;
205
+ return token;
206
+ }
207
+
208
+ /** @param {ParseInput} input @param {Cursor} cursor @param {Punctuator} value @param {Punctuator} [value2] @return {boolean} */
209
+ function isPunct(input, cursor, value, value2) {
210
+ const t = peekToken(input, cursor);
211
+ return (
212
+ t.type === 'punct' &&
213
+ (t.value === value || (value2 !== undefined && t.value === value2))
214
+ );
215
+ }
216
+
217
+ /** @param {ParseInput} input @param {Cursor} cursor @param {Punctuator} value @return {boolean} */
218
+ function matchPunct(input, cursor, value) {
219
+ if (!isPunct(input, cursor, value)) return false;
220
+ takeToken(input, cursor);
221
+ return true;
222
+ }
223
+
224
+ /** @param {ParseInput} input @param {Cursor} cursor @param {Punctuator} value @return {PunctToken} */
225
+ function expectPunct(input, cursor, value) {
226
+ const t = takeToken(input, cursor);
227
+ if (t.type !== 'punct' || t.value !== value) {
228
+ throw new Error(`Expected ${value} at position ${t.pos}, got "${t.value}"`);
229
+ }
230
+ return t;
231
+ }
232
+
233
+ export {
234
+ Cursor,
235
+ eofPositionAt,
236
+ expectPunct,
237
+ isPunct,
238
+ matchPunct,
239
+ peekToken,
240
+ sourceSpelling,
241
+ takeToken,
242
+ };
package/src/lib/parser.js CHANGED
@@ -1,33 +1,33 @@
1
1
  // Pratt parser over native @csstools/css-tokenizer tokens.
2
- import { TokenType as CssType } from '@csstools/css-tokenizer';
3
2
  import { baseOf } from './convertUnits.js';
4
- import { call, dim, ident, mkProduct, mkSum, num, opaqueCall } from './node.js';
3
+ import {
4
+ call,
5
+ dim,
6
+ ident,
7
+ groupSubstitution,
8
+ mkProduct,
9
+ mkSum,
10
+ num,
11
+ } from './node.js';
5
12
  import { isCalculationFunction, isSupportedMathFunction } from './functions.js';
6
13
  import { assertDepth } from './limits.js';
7
- import { CSS_NUMBER_PREFIX } from './regex.js';
14
+ import { parseOpaqueCall, parseVar } from './parser/opaque.js';
15
+ import {
16
+ Cursor,
17
+ expectPunct,
18
+ isPunct,
19
+ matchPunct,
20
+ peekToken,
21
+ sourceSpelling,
22
+ takeToken,
23
+ } from './parser/tokens.js';
8
24
 
9
25
  /** @typedef {import('@csstools/css-tokenizer').CSSToken} CSSToken */
10
26
  /** @typedef {import('./node.js').Node} Node */
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 */
22
- /**
23
- * Immutable bounds and shared block index for one parse range.
24
- * @typedef {Readonly<{tokens: CSSToken[], end: number, index: BlockIndex}>} ParseInput
25
- */
26
-
27
- /** @param {string} value @return {value is '+' | '-' | '*' | '/'} */
28
- function isOperator(value) {
29
- return value === '+' || value === '-' || value === '*' || value === '/';
30
- }
27
+ /** @typedef {import('./parser/tokens.js').BlockIndex} BlockIndex */
28
+ /** @typedef {import('./parser/tokens.js').ParseInput} ParseInput */
29
+ /** @typedef {import('./parser/tokens.js').Token} Token */
30
+ /** @typedef {import('./parser/tokens.js').FunctionToken} FunctionToken */
31
31
 
32
32
  /**
33
33
  * CSS numeric tokens do not retain a signed-zero distinction. Keep that
@@ -41,220 +41,6 @@ function normalizeSourceZero(value) {
41
41
  return value === 0 ? 0 : value;
42
42
  }
43
43
 
44
- /** @param {string} raw @param {string} decoded */
45
- function sourceSpelling(raw, decoded) {
46
- return raw === decoded ? undefined : raw;
47
- }
48
-
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;
64
- }
65
-
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;
72
- }
73
- }
74
-
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
- }
81
-
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;
100
- }
101
- if (native[0] === CssType.EOF) break;
102
- const token = normalizeToken(native, i, ws);
103
- cursor.lookahead = token;
104
- cursor.lookaheadNextIndex = i + 1;
105
- return token;
106
- }
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;
123
- }
124
-
125
- /** @param {CSSToken} t @param {number} index @param {boolean} ws @return {Token} */
126
- function normalizeToken(t, index, ws) {
127
- const [type, raw, pos, , detail] = t;
128
- switch (type) {
129
- case CssType.Number:
130
- return {
131
- type: 'number',
132
- value: detail.value,
133
- raw,
134
- pos,
135
- ws,
136
- index,
137
- signCharacter: detail.signCharacter,
138
- };
139
- case CssType.Dimension: {
140
- const match = CSS_NUMBER_PREFIX.exec(raw);
141
- return {
142
- type: 'dimension',
143
- value: detail.value,
144
- raw,
145
- pos,
146
- ws,
147
- index,
148
- unit: detail.unit,
149
- rawUnit: match ? raw.slice(match[0].length) : detail.unit,
150
- signCharacter: detail.signCharacter,
151
- };
152
- }
153
- case CssType.Percentage:
154
- return {
155
- type: 'dimension',
156
- value: detail.value,
157
- raw,
158
- pos,
159
- ws,
160
- index,
161
- unit: '%',
162
- rawUnit: '%',
163
- signCharacter: detail.signCharacter,
164
- };
165
- case CssType.Ident:
166
- case CssType.Function:
167
- return {
168
- type: type === CssType.Ident ? 'ident' : 'function',
169
- value: detail.value,
170
- raw,
171
- pos,
172
- ws,
173
- index,
174
- };
175
- case CssType.OpenParen:
176
- return { type: 'punct', value: '(', raw, pos, ws, index };
177
- case CssType.CloseParen:
178
- return { type: 'punct', value: ')', raw, pos, ws, index };
179
- case CssType.Comma:
180
- return { type: 'punct', value: ',', raw, pos, ws, index };
181
- case CssType.Delim:
182
- if (isOperator(detail.value))
183
- return {
184
- type: 'punct',
185
- value: detail.value,
186
- raw,
187
- pos,
188
- ws,
189
- index,
190
- };
191
- }
192
- throw new Error(`Unexpected character "${raw[0] ?? ''}" at position ${pos}`);
193
- }
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
44
  /** @param {ParseInput} input @param {Cursor} cursor @param {Token} token @param {number} depth @return {Node} */
259
45
  function parsePrefix(input, cursor, token, depth) {
260
46
  switch (token.type) {
@@ -280,9 +66,7 @@ function parsePrefix(input, cursor, token, depth) {
280
66
  case '(': {
281
67
  const expression = parseExpr(input, cursor, 0, depth + 1);
282
68
  expectPunct(input, cursor, ')');
283
- return expression.type === 'Sum'
284
- ? { ...expression, grouped: true }
285
- : expression;
69
+ return groupSubstitution(expression);
286
70
  }
287
71
  }
288
72
  // No unary `+`/`-` production exists in the <calc-value> grammar; a
@@ -308,10 +92,10 @@ function parseExpr(input, cursor, minBp = 0, depth = 0) {
308
92
  `"${nxt.signCharacter}" must be surrounded by whitespace at position ${nxt.pos}`
309
93
  );
310
94
  }
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 === '-') {
95
+ if (nxt.type !== 'punct') break;
96
+ const op = nxt.value;
97
+ if (op === '+' || op === '-') {
98
+ if (ADD_BP < minBp) break;
315
99
  /** @type {import('./node.js').SumTerm[]} */
316
100
  const terms = [{ sign: /** @type {1} */ (1), node: left }];
317
101
  do {
@@ -325,7 +109,8 @@ function parseExpr(input, cursor, minBp = 0, depth = 0) {
325
109
  left = mkSum(terms);
326
110
  continue;
327
111
  }
328
- if (infixKey === '*' || infixKey === '/') {
112
+ if (op === '*' || op === '/') {
113
+ if (MUL_BP < minBp) break;
329
114
  /** @type {import('./node.js').ProductFactor[]} */
330
115
  const factors = [{ exponent: /** @type {1} */ (1), node: left }];
331
116
  do {
@@ -361,22 +146,6 @@ function foldCalcKeyword(name) {
361
146
 
362
147
  const ADD_BP = 1;
363
148
  const MUL_BP = 3;
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);
368
- if (close === -1)
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)
377
- );
378
- }
379
-
380
149
  /** @param {ParseInput} input @param {Cursor} cursor @param {Token} token */
381
150
  function requireSurroundingWs(input, cursor, token) {
382
151
  if (!token.ws || !peekToken(input, cursor).ws)
@@ -390,9 +159,9 @@ function parseCall(input, cursor, t, depth) {
390
159
  const name = t.value;
391
160
  const rawName = t.raw.slice(0, -1);
392
161
  if (name.toLowerCase() === 'var')
393
- return parseVar(input, cursor, t, name, rawName);
162
+ return parseVar(input, cursor, t, name, rawName, parseRange);
394
163
  if (!isCalculationFunction(name) && !isSupportedMathFunction(name))
395
- return parseOpaqueCall(input, cursor, t, name, rawName);
164
+ return parseOpaqueCall(input, cursor, t, name, rawName, parseRange);
396
165
  /** @type {Node[]} */ const args = [];
397
166
  if (!isPunct(input, cursor, ')')) {
398
167
  args.push(parseExpr(input, cursor, 0, depth + 1));
@@ -403,120 +172,6 @@ function parseCall(input, cursor, t, depth) {
403
172
  return call(name, args, sourceSpelling(rawName, name));
404
173
  }
405
174
 
406
- /** @param {CSSToken[]} tokens @param {number} start @param {number} end */
407
- function rawTokens(tokens, start, end) {
408
- let raw = '';
409
- for (let i = start; i < end; i++) raw += tokens[i][1];
410
- return raw;
411
- }
412
- /** @param {CSSToken[]} tokens @param {number} start @param {number} end */
413
- function customProperty(tokens, start, end) {
414
- /** @type {CSSToken | null} */
415
- let found = null;
416
- let foundIndex = -1;
417
- for (let i = start; i < end; i++) {
418
- const type = tokens[i][0];
419
- if (type === CssType.Whitespace || type === CssType.Comment) continue;
420
- if (found !== null) return null;
421
- found = tokens[i];
422
- foundIndex = i;
423
- }
424
- if (!found || found[0] !== CssType.Ident) return null;
425
- const decoded = found[4].value;
426
- return decoded.startsWith('--') && decoded !== '--'
427
- ? { decoded, raw: found[1], index: foundIndex }
428
- : null;
429
- }
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
-
454
- const token = tokens[i];
455
- const close = input.index.closeOf(i, end);
456
- if (close === -1) {
457
- pushComponent(tree, token[1]);
458
- i++;
459
- continue;
460
- }
461
-
462
- const isMathFunction =
463
- token[0] === CssType.Function &&
464
- (isCalculationFunction(token[4].value) ||
465
- isSupportedMathFunction(token[4].value));
466
- if (isMathFunction) {
467
- try {
468
- pushComponent(tree, parseRange(input, i, close + 1));
469
- } catch {
470
- pushComponent(tree, rawTokens(tokens, i, close + 1));
471
- }
472
- i = close + 1;
473
- continue;
474
- }
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++;
484
- }
485
- return root;
486
- }
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);
492
- if (close === -1)
493
- throw new Error(
494
- `Unclosed ${name}( at position ${eofPositionAt(input, cursor.index)}`
495
- );
496
- const comma = input.index.firstTopLevelComma(start, close);
497
- const property = customProperty(tokens, start, comma === -1 ? close : comma);
498
- if (!property)
499
- throw new Error(
500
- `Invalid custom property in ${name}() at position ${tokens[start]?.[2] ?? eofPositionAt(input, cursor.index)}`
501
- );
502
- cursor.skipTo(close + 1);
503
- /** @type {OpaqueComponent[]} */
504
- const components = [
505
- ident(property.decoded, sourceSpelling(property.raw, property.decoded)),
506
- ];
507
- if (comma !== -1)
508
- components.push(...componentTree(input, property.index + 1, close));
509
- return opaqueCall(name, components, sourceSpelling(rawName, name));
510
- }
511
-
512
- /** @type {Record<string, {lbp: number}>} */
513
- const INFIX = {
514
- '+': { lbp: ADD_BP },
515
- '-': { lbp: ADD_BP },
516
- '*': { lbp: MUL_BP },
517
- '/': { lbp: MUL_BP },
518
- };
519
-
520
175
  /**
521
176
  * @param {ParseInput} input
522
177
  * @param {number} start