postcss-calc 11.1.1 → 11.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +40 -18
  2. package/package.json +16 -11
  3. package/src/index.js +8 -5
  4. package/src/lib/analyze.js +228 -0
  5. package/src/lib/block-index.js +118 -0
  6. package/src/lib/calculation-type.js +25 -0
  7. package/src/lib/compile.js +80 -0
  8. package/src/lib/convertUnits.js +15 -1
  9. package/src/lib/functions.js +364 -0
  10. package/src/lib/limits.js +49 -0
  11. package/src/lib/node.js +85 -39
  12. package/src/lib/opaque.js +20 -0
  13. package/src/lib/parser.js +493 -257
  14. package/src/lib/print.js +75 -0
  15. package/src/lib/regex.js +4 -0
  16. package/src/lib/scan.js +61 -0
  17. package/src/lib/serialize.js +574 -207
  18. package/src/lib/simplify/abs.js +1 -1
  19. package/src/lib/simplify/bucket.js +18 -23
  20. package/src/lib/simplify/call.js +6 -92
  21. package/src/lib/simplify/product.js +7 -4
  22. package/src/lib/simplify/round.js +4 -4
  23. package/src/lib/simplify/sum.js +22 -10
  24. package/src/lib/simplify.js +20 -5
  25. package/src/reduce.js +46 -118
  26. package/types/index.d.ts +4 -0
  27. package/types/lib/analyze.d.ts +18 -0
  28. package/types/lib/block-index.d.ts +35 -0
  29. package/types/lib/calculation-type.d.ts +18 -0
  30. package/types/lib/compile.d.ts +37 -0
  31. package/types/lib/convertUnits.d.ts +8 -1
  32. package/types/lib/functions.d.ts +40 -0
  33. package/types/lib/limits.d.ts +8 -0
  34. package/types/lib/node.d.ts +31 -9
  35. package/types/lib/opaque.d.ts +9 -0
  36. package/types/lib/parser.d.ts +47 -49
  37. package/types/lib/print.d.ts +16 -0
  38. package/types/lib/regex.d.ts +2 -0
  39. package/types/lib/scan.d.ts +35 -0
  40. package/types/lib/serialize.d.ts +19 -2
  41. package/types/lib/simplify/bucket.d.ts +2 -2
  42. package/types/lib/simplify/call.d.ts +4 -18
  43. package/types/lib/simplify/round.d.ts +3 -1
  44. package/types/lib/simplify.d.ts +5 -2
  45. package/types/reduce.d.ts +47 -6
  46. package/src/lib/tokenizer.js +0 -139
  47. package/types/lib/tokenizer.d.ts +0 -32
@@ -15,7 +15,7 @@ function simplifyAbs(args) {
15
15
  return num(Math.abs(a.value));
16
16
  }
17
17
  if (a.type === 'Dim' && a.unit !== '%') {
18
- return dim(Math.abs(a.value), a.unit);
18
+ return dim(Math.abs(a.value), a.unit, a.rawUnit);
19
19
  }
20
20
  return call('abs', [a]);
21
21
  }
@@ -9,10 +9,10 @@ import { convert } from '../convertUnits.js';
9
9
  /**
10
10
  * @typedef {object} UnitBucket
11
11
  * @property {string} unit
12
+ * @property {string} [rawUnit]
12
13
  * @property {number} total
13
14
  * @property {number} scale largest |term| accumulated into `total`, for noise detection
14
15
  * @property {import('../convertUnits.js').BaseType | null} base
15
- * @property {number} order
16
16
  */
17
17
 
18
18
  /** Mutates `buckets` in place — totals of survivor buckets accumulate the
@@ -21,32 +21,27 @@ import { convert } from '../convertUnits.js';
21
21
  * @return {UnitBucket[]}
22
22
  */
23
23
  function mergeConvertibleBuckets(buckets) {
24
- const ordered = buckets.sort((a, b) => a.order - b.order);
25
- /** @type {Set<string>} */ const merged = new Set();
24
+ /** @type {Map<import('../convertUnits.js').BaseType, UnitBucket>} */ const representative =
25
+ new Map();
26
26
  /** @type {UnitBucket[]} */ const out = [];
27
- for (const b of ordered) {
28
- const keyB = b.unit.toLowerCase();
29
- if (merged.has(keyB)) {
27
+ for (const b of buckets) {
28
+ if (b.base === null) {
29
+ out.push(b);
30
30
  continue;
31
31
  }
32
- merged.add(keyB);
33
- if (b.base !== null) {
34
- for (const other of ordered) {
35
- const keyO = other.unit.toLowerCase();
36
- if (merged.has(keyO)) {
37
- continue;
38
- }
39
- if (other.base !== b.base) {
40
- continue;
41
- }
42
- const converted = convert(other.total, other.unit, b.unit);
43
- if (converted !== null) {
44
- b.total += converted;
45
- merged.add(keyO);
46
- }
47
- }
32
+ const first = representative.get(b.base);
33
+ if (!first) {
34
+ representative.set(b.base, b);
35
+ out.push(b);
36
+ continue;
37
+ }
38
+ const converted = convert(b.total, b.unit, first.unit);
39
+ if (converted === null) {
40
+ out.push(b);
41
+ continue;
48
42
  }
49
- out.push(b);
43
+ first.total += converted;
44
+ first.scale = Math.max(first.scale, Math.abs(converted));
50
45
  }
51
46
  return out;
52
47
  }
@@ -1,89 +1,13 @@
1
1
  // Pre-simplify args once, route by name. Leaf folds receive simplified
2
2
  // args so they don't need to recurse into `simplify` themselves.
3
3
 
4
- import { simplifyMinMax } from './min-max.js';
5
- import { simplifyClamp } from './clamp.js';
6
- import { simplifyAbs } from './abs.js';
7
- import { simplifySign } from './sign.js';
8
- import { simplifyModRem } from './mod-rem.js';
9
- import { simplifyRound } from './round.js';
10
- import { simplifyTrig } from './trig.js';
11
- import { simplifyInverseTrig } from './inverse-trig.js';
12
- import { simplifyAtan2 } from './atan2.js';
13
- import { simplifyPow } from './pow.js';
14
- import { simplifySqrt } from './sqrt.js';
15
- import { simplifyExp } from './exp.js';
16
- import { simplifyLog } from './log.js';
17
- import { simplifyHypot } from './hypot.js';
18
-
19
4
  import { call } from '../node.js';
5
+ import { mathFunctions, isCalculationFunction } from '../functions.js';
20
6
 
21
7
  /** @typedef {import('../node.js').Node} Node */
22
8
  /** @typedef {import('../simplify.js').SimplifyFn} SimplifyFn */
23
-
24
9
  /** @typedef {(name: string, args: Node[]) => Node} MathSimplifier */
25
10
 
26
- // Bare CSS math functions with implemented simplification semantics, keyed
27
- // by lowercase name. calc() and its vendor-prefixed forms are handled
28
- // separately as wrappers in simplifyCall. This map is the single source of
29
- // truth for dispatch, `isSupportedMathFunction`, and `QUICK_MATH_TEST`.
30
- /** @type {Map<string, MathSimplifier>} */
31
- const MATH_SIMPLIFIERS = new Map([
32
- ['min', simplifyMinMax],
33
- ['max', simplifyMinMax],
34
- ['clamp', (_name, args) => simplifyClamp(args)],
35
- ['abs', (_name, args) => simplifyAbs(args)],
36
- ['sign', (_name, args) => simplifySign(args)],
37
- ['mod', (_name, args) => simplifyModRem('mod', args)],
38
- ['rem', (_name, args) => simplifyModRem('rem', args)],
39
- ['round', (_name, args) => simplifyRound(args)],
40
- ['sin', (_name, args) => simplifyTrig('sin', args)],
41
- ['cos', (_name, args) => simplifyTrig('cos', args)],
42
- ['tan', (_name, args) => simplifyTrig('tan', args)],
43
- ['asin', (_name, args) => simplifyInverseTrig('asin', args)],
44
- ['acos', (_name, args) => simplifyInverseTrig('acos', args)],
45
- ['atan', (_name, args) => simplifyInverseTrig('atan', args)],
46
- ['atan2', (_name, args) => simplifyAtan2(args)],
47
- ['pow', (_name, args) => simplifyPow(args)],
48
- ['sqrt', (_name, args) => simplifySqrt(args)],
49
- ['hypot', (_name, args) => simplifyHypot(args)],
50
- ['log', (_name, args) => simplifyLog(args)],
51
- ['exp', (_name, args) => simplifyExp(args)],
52
- ]);
53
-
54
- const mathFnNames = [...MATH_SIMPLIFIERS.keys()].sort(
55
- (a, b) => b.length - a.length
56
- );
57
-
58
- const QUICK_MATH_TEST = new RegExp(
59
- `(?:-(?:webkit|moz)-)?(?:calc|${mathFnNames.join('|')})\\(`,
60
- 'i'
61
- );
62
-
63
- /**
64
- * Fast check to determine whether a CSS component value could contain
65
- * a supported calculation or math function call (or an escape sequence
66
- * that could decode to one).
67
- *
68
- * @param {string} value
69
- * @return {boolean}
70
- */
71
- function hasPotentialMathFunction(value) {
72
- return (
73
- value.includes('(') && (QUICK_MATH_TEST.test(value) || value.includes('\\'))
74
- );
75
- }
76
-
77
- /**
78
- * Whether a bare CSS math function has an implemented simplifier.
79
- *
80
- * @param {string} name
81
- * @return {boolean}
82
- */
83
- function isSupportedMathFunction(name) {
84
- return MATH_SIMPLIFIERS.has(name.toLowerCase());
85
- }
86
-
87
11
  /**
88
12
  * @param {Extract<Node, { type: 'Call' }>} node
89
13
  * @param {SimplifyFn} simplify
@@ -92,7 +16,7 @@ function isSupportedMathFunction(name) {
92
16
  function simplifyCall(node, simplify) {
93
17
  const name = node.name.toLowerCase();
94
18
 
95
- if (name === 'calc' || name === '-webkit-calc' || name === '-moz-calc') {
19
+ if (isCalculationFunction(name)) {
96
20
  if (node.args.length !== 1) {
97
21
  throw new Error(`${node.name}() takes exactly one argument`);
98
22
  }
@@ -101,22 +25,12 @@ function simplifyCall(node, simplify) {
101
25
 
102
26
  const args = node.args.map((a) => simplify(a));
103
27
 
104
- const simplifier = MATH_SIMPLIFIERS.get(name);
28
+ const simplifier = mathFunctions.get(name)?.simplify;
105
29
  if (simplifier) {
106
- // min/max preserve the call's original casing in their opaque-args
107
- // fallback; the rest normalize to lowercase internally.
108
- return simplifier(
109
- name === 'min' || name === 'max' ? node.name : name,
110
- args
111
- );
30
+ return simplifier(name, args);
112
31
  }
113
32
 
114
- return call(node.name, args);
33
+ return call(node.name, args, node.rawName);
115
34
  }
116
35
 
117
- export {
118
- isSupportedMathFunction,
119
- simplifyCall,
120
- hasPotentialMathFunction,
121
- QUICK_MATH_TEST,
122
- };
36
+ export { simplifyCall };
@@ -15,7 +15,7 @@ import { tryCancelPair } from './cancel.js';
15
15
  */
16
16
  function simplifyProduct(product, simplify) {
17
17
  let coeff = 1;
18
- /** @type {{exponent: 1 | -1, value: number, unit: string}[]} */
18
+ /** @type {{exponent: 1 | -1, value: number, unit: string, rawUnit?: string}[]} */
19
19
  const dims = [];
20
20
  /** @type {ProductFactor[]} */
21
21
  const opaque = [];
@@ -54,7 +54,7 @@ function simplifyProduct(product, simplify) {
54
54
  return;
55
55
  }
56
56
  if (n.type === 'Dim') {
57
- dims.push({ exponent, value: n.value, unit: n.unit });
57
+ dims.push({ exponent, value: n.value, unit: n.unit, rawUnit: n.rawUnit });
58
58
  scalarChain.push({ exponent, value: n.value });
59
59
  return;
60
60
  }
@@ -112,7 +112,7 @@ function simplifyProduct(product, simplify) {
112
112
  value = value / f.value;
113
113
  }
114
114
  }
115
- return dim(value, d.unit);
115
+ return dim(value, d.unit, d.rawUnit);
116
116
  }
117
117
 
118
118
  if (remainingDims.length === 0 && opaque.length === 0) {
@@ -125,7 +125,10 @@ function simplifyProduct(product, simplify) {
125
125
  factors.push({ exponent: 1, node: num(coeff) });
126
126
  }
127
127
  for (const d of remainingDims) {
128
- factors.push({ exponent: d.exponent, node: dim(d.value, d.unit) });
128
+ factors.push({
129
+ exponent: d.exponent,
130
+ node: dim(d.value, d.unit, d.rawUnit),
131
+ });
129
132
  }
130
133
  factors.push(...opaque);
131
134
 
@@ -44,11 +44,11 @@ function simplifyRound(args) {
44
44
  // to applyRound, where floor*b===ceil*b===±∞ collapses back to A
45
45
  // (§10.3.1 "result is the same infinity").
46
46
  if (Number.isNaN(b)) {
47
- return num(Number.NaN);
47
+ return foldResult(fold, Number.NaN);
48
48
  }
49
49
  if (!Number.isFinite(b)) {
50
50
  if (!Number.isFinite(a)) {
51
- return num(Number.NaN);
51
+ return foldResult(fold, Number.NaN);
52
52
  }
53
53
  let result;
54
54
  if (strategy === 'up' && a > 0) {
@@ -63,7 +63,7 @@ function simplifyRound(args) {
63
63
 
64
64
  const result = applyRound(strategy, a, b);
65
65
  if (Number.isNaN(result)) {
66
- return num(Number.NaN);
66
+ return foldResult(fold, Number.NaN);
67
67
  }
68
68
  return foldResult(fold, result);
69
69
  }
@@ -116,4 +116,4 @@ function applyRound(strategy, a, b) {
116
116
  }
117
117
  }
118
118
 
119
- export { simplifyRound };
119
+ export { ROUND_STRATEGIES, simplifyRound };
@@ -1,5 +1,5 @@
1
1
  import { mkSum, num, dim } from '../node.js';
2
- import { baseOf } from '../convertUnits.js';
2
+ import { staticBaseOf } from '../convertUnits.js';
3
3
  import { mergeConvertibleBuckets } from './bucket.js';
4
4
 
5
5
  /**
@@ -34,12 +34,12 @@ function simplifySum(sum, simplify) {
34
34
  // encountered unit. `100vh - 5rem - 10rem - 100px` → `-15rem` in phase 1,
35
35
  // then vh/rem/px stay separate in phase 2 (none convert to each other).
36
36
  let numTotal = 0;
37
+ let hasNum = false;
37
38
  let numScale = 0;
38
39
  /** @type {Map<string, UnitBucket>} */
39
40
  const byUnit = new Map();
40
41
  /** @type {SumTerm[]} */
41
42
  const opaque = [];
42
- let bucketOrder = 0;
43
43
 
44
44
  /**
45
45
  * @param {1 | -1} sign
@@ -65,7 +65,11 @@ function simplifySum(sum, simplify) {
65
65
  return;
66
66
  }
67
67
  if (n.type === 'Num') {
68
- numTotal += sign * n.value;
68
+ // Do not add an artificial +0 before the first value: `+0 + -0`
69
+ // becomes +0 in IEEE-754 and would discard a parsed/simplified -0.
70
+ const value = sign * n.value;
71
+ numTotal = hasNum ? numTotal + value : value;
72
+ hasNum = true;
69
73
  numScale = Math.max(numScale, Math.abs(n.value));
70
74
  return;
71
75
  }
@@ -78,10 +82,10 @@ function simplifySum(sum, simplify) {
78
82
  } else {
79
83
  byUnit.set(key, {
80
84
  unit: n.unit,
85
+ rawUnit: n.rawUnit,
81
86
  total: sign * n.value,
82
87
  scale: Math.abs(n.value),
83
- base: baseOf(n.unit),
84
- order: bucketOrder++,
88
+ base: staticBaseOf(n.unit),
85
89
  });
86
90
  }
87
91
  return;
@@ -93,15 +97,23 @@ function simplifySum(sum, simplify) {
93
97
  processTerm(t.sign, simplify(t.node));
94
98
  }
95
99
 
96
- // mkSum drops zero-valued Nums, so pushing the numeric total
97
- // unconditionally is harmless. Zero-valued unit buckets are kept for
98
- // type info (WPT calc-serialization-002).
100
+ // Keep a numeric total, including zero, when the source sum contained a
101
+ // number. In a mixed or unresolved sum that value constrains the other
102
+ // terms to <number>; mkSum retains it as a type anchor. A negative zero is
103
+ // also deliberately retained. Zero-valued unit buckets are kept for type
104
+ // info (WPT calc-serialization-002).
99
105
  /** @type {SumTerm[]} */
100
- const terms = [{ sign: 1, node: num(denoise(numTotal, numScale)) }];
106
+ const terms = hasNum
107
+ ? [{ sign: /** @type {1} */ (1), node: num(denoise(numTotal, numScale)) }]
108
+ : [];
101
109
  for (const bucket of mergeConvertibleBuckets([...byUnit.values()])) {
102
110
  terms.push({
103
111
  sign: 1,
104
- node: dim(denoise(bucket.total, bucket.scale), bucket.unit),
112
+ node: dim(
113
+ denoise(bucket.total, bucket.scale),
114
+ bucket.unit,
115
+ bucket.rawUnit
116
+ ),
105
117
  });
106
118
  }
107
119
  terms.push(...opaque);
@@ -5,31 +5,46 @@
5
5
  import { simplifySum } from './simplify/sum.js';
6
6
  import { simplifyProduct } from './simplify/product.js';
7
7
  import { simplifyCall } from './simplify/call.js';
8
+ import { simplifyComponents } from './opaque.js';
9
+ import { opaqueCall } from './node.js';
10
+ import { assertDepth } from './limits.js';
8
11
 
9
12
  /**
10
13
  * @typedef {import('./node.js').Node} Node
11
14
  *
12
- * Recursive simplifier reference, threaded into Sum/Product/Call. Lets
15
+ * Recursive simplifier reference, threaded into Sum/Product/Call/OpaqueCall. Lets
13
16
  * leaf fold modules avoid circular imports of the entry function.
14
17
  * @typedef {(node: Node) => Node} SimplifyFn
15
18
  */
16
19
 
17
20
  /**
21
+ * Simplify is an independent, composable AST transformation. It may
22
+ * synthesize canonical nodes while preserving the Node -> Node contract.
18
23
  * @param {Node} node
24
+ * @param {number} [depth]
19
25
  * @return {Node}
20
26
  */
21
- function simplify(node) {
27
+ function simplify(node, depth = 0) {
28
+ assertDepth(depth);
29
+ /** @param {Node} value */
30
+ const child = (value) => simplify(value, depth + 1);
22
31
  switch (node.type) {
23
32
  case 'Num':
24
33
  case 'Dim':
25
34
  case 'Ident':
26
35
  return node;
27
36
  case 'Call':
28
- return simplifyCall(node, simplify);
37
+ return simplifyCall(node, child);
38
+ case 'OpaqueCall':
39
+ return opaqueCall(
40
+ node.name,
41
+ simplifyComponents(node.components, child),
42
+ node.rawName
43
+ );
29
44
  case 'Sum':
30
- return simplifySum(node, simplify);
45
+ return simplifySum(node, child);
31
46
  case 'Product':
32
- return simplifyProduct(node, simplify);
47
+ return simplifyProduct(node, child);
33
48
  }
34
49
  }
35
50
 
package/src/reduce.js CHANGED
@@ -1,33 +1,20 @@
1
1
  // CSS component-value reducer. This module deliberately has no PostCSS
2
2
  // dependency so it can also be used for individual declaration values,
3
3
  // at-rule parameters, or selector text.
4
- import {
5
- tokenize as cssTokenize,
6
- TokenType as CssType,
7
- } from '@csstools/css-tokenizer';
8
- import { tokenizeTokens } from './lib/tokenizer.js';
9
- import { parse } from './lib/parser.js';
10
- import { simplify } from './lib/simplify.js';
11
- import {
12
- isSupportedMathFunction,
13
- hasPotentialMathFunction,
14
- QUICK_MATH_TEST,
15
- } from './lib/simplify/call.js';
16
- import { serialize } from './lib/serialize.js';
17
-
18
- const MATCH_CALC = /^(?:-(?:moz|webkit)-)?calc$/i;
19
-
20
- const BLOCK_CLOSE = new Map([
21
- [CssType.OpenParen, CssType.CloseParen],
22
- [CssType.OpenSquare, CssType.CloseSquare],
23
- [CssType.OpenCurly, CssType.CloseCurly],
24
- ]);
4
+ import { tokenize as cssTokenize } from '@csstools/css-tokenizer';
5
+ import { indexBlocks } from './lib/block-index.js';
6
+ import { hasPotentialMathFunction } from './lib/functions.js';
7
+ import { assertDepth } from './lib/limits.js';
8
+ import { findCalculations } from './lib/scan.js';
9
+ import { compileCandidates } from './lib/compile.js';
10
+ import { applyReplacements } from './lib/print.js';
25
11
 
26
12
  /**
27
13
  * @typedef {object} ReduceCalcOptions
28
14
  * @property {number | false} [precision]
29
15
  * @property {boolean} [warnWhenCannotResolve]
30
- * @property {boolean} [unwrapSingleNegativeNumber] Serialize finite negative results without a `calc()` wrapper. Defaults to `false`.
16
+ * @property {boolean} [unwrapSingleNegativeNumber] Deprecated alias for `unwrapSingleValue`.
17
+ * @property {boolean} [unwrapSingleValue] Serialize fully resolved finite scalar results without calculation syntax. Defaults to `false`.
31
18
  * @property {(error: Error, input: string) => void} [onParseError] Invoked when parse/simplify throws.
32
19
  * @property {(message: string) => void} [onWarn] Invoked when `warnWhenCannotResolve` is set and an expression cannot be reduced to a single value.
33
20
  */
@@ -41,89 +28,23 @@ const BLOCK_CLOSE = new Map([
41
28
  * @property {ResolvedReduceCalcOptions} options
42
29
  * @property {string} value
43
30
  * @property {import('@csstools/css-tokenizer').CSSToken[]} tokens
44
- * @property {Replacement[]} replacements
45
31
  */
46
32
 
47
33
  /**
48
34
  * @typedef {object} Replacement
49
35
  * @property {number} start
50
36
  * @property {number} end
51
- * @property {import('./lib/node.js').Node} node
52
- * @property {string} calcName
37
+ * @property {CalculationResult} result
53
38
  */
54
-
55
39
  /**
56
- * Walk one component-value level. Unsupported functions and simple blocks are
57
- * traversed, while a supported function is treated as one opaque calculation
58
- * even when parsing it fails. A missing closer consumes through EOF, matching
59
- * CSS component-value parsing's error recovery.
60
- *
61
- * @param {number} start
62
- * @param {import('@csstools/css-tokenizer').TokenType | undefined} expectedClose
63
- * @param {TransformContext} ctx
64
- * @param {boolean} transform
65
- * @return {number} Index of the matching closer, or the EOF token.
40
+ * @typedef {object} CalculationResult
41
+ * @property {import('./lib/node.js').Node} tree
42
+ * @property {'resolved' | 'unresolved'} status
43
+ * @property {string} rootName
44
+ * @property {string} rootSpelling
45
+ * @property {boolean} calculation
46
+ * @property {string | undefined} original
66
47
  */
67
- function walkTokens(start, expectedClose, ctx, transform) {
68
- for (let i = start; i < ctx.tokens.length; i++) {
69
- const token = ctx.tokens[i];
70
- if (token[0] === CssType.EOF || token[0] === expectedClose) return i;
71
-
72
- const blockClose = BLOCK_CLOSE.get(token[0]);
73
- if (blockClose) {
74
- i = walkTokens(i + 1, blockClose, ctx, transform);
75
- continue;
76
- }
77
-
78
- if (token[0] !== CssType.Function) continue;
79
-
80
- const name = token[4].value;
81
- const isCalc = MATCH_CALC.test(name);
82
- const isMath = !isCalc && isSupportedMathFunction(name);
83
- if (!transform || (!isCalc && !isMath)) {
84
- i = walkTokens(i + 1, CssType.CloseParen, ctx, transform);
85
- continue;
86
- }
87
-
88
- // Locate the complete outer function without transforming its children.
89
- const close = walkTokens(i + 1, CssType.CloseParen, ctx, false);
90
- const closed = ctx.tokens[close][0] === CssType.CloseParen;
91
- const end = closed ? ctx.tokens[close][3] + 1 : ctx.value.length;
92
- const sliceStart = isCalc ? i + 1 : i;
93
- const sliceEnd = closed ? close + (isCalc ? 0 : 1) : close;
94
- const inputStart = isCalc ? token[3] + 1 : token[2];
95
- const inputEnd = closed && isCalc ? ctx.tokens[close][2] : end;
96
- const contents = ctx.value.slice(inputStart, inputEnd);
97
- try {
98
- const node = simplify(
99
- parse(tokenizeTokens(ctx.tokens, end, sliceStart, sliceEnd))
100
- );
101
- ctx.replacements.push({
102
- start: token[2],
103
- end,
104
- node,
105
- calcName: isCalc ? name : 'calc',
106
- });
107
- } catch (error) {
108
- const err = error instanceof Error ? error : new Error('Error');
109
- ctx.options.onParseError?.(err, contents);
110
- }
111
- i = close;
112
- }
113
-
114
- return ctx.tokens.length - 1;
115
- }
116
-
117
- /**
118
- * @param {import('./lib/node.js').Node} node
119
- * @return {boolean}
120
- */
121
- function isUnresolvedResult(node) {
122
- if (node.type === 'Sum' || node.type === 'Product') {
123
- return true;
124
- }
125
- return node.type === 'Call' && isSupportedMathFunction(node.name);
126
- }
127
48
 
128
49
  /**
129
50
  * Simplify every supported CSS math function in a component-value string.
@@ -143,34 +64,41 @@ function reduceCalc(value, opts) {
143
64
  precision: 5,
144
65
  warnWhenCannotResolve: false,
145
66
  unwrapSingleNegativeNumber: false,
67
+ unwrapSingleValue: false,
146
68
  ...opts,
147
69
  };
148
- const tokens = cssTokenize({ css: value });
149
- /** @type {Replacement[]} */
150
- const replacements = [];
151
- walkTokens(0, undefined, { options, value, tokens, replacements }, true);
152
-
153
- if (replacements.length === 0) {
70
+ /** @type {import('@csstools/css-tokenizer').CSSToken[]} */
71
+ let tokens;
72
+ /** @type {ReturnType<typeof indexBlocks>} */
73
+ let index;
74
+ try {
75
+ tokens = cssTokenize({ css: value });
76
+ index = indexBlocks(tokens);
77
+ assertDepth(index.maxDepth);
78
+ } catch (error) {
79
+ options.onParseError?.(
80
+ error instanceof Error ? error : new Error('Error', { cause: error }),
81
+ value
82
+ );
154
83
  return value;
155
84
  }
156
85
 
157
- let output = '';
158
- let lastIndex = 0;
159
- for (const replacement of replacements) {
160
- const text = serialize(replacement.node, {
161
- precision: options.precision,
162
- calcName: replacement.calcName,
163
- unwrapSingleNegativeNumber: options.unwrapSingleNegativeNumber,
164
- });
165
- if (options.warnWhenCannotResolve && isUnresolvedResult(replacement.node)) {
166
- options.onWarn?.('Could not reduce expression: ' + value);
167
- }
168
- output += value.slice(lastIndex, replacement.start) + text;
169
- lastIndex = replacement.end;
86
+ const candidates = findCalculations(value, tokens, index);
87
+ const replacements = compileCandidates(candidates, {
88
+ options,
89
+ value,
90
+ tokens,
91
+ index,
92
+ });
93
+ if (replacements.length === 0) {
94
+ return value;
170
95
  }
171
- output += value.slice(lastIndex);
172
- return output;
96
+ const serializationOptions = {
97
+ precision: options.precision,
98
+ unwrapSingleNegativeNumber: options.unwrapSingleNegativeNumber,
99
+ unwrapSingleValue: options.unwrapSingleValue,
100
+ };
101
+ return applyReplacements(value, replacements, options, serializationOptions);
173
102
  }
174
103
 
175
- export { QUICK_MATH_TEST, hasPotentialMathFunction };
176
104
  export default reduceCalc;
package/types/index.d.ts CHANGED
@@ -3,6 +3,10 @@ export type PostCssCalcOptions = {
3
3
  warnWhenCannotResolve?: boolean;
4
4
  mediaQueries?: boolean;
5
5
  selectors?: boolean;
6
+ /**
7
+ * Serialize fully resolved finite scalar results without calculation syntax. Defaults to `false`.
8
+ */
9
+ unwrapSingleValue?: boolean;
6
10
  /**
7
11
  * Invoked when parse/simplify throws. Replaces the default `result.warn`.
8
12
  */
@@ -0,0 +1,18 @@
1
+ export type Node = import('./node.js').Node;
2
+ export type CalculationType = import('./functions.js').CalculationType;
3
+ export type AnalysisType = 'number' | 'unknown' | {
4
+ dimension: string | null;
5
+ };
6
+ export type Analysis = {
7
+ type: AnalysisType;
8
+ valid: boolean;
9
+ unresolved: boolean;
10
+ };
11
+ /**
12
+ * Analyze the original complete tree and return its root summary. Analysis
13
+ * validates and classifies the tree; it is not a rewrite plan.
14
+ * @param {Node} node
15
+ * @return {Analysis}
16
+ */
17
+ declare function analyze(node: Node): Analysis;
18
+ export { analyze };
@@ -0,0 +1,35 @@
1
+ export type CSSToken = import('@csstools/css-tokenizer').CSSToken;
2
+ /**
3
+ * Read-only delimiter navigation for one native token stream range.
4
+ */
5
+ declare class BlockIndex {
6
+ #private;
7
+ /**
8
+ * Build the delimiter index once for a token stream range. Matching remains
9
+ * LIFO: a mismatched closer is ignored and does not disturb the open stack.
10
+ *
11
+ * @param {CSSToken[]} tokens
12
+ * @param {number} start
13
+ * @param {number} end
14
+ */
15
+ constructor(tokens: CSSToken[], start: number, end: number);
16
+ /** @return {number} */
17
+ get maxDepth(): number;
18
+ /** @param {number} openPosition @param {number} [endPosition] @return {number} */
19
+ closeOf(openPosition: number, endPosition?: number): number;
20
+ /** @param {number} position @param {number} endPosition @return {number} */
21
+ nextComponent(position: number, endPosition: number): number;
22
+ /** @param {number} startPosition @param {number} endPosition @return {number} */
23
+ firstTopLevelComma(startPosition: number, endPosition: number): number;
24
+ }
25
+ /**
26
+ * Build the delimiter index once for a token stream range. Matching remains
27
+ * LIFO: a mismatched closer is ignored and does not disturb the open stack.
28
+ *
29
+ * @param {CSSToken[]} tokens
30
+ * @param {number} [start]
31
+ * @param {number} [end]
32
+ * @return {BlockIndex}
33
+ */
34
+ declare function indexBlocks(tokens: CSSToken[], start?: number, end?: number): BlockIndex;
35
+ export { indexBlocks };