postcss-calc 11.2.0 → 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.
Files changed (40) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +44 -16
  3. package/package.json +13 -13
  4. package/src/index.js +2 -1
  5. package/src/lib/analyze.js +81 -43
  6. package/src/lib/compile.js +1 -1
  7. package/src/lib/functions.js +40 -10
  8. package/src/lib/node.js +62 -5
  9. package/src/lib/parser/opaque.js +151 -0
  10. package/src/lib/parser/tokens.js +242 -0
  11. package/src/lib/parser.js +27 -382
  12. package/src/lib/serialize/expression.js +376 -0
  13. package/src/lib/serialize/precision.js +244 -0
  14. package/src/lib/serialize.js +24 -517
  15. package/src/lib/simplify/bucket.js +22 -5
  16. package/src/lib/simplify/cancel.js +35 -11
  17. package/src/lib/simplify/clamp.js +2 -2
  18. package/src/lib/simplify/exact.js +18 -0
  19. package/src/lib/simplify/fold.js +15 -1
  20. package/src/lib/simplify/min-max.js +2 -2
  21. package/src/lib/simplify/product.js +263 -48
  22. package/src/lib/simplify/sum.js +6 -2
  23. package/src/lib/simplify.js +22 -5
  24. package/src/reduce.js +2 -1
  25. package/types/lib/analyze.d.ts +27 -0
  26. package/types/lib/functions.d.ts +19 -1
  27. package/types/lib/node.d.ts +17 -2
  28. package/types/lib/parser/opaque.d.ts +12 -0
  29. package/types/lib/parser/tokens.d.ts +83 -0
  30. package/types/lib/parser.d.ts +4 -44
  31. package/types/lib/serialize/expression.d.ts +95 -0
  32. package/types/lib/serialize/precision.d.ts +101 -0
  33. package/types/lib/simplify/bucket.d.ts +4 -1
  34. package/types/lib/simplify/cancel.d.ts +11 -7
  35. package/types/lib/simplify/exact.d.ts +9 -0
  36. package/types/lib/simplify/fold.d.ts +14 -1
  37. package/types/lib/simplify/product.d.ts +3 -7
  38. package/types/lib/simplify/sum.d.ts +2 -1
  39. package/types/lib/simplify.d.ts +3 -8
  40. package/src/lib/calculation-type.js +0 -25
package/src/lib/parser.js CHANGED
@@ -1,42 +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
3
  import {
5
4
  call,
6
5
  dim,
7
6
  ident,
7
+ groupSubstitution,
8
8
  mkProduct,
9
9
  mkSum,
10
- negate,
11
10
  num,
12
- opaqueCall,
13
11
  } from './node.js';
14
12
  import { isCalculationFunction, isSupportedMathFunction } from './functions.js';
15
13
  import { assertDepth } from './limits.js';
16
- 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';
17
24
 
18
25
  /** @typedef {import('@csstools/css-tokenizer').CSSToken} CSSToken */
19
26
  /** @typedef {import('./node.js').Node} Node */
20
- /** @typedef {import('./node.js').OpaqueComponent} OpaqueComponent */
21
- /** @typedef {ReturnType<typeof import('./block-index.js').indexBlocks>} BlockIndex */
22
- /** @typedef {{raw: string, pos: number, ws: boolean, index: number}} TokenBase */
23
- /** @typedef {TokenBase & {type: 'number', value: number, signCharacter?: '+' | '-'}} NumberToken */
24
- /** @typedef {TokenBase & {type: 'dimension', value: number, unit: string, rawUnit: string, signCharacter?: '+' | '-'}} DimensionToken */
25
- /** @typedef {TokenBase & {type: 'ident', value: string}} IdentToken */
26
- /** @typedef {TokenBase & {type: 'function', value: string}} FunctionToken */
27
- /** @typedef {'(' | ')' | ',' | '+' | '-' | '*' | '/'} Punctuator */
28
- /** @typedef {TokenBase & {type: 'punct', value: Punctuator}} PunctToken */
29
- /** @typedef {TokenBase & {type: 'eof', value: '', raw: ''}} EofToken */
30
- /** @typedef {NumberToken | DimensionToken | IdentToken | FunctionToken | PunctToken | EofToken} Token */
31
- /**
32
- * Immutable bounds and shared block index for one parse range.
33
- * @typedef {Readonly<{tokens: CSSToken[], end: number, index: BlockIndex}>} ParseInput
34
- */
35
-
36
- /** @param {string} value @return {value is '+' | '-' | '*' | '/'} */
37
- function isOperator(value) {
38
- return value === '+' || value === '-' || value === '*' || value === '/';
39
- }
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 */
40
31
 
41
32
  /**
42
33
  * CSS numeric tokens do not retain a signed-zero distinction. Keep that
@@ -50,220 +41,6 @@ function normalizeSourceZero(value) {
50
41
  return value === 0 ? 0 : value;
51
42
  }
52
43
 
53
- /** @param {string} raw @param {string} decoded */
54
- function sourceSpelling(raw, decoded) {
55
- return raw === decoded ? undefined : raw;
56
- }
57
-
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;
73
- }
74
-
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;
81
- }
82
- }
83
-
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
- }
90
-
91
- /**
92
- * Scan one token without consuming it. `firstToken` supplies the virtual
93
- * leading trivia at a bounded parse boundary; all later whitespace state is
94
- * derived from the native tokens encountered in this scan.
95
- *
96
- * @param {ParseInput} input
97
- * @param {Cursor} cursor
98
- * @return {Token}
99
- */
100
- function scanToken(input, cursor) {
101
- let i = cursor.index;
102
- let ws = cursor.firstToken;
103
- while (i < input.end) {
104
- const native = input.tokens[i];
105
- if (native[0] === CssType.Whitespace || native[0] === CssType.Comment) {
106
- ws = true;
107
- i++;
108
- continue;
109
- }
110
- if (native[0] === CssType.EOF) break;
111
- const token = normalizeToken(native, i, ws);
112
- cursor.lookahead = token;
113
- cursor.lookaheadNextIndex = i + 1;
114
- return token;
115
- }
116
- /** @type {EofToken} */
117
- const token = {
118
- type: 'eof',
119
- value: '',
120
- raw: '',
121
- pos: eofPositionAt(input, i),
122
- ws,
123
- index: i,
124
- };
125
- cursor.lookahead = token;
126
- // Native EOF is a real token and is consumed past its array index. When
127
- // the bounded range ends before native EOF, this is a virtual EOF and must
128
- // remain at the range boundary.
129
- cursor.lookaheadNextIndex =
130
- i < input.end && input.tokens[i][0] === CssType.EOF ? i + 1 : input.end;
131
- return token;
132
- }
133
-
134
- /** @param {CSSToken} t @param {number} index @param {boolean} ws @return {Token} */
135
- function normalizeToken(t, index, ws) {
136
- const [type, raw, pos, , detail] = t;
137
- switch (type) {
138
- case CssType.Number:
139
- return {
140
- type: 'number',
141
- value: detail.value,
142
- raw,
143
- pos,
144
- ws,
145
- index,
146
- signCharacter: detail.signCharacter,
147
- };
148
- case CssType.Dimension: {
149
- const match = CSS_NUMBER_PREFIX.exec(raw);
150
- return {
151
- type: 'dimension',
152
- value: detail.value,
153
- raw,
154
- pos,
155
- ws,
156
- index,
157
- unit: detail.unit,
158
- rawUnit: match ? raw.slice(match[0].length) : detail.unit,
159
- signCharacter: detail.signCharacter,
160
- };
161
- }
162
- case CssType.Percentage:
163
- return {
164
- type: 'dimension',
165
- value: detail.value,
166
- raw,
167
- pos,
168
- ws,
169
- index,
170
- unit: '%',
171
- rawUnit: '%',
172
- signCharacter: detail.signCharacter,
173
- };
174
- case CssType.Ident:
175
- case CssType.Function:
176
- return {
177
- type: type === CssType.Ident ? 'ident' : 'function',
178
- value: detail.value,
179
- raw,
180
- pos,
181
- ws,
182
- index,
183
- };
184
- case CssType.OpenParen:
185
- return { type: 'punct', value: '(', raw, pos, ws, index };
186
- case CssType.CloseParen:
187
- return { type: 'punct', value: ')', raw, pos, ws, index };
188
- case CssType.Comma:
189
- return { type: 'punct', value: ',', raw, pos, ws, index };
190
- case CssType.Delim:
191
- if (isOperator(detail.value))
192
- return {
193
- type: 'punct',
194
- value: detail.value,
195
- raw,
196
- pos,
197
- ws,
198
- index,
199
- };
200
- }
201
- throw new Error(`Unexpected character "${raw[0] ?? ''}" at position ${pos}`);
202
- }
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
44
  /** @param {ParseInput} input @param {Cursor} cursor @param {Token} token @param {number} depth @return {Node} */
268
45
  function parsePrefix(input, cursor, token, depth) {
269
46
  switch (token.type) {
@@ -289,15 +66,12 @@ function parsePrefix(input, cursor, token, depth) {
289
66
  case '(': {
290
67
  const expression = parseExpr(input, cursor, 0, depth + 1);
291
68
  expectPunct(input, cursor, ')');
292
- return expression.type === 'Sum'
293
- ? { ...expression, grouped: true }
294
- : expression;
69
+ return groupSubstitution(expression);
295
70
  }
296
- case '-':
297
- return negate(parseExpr(input, cursor, 7, depth + 1));
298
- case '+':
299
- return parseExpr(input, cursor, 7, depth + 1);
300
71
  }
72
+ // No unary `+`/`-` production exists in the <calc-value> grammar; a
73
+ // sign is only valid inside a number/dimension token or as a binary
74
+ // operator. `-(...)` therefore fails to parse and is preserved.
301
75
  }
302
76
  throw new Error(`Unexpected token "${token.raw}" at position ${token.pos}`);
303
77
  }
@@ -318,10 +92,10 @@ function parseExpr(input, cursor, minBp = 0, depth = 0) {
318
92
  `"${nxt.signCharacter}" must be surrounded by whitespace at position ${nxt.pos}`
319
93
  );
320
94
  }
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 === '-') {
95
+ if (nxt.type !== 'punct') break;
96
+ const op = nxt.value;
97
+ if (op === '+' || op === '-') {
98
+ if (ADD_BP < minBp) break;
325
99
  /** @type {import('./node.js').SumTerm[]} */
326
100
  const terms = [{ sign: /** @type {1} */ (1), node: left }];
327
101
  do {
@@ -335,7 +109,8 @@ function parseExpr(input, cursor, minBp = 0, depth = 0) {
335
109
  left = mkSum(terms);
336
110
  continue;
337
111
  }
338
- if (infixKey === '*' || infixKey === '/') {
112
+ if (op === '*' || op === '/') {
113
+ if (MUL_BP < minBp) break;
339
114
  /** @type {import('./node.js').ProductFactor[]} */
340
115
  const factors = [{ exponent: /** @type {1} */ (1), node: left }];
341
116
  do {
@@ -371,22 +146,6 @@ function foldCalcKeyword(name) {
371
146
 
372
147
  const ADD_BP = 1;
373
148
  const MUL_BP = 3;
374
- /** @param {ParseInput} input @param {Cursor} cursor @param {FunctionToken} token @param {string} name @param {string} rawName @return {Node} */
375
- function parseOpaqueCall(input, cursor, token, name, rawName) {
376
- const start = token.index + 1;
377
- const close = input.index.closeOf(token.index, input.end);
378
- if (close === -1)
379
- throw new Error(
380
- `Unclosed ${name}( at position ${eofPositionAt(input, cursor.index)}`
381
- );
382
- cursor.skipTo(close + 1);
383
- return opaqueCall(
384
- name,
385
- componentTree(input, start, close),
386
- sourceSpelling(rawName, name)
387
- );
388
- }
389
-
390
149
  /** @param {ParseInput} input @param {Cursor} cursor @param {Token} token */
391
150
  function requireSurroundingWs(input, cursor, token) {
392
151
  if (!token.ws || !peekToken(input, cursor).ws)
@@ -400,9 +159,9 @@ function parseCall(input, cursor, t, depth) {
400
159
  const name = t.value;
401
160
  const rawName = t.raw.slice(0, -1);
402
161
  if (name.toLowerCase() === 'var')
403
- return parseVar(input, cursor, t, name, rawName);
162
+ return parseVar(input, cursor, t, name, rawName, parseRange);
404
163
  if (!isCalculationFunction(name) && !isSupportedMathFunction(name))
405
- return parseOpaqueCall(input, cursor, t, name, rawName);
164
+ return parseOpaqueCall(input, cursor, t, name, rawName, parseRange);
406
165
  /** @type {Node[]} */ const args = [];
407
166
  if (!isPunct(input, cursor, ')')) {
408
167
  args.push(parseExpr(input, cursor, 0, depth + 1));
@@ -413,120 +172,6 @@ function parseCall(input, cursor, t, depth) {
413
172
  return call(name, args, sourceSpelling(rawName, name));
414
173
  }
415
174
 
416
- /** @param {CSSToken[]} tokens @param {number} start @param {number} end */
417
- function rawTokens(tokens, start, end) {
418
- let raw = '';
419
- for (let i = start; i < end; i++) raw += tokens[i][1];
420
- return raw;
421
- }
422
- /** @param {CSSToken[]} tokens @param {number} start @param {number} end */
423
- function customProperty(tokens, start, end) {
424
- /** @type {CSSToken | null} */
425
- let found = null;
426
- let foundIndex = -1;
427
- for (let i = start; i < end; i++) {
428
- const type = tokens[i][0];
429
- if (type === CssType.Whitespace || type === CssType.Comment) continue;
430
- if (found !== null) return null;
431
- found = tokens[i];
432
- foundIndex = i;
433
- }
434
- if (!found || found[0] !== CssType.Ident) return null;
435
- const decoded = found[4].value;
436
- return decoded.startsWith('--') && decoded !== '--'
437
- ? { decoded, raw: found[1], index: foundIndex }
438
- : null;
439
- }
440
- /** @param {ParseInput} input @param {number} start @param {number} end @param {number} [depth] @return {OpaqueComponent[]} */
441
- function componentTree(input, start, end, depth = 0) {
442
- assertDepth(depth);
443
- /** @type {OpaqueComponent[]} */ const root = [];
444
- /** @type {OpaqueComponent[]} */ let tree = root;
445
- /** @type {{parent: OpaqueComponent[], tree: OpaqueComponent[], close: number, end: number}[]} */
446
- const frames = [];
447
- const { tokens } = input;
448
- let i = start;
449
- while (true) {
450
- if (i >= end) {
451
- if (frames.length === 0) break;
452
- const frame =
453
- /** @type {{parent: OpaqueComponent[], tree: OpaqueComponent[], close: number, end: number}} */ (
454
- frames.pop()
455
- );
456
- tree = frame.parent;
457
- pushComponent(tree, frame.tree);
458
- pushComponent(tree, tokens[frame.close][1]);
459
- end = frame.end;
460
- i = frame.close + 1;
461
- continue;
462
- }
463
-
464
- const token = tokens[i];
465
- const close = input.index.closeOf(i, end);
466
- if (close === -1) {
467
- pushComponent(tree, token[1]);
468
- i++;
469
- continue;
470
- }
471
-
472
- const isMathFunction =
473
- token[0] === CssType.Function &&
474
- (isCalculationFunction(token[4].value) ||
475
- isSupportedMathFunction(token[4].value));
476
- if (isMathFunction) {
477
- try {
478
- pushComponent(tree, parseRange(input, i, close + 1));
479
- } catch {
480
- pushComponent(tree, rawTokens(tokens, i, close + 1));
481
- }
482
- i = close + 1;
483
- continue;
484
- }
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++;
494
- }
495
- return root;
496
- }
497
- /** @param {ParseInput} input @param {Cursor} cursor @param {FunctionToken} token @param {string} name @param {string} rawName @return {Node} */
498
- function parseVar(input, cursor, token, name, rawName) {
499
- const { tokens } = input;
500
- const start = token.index + 1;
501
- const close = input.index.closeOf(token.index, input.end);
502
- if (close === -1)
503
- throw new Error(
504
- `Unclosed ${name}( at position ${eofPositionAt(input, cursor.index)}`
505
- );
506
- const comma = input.index.firstTopLevelComma(start, close);
507
- const property = customProperty(tokens, start, comma === -1 ? close : comma);
508
- if (!property)
509
- throw new Error(
510
- `Invalid custom property in ${name}() at position ${tokens[start]?.[2] ?? eofPositionAt(input, cursor.index)}`
511
- );
512
- cursor.skipTo(close + 1);
513
- /** @type {OpaqueComponent[]} */
514
- const components = [
515
- ident(property.decoded, sourceSpelling(property.raw, property.decoded)),
516
- ];
517
- if (comma !== -1)
518
- components.push(...componentTree(input, property.index + 1, close));
519
- return opaqueCall(name, components, sourceSpelling(rawName, name));
520
- }
521
-
522
- /** @type {Record<string, {lbp: number}>} */
523
- const INFIX = {
524
- '+': { lbp: ADD_BP },
525
- '-': { lbp: ADD_BP },
526
- '*': { lbp: MUL_BP },
527
- '/': { lbp: MUL_BP },
528
- };
529
-
530
175
  /**
531
176
  * @param {ParseInput} input
532
177
  * @param {number} start