postcss-calc 11.2.1 → 11.2.3

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 (49) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +47 -35
  3. package/package.json +15 -15
  4. package/src/index.js +2 -1
  5. package/src/lib/analyze.js +0 -6
  6. package/src/lib/compile.js +1 -1
  7. package/src/lib/function-analyzers.js +119 -0
  8. package/src/lib/functions.js +25 -175
  9. package/src/lib/node.js +61 -4
  10. package/src/lib/parser/opaque.js +152 -0
  11. package/src/lib/parser/tokens.js +242 -0
  12. package/src/lib/parser.js +32 -377
  13. package/src/lib/serialize/expression.js +329 -0
  14. package/src/lib/serialize/precedence.js +47 -0
  15. package/src/lib/serialize/precision.js +244 -0
  16. package/src/lib/serialize.js +24 -561
  17. package/src/lib/simplify/bucket.js +22 -5
  18. package/src/lib/simplify/cancel.js +35 -11
  19. package/src/lib/simplify/clamp.js +2 -2
  20. package/src/lib/simplify/exact.js +18 -0
  21. package/src/lib/simplify/fold.js +15 -1
  22. package/src/lib/simplify/min-max.js +2 -2
  23. package/src/lib/simplify/product-barrier.js +55 -0
  24. package/src/lib/simplify/product-quotient.js +141 -0
  25. package/src/lib/simplify/product.js +92 -48
  26. package/src/lib/simplify/sum.js +6 -2
  27. package/src/lib/simplify.js +22 -5
  28. package/src/lib/types.js +94 -0
  29. package/src/reduce.js +2 -1
  30. package/types/lib/function-analyzers.d.ts +27 -0
  31. package/types/lib/functions.d.ts +3 -28
  32. package/types/lib/node.d.ts +17 -2
  33. package/types/lib/parser/opaque.d.ts +12 -0
  34. package/types/lib/parser/tokens.d.ts +83 -0
  35. package/types/lib/parser.d.ts +4 -44
  36. package/types/lib/serialize/expression.d.ts +55 -0
  37. package/types/lib/serialize/precedence.d.ts +18 -0
  38. package/types/lib/serialize/precision.d.ts +102 -0
  39. package/types/lib/simplify/bucket.d.ts +4 -1
  40. package/types/lib/simplify/cancel.d.ts +11 -7
  41. package/types/lib/simplify/exact.d.ts +9 -0
  42. package/types/lib/simplify/fold.d.ts +14 -1
  43. package/types/lib/simplify/product-barrier.d.ts +26 -0
  44. package/types/lib/simplify/product-quotient.d.ts +48 -0
  45. package/types/lib/simplify/product.d.ts +3 -7
  46. package/types/lib/simplify/sum.d.ts +2 -1
  47. package/types/lib/simplify.d.ts +3 -8
  48. package/types/lib/types.d.ts +48 -0
  49. package/types/lib/calculation-type.d.ts +0 -18
@@ -1,17 +1,21 @@
1
1
  import { baseOf, convert } from '../convertUnits.js';
2
+ import { isExact } from './exact.js';
2
3
 
3
4
  /**
4
5
  * If `dims` contain exactly one numerator / one denominator pair with the
5
- * same base type and convertible units, return the numeric factor produced
6
- * by cancelling them and the list of remaining (uncancelled) dims.
7
- * Otherwise return null. Used by `simplifyProduct` for typed division
8
- * (§10.2). More complex cancellation (e.g. `px^2 / px`) is left
6
+ * same base type and convertible units, return the two sides expressed in a
7
+ * common unit, so the caller can fold them into its own numerator and
8
+ * denominator. Otherwise return null. Used by `simplifyProduct` for typed
9
+ * division (§10.2). More complex cancellation (e.g. `px^2 / px`) is left
9
10
  * unreduced — consumers rarely rely on it and the spec doesn't require it.
10
11
  * @template {{ exponent: 1 | -1, value: number, unit: string }} D
11
12
  * @param {D[]} dims
12
- * @return {{ factor: number, remaining: D[] } | null}
13
+ * @param {number | false} [precision] The denominator is converted into the
14
+ * numerator's unit when that is exact at this precision (`1px / 1in` →
15
+ * `1 / 96`); otherwise the numerator is converted (`1in / 1px` → `96 / 1`).
16
+ * @return {{ numerator: number, denominator: number, remaining: D[] } | null}
13
17
  */
14
- function tryCancelPair(dims) {
18
+ function tryCancelPair(dims, precision = false) {
15
19
  if (dims.length !== 2) {
16
20
  return null;
17
21
  }
@@ -26,12 +30,32 @@ function tryCancelPair(dims) {
26
30
  if (!numBase || numBase !== denBase) {
27
31
  return null;
28
32
  }
29
- const converted = convert(numerator.value, numerator.unit, denominator.unit);
30
- if (converted === null) {
31
- return null;
32
- }
33
+ const converted = convert(
34
+ denominator.value,
35
+ denominator.unit,
36
+ numerator.unit
37
+ );
33
38
  // denominator.value === 0 yields ±Infinity / NaN naturally (§10.9.1).
34
- return { factor: converted / denominator.value, remaining: [] };
39
+ if (converted !== null && isExact(converted, precision)) {
40
+ return {
41
+ numerator: numerator.value,
42
+ denominator: converted,
43
+ remaining: [],
44
+ };
45
+ }
46
+ const numConverted = convert(
47
+ numerator.value,
48
+ numerator.unit,
49
+ denominator.unit
50
+ );
51
+ if (numConverted !== null && isExact(numConverted, precision)) {
52
+ return {
53
+ numerator: numConverted,
54
+ denominator: denominator.value,
55
+ remaining: [],
56
+ };
57
+ }
58
+ return null;
35
59
  }
36
60
 
37
61
  export { tryCancelPair };
@@ -1,5 +1,5 @@
1
1
  import { call } from '../node.js';
2
- import { foldConstArgs, foldResult } from './fold.js';
2
+ import { foldConstArgs, chosenArg } from './fold.js';
3
3
  import { simplifyMinMax } from './min-max.js';
4
4
 
5
5
  /** @typedef {import('../node.js').Node} Node */
@@ -29,7 +29,7 @@ function simplifyClamp(args) {
29
29
  // Spec §10.8: clamp(MIN, VAL, MAX) = max(MIN, min(VAL, MAX)). The
30
30
  // outer max(MIN, …) means MIN wins when MIN > MAX — not MAX.
31
31
  const clamped = Math.max(lo, Math.min(v, hi));
32
- return foldResult(fold, clamped);
32
+ return chosenArg(args, fold, clamped);
33
33
  }
34
34
  }
35
35
  return call('clamp', args);
@@ -0,0 +1,18 @@
1
+ import { round } from '../serialize/precision.js';
2
+
3
+ /**
4
+ * A value is exact at `precision` when rounding it for output loses nothing
5
+ * beyond float noise.
6
+ * @param {number} value
7
+ * @param {number | false} precision
8
+ * @return {boolean}
9
+ */
10
+ function isExact(value, precision) {
11
+ if (precision === false || !Number.isFinite(value)) {
12
+ return true;
13
+ }
14
+ const snapped = Number(value.toPrecision(15));
15
+ return round(snapped, precision) === snapped;
16
+ }
17
+
18
+ export { isExact };
@@ -16,6 +16,20 @@ function foldResult(fold, value) {
16
16
  return fold.unit === '' ? num(value) : dim(value, fold.unit);
17
17
  }
18
18
 
19
+ /**
20
+ * Return the argument that produced a folded min/max/clamp value, so the
21
+ * result keeps its own unit, spelling and zero sign. Falls back to a fresh
22
+ * node when no argument matches (NaN).
23
+ * @param {Node[]} args
24
+ * @param {{ values: number[], unit: string }} fold
25
+ * @param {number} value
26
+ * @return {Node}
27
+ */
28
+ function chosenArg(args, fold, value) {
29
+ const index = fold.values.findIndex((v) => Object.is(v, value));
30
+ return index === -1 ? foldResult(fold, value) : args[index];
31
+ }
32
+
19
33
  /**
20
34
  * @param {Node[]} args
21
35
  * @return {{ values: number[], unit: string } | null}
@@ -75,4 +89,4 @@ function foldDimArgs(args, unit, base) {
75
89
  return { values, unit };
76
90
  }
77
91
 
78
- export { foldConstArgs, foldResult };
92
+ export { foldConstArgs, foldResult, chosenArg };
@@ -1,5 +1,5 @@
1
1
  import { call } from '../node.js';
2
- import { foldConstArgs, foldResult } from './fold.js';
2
+ import { foldConstArgs, chosenArg } from './fold.js';
3
3
 
4
4
  /** @typedef {import('../node.js').Node} Node */
5
5
 
@@ -13,7 +13,7 @@ function simplifyMinMax(name, args) {
13
13
  if (fold !== null) {
14
14
  const fn = name.toLowerCase() === 'min' ? Math.min : Math.max;
15
15
  const value = fn(...fold.values);
16
- return foldResult(fold, value);
16
+ return chosenArg(args, fold, value);
17
17
  }
18
18
  return call(name, args);
19
19
  }
@@ -0,0 +1,55 @@
1
+ import { num } from '../node.js';
2
+
3
+ /**
4
+ * @typedef {import('../node.js').Node} Node
5
+ * @typedef {import('../node.js').ProductFactor} ProductFactor
6
+ */
7
+
8
+ /**
9
+ * A substitution function (`var()`, `env()`, `attr()`, ...) is replaced by
10
+ * tokens, not by a value, so factors cannot move or cancel across it. Any
11
+ * function the parser does not recognise (`anchor()`, `foo()`, ...) is also an
12
+ * OpaqueCall and is treated the same, conservatively. A grouped Product is a
13
+ * parenthesized substitution and is equally opaque.
14
+ * @param {Node} node
15
+ * @return {boolean}
16
+ */
17
+ function isBarrier(node) {
18
+ return (
19
+ node.type === 'OpaqueCall' ||
20
+ (node.type === 'Product' && node.grouped === true)
21
+ );
22
+ }
23
+
24
+ /**
25
+ * Flatten a simplified factor through nested ungrouped Products and the
26
+ * canonical negation form, so barriers are visible at the top level.
27
+ * @param {ProductFactor[]} out
28
+ * @param {1 | -1} exponent
29
+ * @param {Node} node
30
+ * @return {boolean} Whether a barrier was pushed.
31
+ */
32
+ function flattenFactor(out, exponent, node) {
33
+ if (node.type === 'Product' && !node.grouped) {
34
+ let barrier = false;
35
+ for (const inner of node.factors) {
36
+ if (
37
+ flattenFactor(
38
+ out,
39
+ /** @type {1 | -1} */ (exponent * inner.exponent),
40
+ inner.node
41
+ )
42
+ )
43
+ barrier = true;
44
+ }
45
+ return barrier;
46
+ }
47
+ if (node.type === 'Sum' && node.terms.length === 1) {
48
+ out.push({ exponent, node: num(-1) });
49
+ return flattenFactor(out, exponent, node.terms[0].node);
50
+ }
51
+ out.push({ exponent, node });
52
+ return isBarrier(node);
53
+ }
54
+
55
+ export { flattenFactor, isBarrier };
@@ -0,0 +1,141 @@
1
+ import { mkProduct, num, dim } from '../node.js';
2
+ import { isExact } from './exact.js';
3
+
4
+ /**
5
+ * @typedef {import('../node.js').Node} Node
6
+ * @typedef {import('../node.js').ProductFactor} ProductFactor
7
+ */
8
+
9
+ /**
10
+ * @param {number} a non-negative
11
+ * @param {number} b non-negative
12
+ * @return {number}
13
+ */
14
+ function gcd(a, b) {
15
+ let x = a;
16
+ let y = b;
17
+ while (y !== 0) {
18
+ [x, y] = [y, x % y];
19
+ }
20
+ return x || 1;
21
+ }
22
+
23
+ /**
24
+ * @param {{exponent: 1 | -1, value: number}[]} chain
25
+ * @return {number}
26
+ */
27
+ function chainValue(chain) {
28
+ let value = 1;
29
+ for (const f of chain) {
30
+ value = f.exponent === 1 ? value * f.value : value / f.value;
31
+ }
32
+ return value;
33
+ }
34
+
35
+ /**
36
+ * Whether the opaque factors are a single Sum of only Num/Dim terms.
37
+ * @param {ProductFactor[]} opaque
38
+ * @return {boolean}
39
+ */
40
+ function isScalarSum(opaque) {
41
+ return (
42
+ opaque.length === 1 &&
43
+ opaque[0].exponent === 1 &&
44
+ opaque[0].node.type === 'Sum' &&
45
+ opaque[0].node.terms.every(
46
+ (t) => t.node.type === 'Num' || t.node.type === 'Dim'
47
+ )
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Whether a folded quotient prints exactly. A quotient distributed over a
53
+ * scalar Sum is judged term by term, so `(3px + 6em) / 3` still folds.
54
+ * @param {number} value
55
+ * @param {ProductFactor[]} opaque
56
+ * @param {boolean} distributable
57
+ * @param {number | false} precision
58
+ * @return {boolean}
59
+ */
60
+ function foldsExactly(value, opaque, distributable, precision) {
61
+ if (!Number.isFinite(value) || isExact(value, precision)) {
62
+ return true;
63
+ }
64
+ if (!distributable) {
65
+ return false;
66
+ }
67
+ const sum = /** @type {import('../node.js').Sum} */ (opaque[0].node);
68
+ return sum.terms.every((t) =>
69
+ isExact(
70
+ value *
71
+ /** @type {import('../node.js').Num | import('../node.js').Dim} */ (
72
+ t.node
73
+ ).value,
74
+ precision
75
+ )
76
+ );
77
+ }
78
+
79
+ /**
80
+ * Emit an inexact quotient as `numerator * dims * opaque / denominator`, with
81
+ * integer parts reduced by their gcd. A lone Dim absorbs the numerator.
82
+ * @param {number} numerator
83
+ * @param {number} denominator
84
+ * @param {{value: number, unit: string, rawUnit?: string} | null} loneDim
85
+ * @param {{exponent: 1 | -1, value: number, unit: string, rawUnit?: string}[]} dims
86
+ * @param {ProductFactor[]} opaque
87
+ * @param {number | false} precision
88
+ * @return {Node | null} null when a part would be rounded on output
89
+ */
90
+ function rationalProduct(
91
+ numerator,
92
+ denominator,
93
+ loneDim,
94
+ dims,
95
+ opaque,
96
+ precision
97
+ ) {
98
+ // Drop float noise (`1.9999999999999993 * 100`) so the parts print as
99
+ // written and the gcd below can reduce them.
100
+ let n = Number(
101
+ (loneDim === null ? numerator : numerator * loneDim.value).toPrecision(15)
102
+ );
103
+ let d = Number(denominator.toPrecision(15));
104
+ if (!isExact(n, precision) || !isExact(d, precision)) {
105
+ return null;
106
+ }
107
+ if (Number.isSafeInteger(n) && Number.isSafeInteger(d)) {
108
+ const g = gcd(Math.abs(n), Math.abs(d));
109
+ n /= g;
110
+ d /= g;
111
+ }
112
+ if (d < 0) {
113
+ n = -n;
114
+ d = -d;
115
+ }
116
+ /** @type {ProductFactor[]} */
117
+ const factors = [];
118
+ if (loneDim !== null) {
119
+ factors.push({
120
+ exponent: 1,
121
+ node: dim(n, loneDim.unit, loneDim.rawUnit),
122
+ });
123
+ } else {
124
+ if (n !== 1) {
125
+ factors.push({ exponent: 1, node: num(n) });
126
+ }
127
+ for (const dm of dims) {
128
+ factors.push({
129
+ exponent: dm.exponent,
130
+ node: dim(dm.value, dm.unit, dm.rawUnit),
131
+ });
132
+ }
133
+ }
134
+ factors.push(...opaque);
135
+ if (d !== 1) {
136
+ factors.push({ exponent: -1, node: num(d) });
137
+ }
138
+ return mkProduct(factors);
139
+ }
140
+
141
+ export { chainValue, foldsExactly, isScalarSum, rationalProduct };
@@ -1,5 +1,12 @@
1
- import { mkSum, mkProduct, num, dim } from '../node.js';
1
+ import { mkSum, mkProduct, mkGroup, num, dim } from '../node.js';
2
2
  import { tryCancelPair } from './cancel.js';
3
+ import { flattenFactor, isBarrier } from './product-barrier.js';
4
+ import {
5
+ chainValue,
6
+ foldsExactly,
7
+ isScalarSum,
8
+ rationalProduct,
9
+ } from './product-quotient.js';
3
10
 
4
11
  /**
5
12
  * @typedef {import('../node.js').Node} Node
@@ -9,12 +16,20 @@ import { tryCancelPair } from './cancel.js';
9
16
  */
10
17
 
11
18
  /**
12
- * @param {Product} product
19
+ * Fold a run of factors that contains no substitution barrier.
20
+ * @param {ProductFactor[]} items Simplified factors, flattened by the caller.
21
+ * @param {number} start
22
+ * @param {number} end
13
23
  * @param {SimplifyFn} simplify
24
+ * @param {number | false} precision
14
25
  * @return {Node}
15
26
  */
16
- function simplifyProduct(product, simplify) {
27
+ function foldFactors(items, start, end, simplify, precision) {
17
28
  let coeff = 1;
29
+ // Num factors by exponent, so an inexact quotient can be re-emitted as a
30
+ // reduced `numerator / denominator`.
31
+ let numerator = 1;
32
+ let denominator = 1;
18
33
  /** @type {{exponent: 1 | -1, value: number, unit: string, rawUnit?: string}[]} */
19
34
  const dims = [];
20
35
  /** @type {ProductFactor[]} */
@@ -28,27 +43,13 @@ function simplifyProduct(product, simplify) {
28
43
  * @return {void}
29
44
  */
30
45
  function processFactor(exponent, n) {
31
- if (n.type === 'Product') {
32
- for (const inner of n.factors) {
33
- processFactor(
34
- /** @type {1 | -1} */ (exponent * inner.exponent),
35
- inner.node
36
- );
37
- }
38
- return;
39
- }
40
- // Canonical negation form (see node.js's `negate`); flatten through it
41
- // like a nested Product so cancellation below can see what's inside.
42
- if (n.type === 'Sum' && n.terms.length === 1) {
43
- processFactor(exponent, num(-1));
44
- processFactor(exponent, n.terms[0].node);
45
- return;
46
- }
47
46
  if (n.type === 'Num') {
48
47
  if (exponent === 1) {
49
48
  coeff *= n.value;
49
+ numerator *= n.value;
50
50
  } else {
51
51
  coeff /= n.value; // §10.9.1: 1/0 → ±Infinity, 0/0 → NaN per IEEE-754
52
+ denominator *= n.value;
52
53
  }
53
54
  scalarChain.push({ exponent, value: n.value });
54
55
  return;
@@ -61,31 +62,50 @@ function simplifyProduct(product, simplify) {
61
62
  opaque.push({ exponent, node: n });
62
63
  }
63
64
 
64
- for (const f of product.factors) {
65
- processFactor(f.exponent, simplify(f.node));
65
+ for (let i = start; i < end; i++) {
66
+ processFactor(items[i].exponent, items[i].node);
66
67
  }
67
68
 
68
69
  // §10.2 typed division. Higher-power cancellation (`px^2 / px`) is left
69
70
  // unreduced — consumers don't rely on it and the spec doesn't require it.
70
- const cancelled = tryCancelPair(dims);
71
+ const cancelled = tryCancelPair(dims, precision);
71
72
  if (cancelled !== null) {
72
- coeff *= cancelled.factor;
73
+ coeff *= cancelled.numerator / cancelled.denominator;
74
+ numerator *= cancelled.numerator;
75
+ denominator *= cancelled.denominator;
73
76
  }
77
+ const divides = denominator !== 1 || cancelled !== null;
74
78
  const remainingDims = cancelled ? cancelled.remaining : dims;
79
+ const loneDim =
80
+ remainingDims.length === 1 &&
81
+ remainingDims[0].exponent === 1 &&
82
+ opaque.length === 0
83
+ ? remainingDims[0]
84
+ : null;
85
+ const value = loneDim === null ? coeff : chainValue(scalarChain);
86
+ const distributable = remainingDims.length === 0 && isScalarSum(opaque);
87
+
88
+ // An inexact quotient stays symbolic when its numerator and denominator
89
+ // print exactly; otherwise it is folded and rounded at serialization.
90
+ if (divides && !foldsExactly(value, opaque, distributable, precision)) {
91
+ const rational = rationalProduct(
92
+ numerator,
93
+ denominator,
94
+ loneDim,
95
+ remainingDims,
96
+ opaque,
97
+ precision
98
+ );
99
+ if (rational !== null) {
100
+ return rational;
101
+ }
102
+ }
75
103
 
76
104
  // §10.10 distributive multiplication: `0.5 * (100vw - 10px)` → `50vw - 5px`.
77
105
  // Only distribute when every Sum term is Num/Dim — partial distribution
78
106
  // over opaque terms matches neither the legacy implementation nor csstools.
79
- if (
80
- remainingDims.length === 0 &&
81
- opaque.length === 1 &&
82
- opaque[0].exponent === 1 &&
83
- opaque[0].node.type === 'Sum' &&
84
- opaque[0].node.terms.every(
85
- (t) => t.node.type === 'Num' || t.node.type === 'Dim'
86
- )
87
- ) {
88
- const sum = opaque[0].node;
107
+ if (distributable) {
108
+ const sum = /** @type {import('../node.js').Sum} */ (opaque[0].node);
89
109
  const distributed = sum.terms.map((t) => ({
90
110
  sign: t.sign,
91
111
  node: simplify(
@@ -98,21 +118,8 @@ function simplifyProduct(product, simplify) {
98
118
  return mkSum(distributed);
99
119
  }
100
120
 
101
- if (
102
- remainingDims.length === 1 &&
103
- remainingDims[0].exponent === 1 &&
104
- opaque.length === 0
105
- ) {
106
- const d = remainingDims[0];
107
- let value = 1;
108
- for (const f of scalarChain) {
109
- if (f.exponent === 1) {
110
- value = value * f.value;
111
- } else {
112
- value = value / f.value;
113
- }
114
- }
115
- return dim(value, d.unit, d.rawUnit);
121
+ if (loneDim !== null) {
122
+ return dim(value, loneDim.unit, loneDim.rawUnit);
116
123
  }
117
124
 
118
125
  if (remainingDims.length === 0 && opaque.length === 0) {
@@ -135,4 +142,41 @@ function simplifyProduct(product, simplify) {
135
142
  return mkProduct(factors);
136
143
  }
137
144
 
145
+ /**
146
+ * @param {Product} product
147
+ * @param {SimplifyFn} simplify
148
+ * @param {number | false} [precision] A quotient that is not exact at this
149
+ * precision stays symbolic (`100% / 3`) instead of being rounded.
150
+ * @return {Node}
151
+ */
152
+ function simplifyProduct(product, simplify, precision = false) {
153
+ /** @type {ProductFactor[]} */
154
+ const items = [];
155
+ let hasBarrier = false;
156
+ for (const f of product.factors) {
157
+ if (flattenFactor(items, f.exponent, simplify(f.node))) hasBarrier = true;
158
+ }
159
+ if (!hasBarrier)
160
+ return foldFactors(items, 0, items.length, simplify, precision);
161
+
162
+ // Fold each run between barriers on its own; never move or cancel a factor
163
+ // across a barrier, and keep each barrier's own operator.
164
+ /** @type {ProductFactor[]} */
165
+ const factors = [];
166
+ let start = 0;
167
+ for (let i = 0; i <= items.length; i++) {
168
+ if (i < items.length && !isBarrier(items[i].node)) continue;
169
+ if (i - start === 1) factors.push(items[start]);
170
+ else if (i - start > 1)
171
+ factors.push({
172
+ exponent: 1,
173
+ node: foldFactors(items, start, i, simplify, precision),
174
+ });
175
+ if (i < items.length) factors.push(items[i]);
176
+ start = i + 1;
177
+ }
178
+ const result = mkProduct(factors);
179
+ return product.grouped === true ? mkGroup(result) : result;
180
+ }
181
+
138
182
  export { simplifyProduct };
@@ -26,9 +26,10 @@ function denoise(total, scale) {
26
26
  /**
27
27
  * @param {Sum} sum
28
28
  * @param {SimplifyFn} simplify
29
+ * @param {number | false} [precision]
29
30
  * @return {Node}
30
31
  */
31
- function simplifySum(sum, simplify) {
32
+ function simplifySum(sum, simplify, precision = false) {
32
33
  // §10.10 two-phase dim handling: phase 1 buckets by exact unit (`1em + 1em`
33
34
  // → `2em`); phase 2 merges convertible same-base buckets into the first-
34
35
  // encountered unit. `100vh - 5rem - 10rem - 100px` → `-15rem` in phase 1,
@@ -106,7 +107,10 @@ function simplifySum(sum, simplify) {
106
107
  const terms = hasNum
107
108
  ? [{ sign: /** @type {1} */ (1), node: num(denoise(numTotal, numScale)) }]
108
109
  : [];
109
- for (const bucket of mergeConvertibleBuckets([...byUnit.values()])) {
110
+ for (const bucket of mergeConvertibleBuckets(
111
+ [...byUnit.values()],
112
+ precision
113
+ )) {
110
114
  terms.push({
111
115
  sign: 1,
112
116
  node: dim(
@@ -6,7 +6,8 @@ import { simplifySum } from './simplify/sum.js';
6
6
  import { simplifyProduct } from './simplify/product.js';
7
7
  import { simplifyCall } from './simplify/call.js';
8
8
  import { simplifyComponents } from './opaque.js';
9
- import { opaqueCall } from './node.js';
9
+ import { groupSubstitution, opaqueCall } from './node.js';
10
+ import { isCalculationFunction } from './functions.js';
10
11
  import { assertDepth } from './limits.js';
11
12
 
12
13
  /**
@@ -17,17 +18,33 @@ import { assertDepth } from './limits.js';
17
18
  * @typedef {(node: Node) => Node} SimplifyFn
18
19
  */
19
20
 
21
+ /**
22
+ * A nested calc() is a parenthesized group: with unresolved tokens inside,
23
+ * its operands must not merge into the surrounding sum or product.
24
+ * @param {Node} value
25
+ * @param {(value: Node) => Node} child
26
+ * @return {Node}
27
+ */
28
+ function operand(value, child) {
29
+ const result = child(value);
30
+ return value.type === 'Call' && isCalculationFunction(value.name)
31
+ ? groupSubstitution(result)
32
+ : result;
33
+ }
34
+
20
35
  /**
21
36
  * Simplify is an independent, composable AST transformation. It may
22
37
  * synthesize canonical nodes while preserving the Node -> Node contract.
23
38
  * @param {Node} node
39
+ * @param {number | false} [precision] When set, divisions and unit
40
+ * conversions that are not exact at this precision stay symbolic.
24
41
  * @param {number} [depth]
25
42
  * @return {Node}
26
43
  */
27
- function simplify(node, depth = 0) {
44
+ function simplify(node, precision = false, depth = 0) {
28
45
  assertDepth(depth);
29
46
  /** @param {Node} value */
30
- const child = (value) => simplify(value, depth + 1);
47
+ const child = (value) => simplify(value, precision, depth + 1);
31
48
  switch (node.type) {
32
49
  case 'Num':
33
50
  case 'Dim':
@@ -42,9 +59,9 @@ function simplify(node, depth = 0) {
42
59
  node.rawName
43
60
  );
44
61
  case 'Sum':
45
- return simplifySum(node, child);
62
+ return simplifySum(node, (value) => operand(value, child), precision);
46
63
  case 'Product':
47
- return simplifyProduct(node, child);
64
+ return simplifyProduct(node, (value) => operand(value, child), precision);
48
65
  }
49
66
  }
50
67