postcss-calc 11.1.0 → 11.1.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.
package/src/lib/parser.js CHANGED
@@ -1,82 +1,144 @@
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.
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
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';
5
7
 
8
+ /** @typedef {import('@csstools/css-tokenizer').CSSToken} CSSToken */
9
+ /** @typedef {import('./node.js').Node} Node */
10
+ /** @typedef {string | Node | Component[]} Component */
6
11
  /**
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
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
11
21
  */
22
+ /** @typedef {(p: Parser, token: Token) => Node} PrefixParselet */
12
23
 
13
- /**
14
- * §10.9 — case-insensitive except for NaN.
15
- * @param {string} name
16
- * @return {Node | null}
17
- */
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;
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
+ ]);
32
+ /** @param {string} raw @param {string} decoded */
33
+ function sourceSpelling(raw, decoded) {
34
+ return raw === decoded ? undefined : raw;
36
35
  }
37
36
 
37
+ /** Bounded cursor that skips trivia but records whether it preceded a token. */
38
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
+
39
52
  /**
40
- * @param {Token[]} tokens
53
+ * @param {CSSToken[]} tokens
54
+ * @param {number} start
55
+ * @param {number} end
56
+ * @param {Map<number, number>} [ends]
41
57
  */
42
- constructor(tokens) {
43
- /** @private */
44
- this.i = 0;
45
- /** @private @readonly */
46
- this.tokens = tokens;
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;
65
+ }
66
+
67
+ /** @return {Map<number, number>} */
68
+ get ends() {
69
+ return this.#ends;
70
+ }
71
+
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
+ }
89
+
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);
102
+ }
103
+ return {
104
+ type: 'eof',
105
+ value: '',
106
+ raw: '',
107
+ pos: this.eofPosition(),
108
+ ws: this.#precededByWhitespace,
109
+ };
47
110
  }
48
111
 
49
112
  /** @return {Token} */
50
113
  peek() {
51
- return this.tokens[this.i];
114
+ if (this.#lookahead === null) this.#lookahead = this.read();
115
+ return this.#lookahead;
52
116
  }
53
117
 
54
118
  /** @return {Token} */
55
119
  next() {
56
- return this.tokens[this.i++];
120
+ const token = this.peek();
121
+ this.#lookahead = null;
122
+ return token;
57
123
  }
58
124
 
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;
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
+ };
73
133
  }
74
134
 
75
- /**
76
- * @param {string} value
77
- * @param {string} [value2]
78
- * @return {boolean}
79
- */
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} */
80
142
  isPunct(value, value2) {
81
143
  const t = this.peek();
82
144
  return (
@@ -85,46 +147,46 @@ class Parser {
85
147
  );
86
148
  }
87
149
 
88
- /**
89
- * @param {string} value
90
- * @return {boolean}
91
- */
150
+ /** @param {string} value @return {boolean} */
92
151
  matchPunct(value) {
93
- if (this.isPunct(value)) {
94
- this.next();
95
- return true;
96
- }
97
- return false;
152
+ if (!this.isPunct(value)) return false;
153
+ this.next();
154
+ return true;
98
155
  }
99
156
 
100
- /**
101
- * @param {string} value
102
- * @return {Token}
103
- */
157
+ /** @param {string} value @return {Token} */
104
158
  expectPunct(value) {
105
- return this.expect('punct', 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;
106
166
  }
107
167
 
108
- /**
109
- * @param {number} [minBp]
110
- * @return {Node}
111
- */
168
+ /** @param {number} [minBp] @return {Node} */
112
169
  parseExpr(minBp = 0) {
113
170
  const t = this.next();
114
- const key = t.type === 'punct' ? t.value : t.type;
171
+ const key = t.type === 'punct' ? String(t.value) : t.type;
115
172
  const prefix = PREFIX[key];
116
- if (!prefix) {
117
- throw new Error(`Unexpected token "${t.value}" at position ${t.pos}`);
118
- }
173
+ if (!prefix)
174
+ throw new Error(`Unexpected token "${t.raw}" at position ${t.pos}`);
119
175
  let left = prefix(this, t);
120
176
 
121
177
  while (true) {
122
178
  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;
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
+ );
127
186
  }
187
+ const infixKey = nxt.type === 'punct' ? String(nxt.value) : nxt.type;
188
+ const rule = INFIX[infixKey];
189
+ if (!rule || rule.lbp < minBp) break;
128
190
  if (infixKey === '+' || infixKey === '-') {
129
191
  /** @type {import('./node.js').SumTerm[]} */
130
192
  const terms = [{ sign: /** @type {1} */ (1), node: left }];
@@ -139,7 +201,6 @@ class Parser {
139
201
  left = mkSum(terms);
140
202
  continue;
141
203
  }
142
-
143
204
  if (infixKey === '*' || infixKey === '/') {
144
205
  /** @type {import('./node.js').ProductFactor[]} */
145
206
  const factors = [{ exponent: /** @type {1} */ (1), node: left }];
@@ -153,148 +214,260 @@ class Parser {
153
214
  left = mkProduct(factors);
154
215
  continue;
155
216
  }
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
217
  break;
160
218
  }
161
-
162
219
  return left;
163
220
  }
164
221
  }
165
222
 
223
+ /** @param {CSSToken} t @param {boolean} ws @return {Token} */
224
+ function normalizeToken(t, ws) {
225
+ const [type, raw, pos, , detail] = t;
226
+ switch (type) {
227
+ case CssType.Number:
228
+ return {
229
+ type: 'number',
230
+ value: detail.value,
231
+ raw,
232
+ pos,
233
+ ws,
234
+ signCharacter: detail.signCharacter,
235
+ };
236
+ case CssType.Dimension: {
237
+ const match = NUMERIC_RAW.exec(raw);
238
+ return {
239
+ type: 'dimension',
240
+ value: detail.value,
241
+ raw,
242
+ pos,
243
+ ws,
244
+ unit: detail.unit,
245
+ rawUnit: match ? raw.slice(match[0].length) : detail.unit,
246
+ signCharacter: detail.signCharacter,
247
+ };
248
+ }
249
+ case CssType.Percentage:
250
+ return {
251
+ type: 'dimension',
252
+ value: detail.value,
253
+ raw,
254
+ pos,
255
+ ws,
256
+ unit: '%',
257
+ rawUnit: '%',
258
+ signCharacter: detail.signCharacter,
259
+ };
260
+ case CssType.Ident:
261
+ case CssType.Function:
262
+ return {
263
+ type: type === CssType.Ident ? 'ident' : 'function',
264
+ value: detail.value,
265
+ raw,
266
+ pos,
267
+ ws,
268
+ };
269
+ case CssType.OpenParen:
270
+ case CssType.CloseParen:
271
+ case CssType.Comma:
272
+ return { type: 'punct', value: raw, raw, pos, ws };
273
+ case CssType.Delim:
274
+ if (PUNCT_DELIMS.has(detail.value))
275
+ return { type: 'punct', value: detail.value, raw, pos, ws };
276
+ }
277
+ throw new Error(`Unexpected character "${raw[0] ?? ''}" at position ${pos}`);
278
+ }
279
+
280
+ /** §10.9 — case-insensitive except for NaN. @param {string} name @return {Node | null} */
281
+ function foldCalcKeyword(name) {
282
+ if (name === 'NaN' || name === '-NaN') return num(Number.NaN);
283
+ switch (name.toLowerCase()) {
284
+ case 'pi':
285
+ return num(Math.PI);
286
+ case 'e':
287
+ return num(Math.E);
288
+ case 'infinity':
289
+ return num(Infinity);
290
+ case '-infinity':
291
+ return num(-Infinity);
292
+ }
293
+ return null;
294
+ }
295
+
166
296
  const ADD_BP = 1;
167
297
  const MUL_BP = 3;
168
- const UNARY_BP = 7;
298
+ const MATCH_CALC = /^(?:-(?:moz|webkit)-)?calc$/i;
169
299
 
170
- /**
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).
175
- */
176
- const OPAQUE_ARG_FUNCTIONS = new Set(['anchor', 'anchor-size']);
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();
303
+ 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)
309
+ );
310
+ }
177
311
 
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 ?? ''}`;
312
+ /** @param {Parser} p @param {Token} token */
313
+ function requireSurroundingWs(p, token) {
314
+ if (!token.ws || !p.peek().ws)
315
+ throw new Error(
316
+ `"${token.value}" must be surrounded by whitespace at position ${token.pos}`
317
+ );
318
+ }
319
+
320
+ /** @param {Parser} p @param {Token} t @return {Node} */
321
+ function parseCall(p, t) {
322
+ const name = String(t.value);
323
+ 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);
327
+ /** @type {Node[]} */ const args = [];
328
+ if (!p.isPunct(')')) {
329
+ args.push(p.parseExpr(0));
330
+ while (p.matchPunct(',')) args.push(p.parseExpr(0));
186
331
  }
187
- return t.value;
332
+ p.expectPunct(')');
333
+ return call(name, args, sourceSpelling(rawName, name));
188
334
  }
189
335
 
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));
336
+ /** @param {CSSToken[]} tokens @param {number} start @param {number} end */
337
+ function rawTokens(tokens, start, end) {
338
+ let raw = '';
339
+ for (let i = start; i < end; i++) raw += tokens[i][1];
340
+ return raw;
341
+ }
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;
205
364
  }
206
- buf = '';
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
+ /** @param {CSSToken[]} tokens @param {number} start @param {number} end */
374
+ function customProperty(tokens, start, end) {
375
+ /** @type {CSSToken | null} */
376
+ let found = null;
377
+ let foundIndex = -1;
378
+ for (let i = start; i < end; i++) {
379
+ const type = tokens[i][0];
380
+ if (type === CssType.Whitespace || type === CssType.Comment) continue;
381
+ if (found !== null) return null;
382
+ found = tokens[i];
383
+ foundIndex = i;
384
+ }
385
+ if (!found || found[0] !== CssType.Ident) return null;
386
+ const decoded = found[4].value;
387
+ return decoded.startsWith('--') && decoded !== '--'
388
+ ? { decoded, raw: found[1], index: foundIndex }
389
+ : null;
390
+ }
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);
207
399
  };
208
- while (true) {
209
- const tk = p.peek();
210
- if (tk.type === 'eof') {
211
- throw new Error(`Unclosed ${name}( at position ${tk.pos}`);
400
+ for (let i = start; i < end; i++) {
401
+ const token = tokens[i];
402
+ const close = ends.get(i) ?? -1;
403
+ if (close === -1) {
404
+ push(token[1]);
405
+ continue;
212
406
  }
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);
222
- }
223
- } else if (tk.value === ',' && depth === 1) {
224
- p.next();
225
- flush();
226
- continue;
407
+ const isMathFunction =
408
+ token[0] === CssType.Function &&
409
+ (MATCH_CALC.test(token[4].value) ||
410
+ isSupportedMathFunction(token[4].value));
411
+ if (isMathFunction) {
412
+ try {
413
+ push(parseRange(tokens, i, close + 1, ends));
414
+ } catch {
415
+ push(rawTokens(tokens, i, close + 1));
227
416
  }
417
+ } else {
418
+ push(token[1]);
419
+ push(componentTree(tokens, i + 1, close, ends));
420
+ push(tokens[close][1]);
228
421
  }
229
- if (tk.ws && buf) {
230
- buf += ' ';
231
- }
232
- buf += tokenText(tk);
233
- p.next();
422
+ i = close;
234
423
  }
424
+ return tree;
235
425
  }
236
-
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) {
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();
429
+ if (close === -1)
430
+ throw new Error(`Unclosed ${name}( at position ${p.eofPosition()}`);
431
+ const comma = firstComma(tokens, start, close, ends);
432
+ const property = customProperty(tokens, start, comma === -1 ? close : comma);
433
+ if (!property)
250
434
  throw new Error(
251
- `"${token.value}" must be surrounded by whitespace at position ${token.pos}`
435
+ `Invalid custom property in ${name}() at position ${tokens[start]?.[2] ?? p.eofPosition()}`
252
436
  );
253
- }
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
+ );
443
+ if (comma !== -1)
444
+ setComponents(node, componentTree(tokens, property.index + 1, close, ends));
445
+ return node;
254
446
  }
255
447
 
256
448
  /** @type {Record<string, PrefixParselet>} */
257
449
  const PREFIX = {
258
- number: (_p, t) => num(Number.parseFloat(t.value)),
259
-
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
- ),
266
-
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
- }
279
- }
280
- p.expectPunct(')');
281
- return call(t.value, args);
282
- }
283
- const kw = foldCalcKeyword(t.value);
284
- if (kw) {
285
- return kw;
286
- }
287
- return ident(t.value);
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
+ );
288
458
  },
289
-
459
+ ident: (_p, t) => {
460
+ const name = String(t.value);
461
+ return foldCalcKeyword(name) ?? ident(name, sourceSpelling(t.raw, name));
462
+ },
463
+ function: parseCall,
290
464
  '(': (p) => {
291
465
  const e = p.parseExpr(0);
292
466
  p.expectPunct(')');
293
467
  return e.type === 'Sum' ? { ...e, grouped: true } : e;
294
468
  },
295
-
296
- '-': (p) => negate(p.parseExpr(UNARY_BP)),
297
- '+': (p) => p.parseExpr(UNARY_BP),
469
+ '-': (p) => negate(p.parseExpr(7)),
470
+ '+': (p) => p.parseExpr(7),
298
471
  };
299
472
 
300
473
  /** @type {Record<string, {lbp: number}>} */
@@ -306,19 +479,27 @@ const INFIX = {
306
479
  };
307
480
 
308
481
  /**
309
- * @param {Token[]} tokens
482
+ * @param {CSSToken[]} tokens
483
+ * @param {number} start
484
+ * @param {number} end
485
+ * @param {Map<number, number>} ends
310
486
  * @return {Node}
311
487
  */
312
- function parse(tokens) {
313
- const p = new Parser(tokens);
488
+ function parseRange(tokens, start, end, ends) {
489
+ const p = new Parser(tokens, start, end, ends);
314
490
  const ast = p.parseExpr(0);
315
491
  const trailing = p.peek();
316
- if (trailing.type !== 'eof') {
492
+ if (trailing.type !== 'eof')
317
493
  throw new Error(
318
- `Unexpected token "${trailing.value}" at position ${trailing.pos}`
494
+ `Unexpected token "${trailing.raw}" at position ${trailing.pos}`
319
495
  );
320
- }
321
496
  return ast;
322
497
  }
323
498
 
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);
503
+ }
504
+
324
505
  export { parse };