postcss-calc 10.1.0 → 11.0.0-rc.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 (67) hide show
  1. package/README.md +56 -1
  2. package/package.json +19 -13
  3. package/src/index.js +122 -27
  4. package/src/lib/convertUnits.js +105 -0
  5. package/src/lib/node.js +189 -0
  6. package/src/lib/parser.js +314 -0
  7. package/src/lib/serialize.js +226 -0
  8. package/src/lib/simplify/abs.js +21 -0
  9. package/src/lib/simplify/atan2.js +25 -0
  10. package/src/lib/simplify/bucket.js +48 -0
  11. package/src/lib/simplify/call.js +59 -0
  12. package/src/lib/simplify/cancel.js +39 -0
  13. package/src/lib/simplify/clamp.js +26 -0
  14. package/src/lib/simplify/exp.js +18 -0
  15. package/src/lib/simplify/fold.js +63 -0
  16. package/src/lib/simplify/hypot.js +22 -0
  17. package/src/lib/simplify/inverse-trig.js +29 -0
  18. package/src/lib/simplify/log.js +25 -0
  19. package/src/lib/simplify/min-max.js +23 -0
  20. package/src/lib/simplify/mod-rem.js +47 -0
  21. package/src/lib/simplify/pow.js +20 -0
  22. package/src/lib/simplify/product.js +133 -0
  23. package/src/lib/simplify/round.js +113 -0
  24. package/src/lib/simplify/sign.js +22 -0
  25. package/src/lib/simplify/sqrt.js +18 -0
  26. package/src/lib/simplify/sum.js +83 -0
  27. package/src/lib/simplify/trig.js +40 -0
  28. package/src/lib/simplify.js +40 -0
  29. package/src/lib/tokenizer.js +115 -0
  30. package/types/index.d.ts +15 -17
  31. package/types/lib/convertUnits.d.ts +16 -0
  32. package/types/lib/node.d.ts +90 -0
  33. package/types/lib/parser.d.ts +39 -0
  34. package/types/lib/serialize.d.ts +20 -0
  35. package/types/lib/simplify/abs.d.ts +7 -0
  36. package/types/lib/simplify/atan2.d.ts +7 -0
  37. package/types/lib/simplify/bucket.d.ts +19 -0
  38. package/types/lib/simplify/call.d.ts +12 -0
  39. package/types/lib/simplify/cancel.d.ts +19 -0
  40. package/types/lib/simplify/clamp.d.ts +7 -0
  41. package/types/lib/simplify/exp.d.ts +7 -0
  42. package/types/lib/simplify/fold.d.ts +12 -0
  43. package/types/lib/simplify/hypot.d.ts +7 -0
  44. package/types/lib/simplify/inverse-trig.d.ts +7 -0
  45. package/types/lib/simplify/log.d.ts +7 -0
  46. package/types/lib/simplify/min-max.d.ts +8 -0
  47. package/types/lib/simplify/mod-rem.d.ts +8 -0
  48. package/types/lib/simplify/pow.d.ts +7 -0
  49. package/types/lib/simplify/product.d.ts +16 -0
  50. package/types/lib/simplify/round.d.ts +8 -0
  51. package/types/lib/simplify/sign.d.ts +7 -0
  52. package/types/lib/simplify/sqrt.d.ts +7 -0
  53. package/types/lib/simplify/sum.d.ts +18 -0
  54. package/types/lib/simplify/trig.d.ts +7 -0
  55. package/types/lib/simplify.d.ts +18 -0
  56. package/types/lib/tokenizer.d.ts +19 -0
  57. package/types/lib/type.d.ts +16 -0
  58. package/src/lib/convertUnit.js +0 -160
  59. package/src/lib/reducer.js +0 -390
  60. package/src/lib/stringifier.js +0 -98
  61. package/src/lib/transform.js +0 -109
  62. package/src/parser.d.ts +0 -79
  63. package/src/parser.js +0 -4200
  64. package/types/lib/convertUnit.d.ts +0 -8
  65. package/types/lib/reducer.d.ts +0 -14
  66. package/types/lib/stringifier.d.ts +0 -5
  67. package/types/lib/transform.d.ts +0 -6
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ const { baseOf, convert } = require('../convertUnits.js');
4
+
5
+ /**
6
+ * If `dims` contain exactly one numerator / one denominator pair with the
7
+ * same base type and convertible units, return the numeric factor produced
8
+ * by cancelling them and the list of remaining (uncancelled) dims.
9
+ * Otherwise return null. Used by `simplifyProduct` for typed division
10
+ * (§10.2). More complex cancellation (e.g. `px^2 / px`) is left
11
+ * unreduced — consumers rarely rely on it and the spec doesn't require it.
12
+ * @template {{ exponent: 1 | -1, value: number, unit: string }} D
13
+ * @param {D[]} dims
14
+ * @return {{ factor: number, remaining: D[] } | null}
15
+ */
16
+ function tryCancelPair(dims) {
17
+ if (dims.length !== 2) {
18
+ return null;
19
+ }
20
+ const [a, b] = /** @type {[D, D]} */ (dims);
21
+ if (a.exponent === b.exponent) {
22
+ return null;
23
+ }
24
+ const numerator = a.exponent === 1 ? a : b;
25
+ const denominator = a.exponent === 1 ? b : a;
26
+ const numBase = baseOf(numerator.unit);
27
+ const denBase = baseOf(denominator.unit);
28
+ if (!numBase || numBase !== denBase) {
29
+ return null;
30
+ }
31
+ const converted = convert(numerator.value, numerator.unit, denominator.unit);
32
+ if (converted === null) {
33
+ return null;
34
+ }
35
+ // denominator.value === 0 yields ±Infinity / NaN naturally (§10.9.1).
36
+ return { factor: converted / denominator.value, remaining: [] };
37
+ }
38
+
39
+ module.exports = { tryCancelPair };
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ const { num, dim } = require('../node.js');
4
+ const { foldConstArgs } = require('./fold.js');
5
+
6
+ /** @typedef {import('../node.js').Node} Node */
7
+
8
+ /**
9
+ * @param {Node[]} args
10
+ * @return {Node}
11
+ */
12
+ function simplifyClamp(args) {
13
+ if (args.length === 3) {
14
+ const fold = foldConstArgs(args);
15
+ if (fold !== null) {
16
+ const [lo, v, hi] = /** @type {[number, number, number]} */ (fold.values);
17
+ // Spec §10.8: clamp(MIN, VAL, MAX) = max(MIN, min(VAL, MAX)). The
18
+ // outer max(MIN, …) means MIN wins when MIN > MAX — not MAX.
19
+ const clamped = Math.max(lo, Math.min(v, hi));
20
+ return fold.unit === '' ? num(clamped) : dim(clamped, fold.unit);
21
+ }
22
+ }
23
+ return { type: 'Call', name: 'clamp', args };
24
+ }
25
+
26
+ module.exports = { simplifyClamp };
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ const { num } = require('../node.js');
4
+
5
+ /** @typedef {import('../node.js').Node} Node */
6
+
7
+ /**
8
+ * @param {Node[]} args
9
+ * @return {Node}
10
+ */
11
+ function simplifyExp(args) {
12
+ if (args.length !== 1 || args[0].type !== 'Num') {
13
+ return { type: 'Call', name: 'exp', args };
14
+ }
15
+ return num(Math.exp(args[0].value));
16
+ }
17
+
18
+ module.exports = { simplifyExp };
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ // Fold args to a common numeric representation if they share a type.
4
+ // Percentages never fold — ordering depends on property-context
5
+ // resolution we don't have. Matches csstools.
6
+
7
+ const { baseOf, convert } = require('../convertUnits.js');
8
+
9
+ /** @typedef {import('../node.js').Node} Node */
10
+ /** @typedef {import('../convertUnits.js').BaseType} BaseType */
11
+
12
+ /**
13
+ * @param {Node[]} args
14
+ * @return {{ values: number[], unit: string } | null}
15
+ */
16
+ function foldConstArgs(args) {
17
+ if (args.length === 0) {return null;}
18
+
19
+ const first = args[0];
20
+ if (first.type === 'Num') {
21
+ return foldNumberArgs(args);
22
+ }
23
+ if (first.type === 'Dim') {
24
+ const b = first.unit === '%' ? null : baseOf(first.unit);
25
+ if (!b) {return null;}
26
+ return foldDimArgs(args, first.unit, b);
27
+ }
28
+ return null;
29
+ }
30
+
31
+ /**
32
+ * @param {Node[]} args
33
+ * @return {{ values: number[], unit: '' } | null}
34
+ */
35
+ function foldNumberArgs(args) {
36
+ /** @type {number[]} */ const values = [];
37
+ for (const a of args) {
38
+ if (a.type !== 'Num') {return null;}
39
+ values.push(a.value);
40
+ }
41
+ return { values, unit: '' };
42
+ }
43
+
44
+ /**
45
+ * @param {Node[]} args
46
+ * @param {string} unit
47
+ * @param {BaseType} base
48
+ * @return {{ values: number[], unit: string } | null}
49
+ */
50
+ function foldDimArgs(args, unit, base) {
51
+ /** @type {number[]} */ const values = [];
52
+ for (const a of args) {
53
+ if (a.type !== 'Dim' || a.unit === '%' || baseOf(a.unit) !== base) {
54
+ return null;
55
+ }
56
+ const converted = convert(a.value, a.unit, unit);
57
+ if (converted === null) {return null;}
58
+ values.push(converted);
59
+ }
60
+ return { values, unit };
61
+ }
62
+
63
+ module.exports = { foldConstArgs };
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ // §10.5 — hypot. Empty args return null from foldConstArgs naturally.
4
+
5
+ const { num, dim } = require('../node.js');
6
+ const { foldConstArgs } = require('./fold.js');
7
+
8
+ /** @typedef {import('../node.js').Node} Node */
9
+
10
+ /**
11
+ * @param {Node[]} args
12
+ * @return {Node}
13
+ */
14
+ function simplifyHypot(args) {
15
+ const fold = foldConstArgs(args);
16
+ if (fold === null) {return { type: 'Call', name: 'hypot', args };}
17
+ const sumSq = fold.values.reduce((acc, v) => acc + v * v, 0);
18
+ const result = Math.sqrt(sumSq);
19
+ return fold.unit === '' ? num(result) : dim(result, fold.unit);
20
+ }
21
+
22
+ module.exports = { simplifyHypot };
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ // §10.4 — asin/acos/atan. Bare <number> in, <angle> in degrees out.
4
+
5
+ const { num, dim } = require('../node.js');
6
+
7
+ /** @typedef {import('../node.js').Node} Node */
8
+
9
+ const INVERSE_TRIG_OPS = /** @type {const} */ ({
10
+ asin: Math.asin,
11
+ acos: Math.acos,
12
+ atan: Math.atan,
13
+ });
14
+
15
+ /**
16
+ * @param {'asin' | 'acos' | 'atan'} name
17
+ * @param {Node[]} args
18
+ * @return {Node}
19
+ */
20
+ function simplifyInverseTrig(name, args) {
21
+ if (args.length !== 1) {return { type: 'Call', name, args };}
22
+ const a = args[0];
23
+ if (a.type !== 'Num') {return { type: 'Call', name, args };}
24
+ const radians = INVERSE_TRIG_OPS[name](a.value);
25
+ if (isNaN(radians)) {return num(NaN);}
26
+ return dim((radians * 180) / Math.PI, 'deg');
27
+ }
28
+
29
+ module.exports = { simplifyInverseTrig };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ const { num } = require('../node.js');
4
+
5
+ /** @typedef {import('../node.js').Node} Node */
6
+
7
+ /**
8
+ * @param {Node[]} args
9
+ * @return {Node}
10
+ */
11
+ function simplifyLog(args) {
12
+ if (args.length === 1 && args[0].type === 'Num') {
13
+ return num(Math.log(args[0].value));
14
+ }
15
+ if (
16
+ args.length === 2 &&
17
+ args[0].type === 'Num' &&
18
+ args[1].type === 'Num'
19
+ ) {
20
+ return num(Math.log(args[0].value) / Math.log(args[1].value));
21
+ }
22
+ return { type: 'Call', name: 'log', args };
23
+ }
24
+
25
+ module.exports = { simplifyLog };
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const { num, dim } = require('../node.js');
4
+ const { foldConstArgs } = require('./fold.js');
5
+
6
+ /** @typedef {import('../node.js').Node} Node */
7
+
8
+ /**
9
+ * @param {string} name
10
+ * @param {Node[]} args
11
+ * @return {Node}
12
+ */
13
+ function simplifyMinMax(name, args) {
14
+ const fold = foldConstArgs(args);
15
+ if (fold !== null) {
16
+ const fn = name.toLowerCase() === 'min' ? Math.min : Math.max;
17
+ const value = fn(...fold.values);
18
+ return fold.unit === '' ? num(value) : dim(value, fold.unit);
19
+ }
20
+ return { type: 'Call', name, args };
21
+ }
22
+
23
+ module.exports = { simplifyMinMax };
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const { num, dim } = require('../node.js');
4
+ const { foldConstArgs } = require('./fold.js');
5
+
6
+ /** @typedef {import('../node.js').Node} Node */
7
+
8
+ /**
9
+ * @param {'mod' | 'rem'} name
10
+ * @param {Node[]} args
11
+ * @return {Node}
12
+ */
13
+ function simplifyModRem(name, args) {
14
+ if (args.length !== 2) {return { type: 'Call', name, args };}
15
+ const fold = foldConstArgs(args);
16
+ if (fold === null) {return { type: 'Call', name, args };}
17
+ const [a, b] = /** @type {[number, number]} */ (fold.values);
18
+ const result = applyModRem(name, a, b);
19
+ // NaN results drop the unit (`mod(5px, 0px)` → `calc(NaN)`, not
20
+ // `calc(NaN * 1px)`). §10.12 unit-preserving form is a known divergence.
21
+ if (isNaN(result)) {return num(NaN);}
22
+ return fold.unit === '' ? num(result) : dim(result, fold.unit);
23
+ }
24
+
25
+ /**
26
+ * @param {'mod' | 'rem'} name
27
+ * @param {number} a
28
+ * @param {number} b
29
+ * @return {number}
30
+ */
31
+ function applyModRem(name, a, b) {
32
+ if (b === 0) {return NaN;}
33
+ if (!isFinite(a)) {return NaN;}
34
+ if (!isFinite(b)) {
35
+ // mod: result is NaN when A has opposite sign to B; otherwise A.
36
+ // rem: result is A regardless of signs.
37
+ if (name === 'mod' && a !== 0 && Math.sign(a) !== Math.sign(b)) {return NaN;}
38
+ return a;
39
+ }
40
+ if (name === 'rem') {
41
+ return a % b; // sign follows dividend
42
+ }
43
+ const result = a - b * Math.floor(a / b); // sign follows divisor
44
+ return result === 0 ? b * 0 : result;
45
+ }
46
+
47
+ module.exports = { simplifyModRem };
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ // §10.5 — pow is <number>-only.
4
+
5
+ const { num } = require('../node.js');
6
+
7
+ /** @typedef {import('../node.js').Node} Node */
8
+
9
+ /**
10
+ * @param {Node[]} args
11
+ * @return {Node}
12
+ */
13
+ function simplifyPow(args) {
14
+ if (args.length !== 2 || args[0].type !== 'Num' || args[1].type !== 'Num') {
15
+ return { type: 'Call', name: 'pow', args };
16
+ }
17
+ return num(Math.pow(args[0].value, args[1].value));
18
+ }
19
+
20
+ module.exports = { simplifyPow };
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ const { mkSum, mkProduct, num, dim } = require('../node.js');
4
+ const { tryCancelPair } = require('./cancel.js');
5
+
6
+ /**
7
+ * @typedef {import('../node.js').Node} Node
8
+ * @typedef {import('../node.js').Product} Product
9
+ * @typedef {import('../node.js').ProductFactor} ProductFactor
10
+ * @typedef {import('../simplify.js').SimplifyFn} SimplifyFn
11
+ */
12
+
13
+ /**
14
+ * @param {Product} product
15
+ * @param {SimplifyFn} simplify
16
+ * @return {Node}
17
+ */
18
+ function simplifyProduct(product, simplify) {
19
+ let coeff = 1;
20
+ /** @type {{exponent: 1 | -1, value: number, unit: string}[]} */
21
+ const dims = [];
22
+ /** @type {ProductFactor[]} */
23
+ const opaque = [];
24
+ /** @type {{exponent: 1 | -1, value: number}[]} */
25
+ const scalarChain = [];
26
+
27
+ /**
28
+ * @param {1 | -1} exponent
29
+ * @param {Node} n
30
+ * @return {void}
31
+ */
32
+ function processFactor(exponent, n) {
33
+ if (n.type === 'Product') {
34
+ for (const inner of n.factors) {
35
+ processFactor(
36
+ /** @type {1 | -1} */ (exponent * inner.exponent),
37
+ inner.node
38
+ );
39
+ }
40
+ return;
41
+ }
42
+ // Canonical negation form (see node.js's `negate`); flatten through it
43
+ // like a nested Product so cancellation below can see what's inside.
44
+ if (n.type === 'Sum' && n.terms.length === 1) {
45
+ processFactor(exponent, num(-1));
46
+ processFactor(exponent, n.terms[0].node);
47
+ return;
48
+ }
49
+ if (n.type === 'Num') {
50
+ if (exponent === 1) {
51
+ coeff *= n.value;
52
+ } else {
53
+ coeff /= n.value; // §10.9.1: 1/0 → ±Infinity, 0/0 → NaN per IEEE-754
54
+ }
55
+ scalarChain.push({ exponent, value: n.value });
56
+ return;
57
+ }
58
+ if (n.type === 'Dim') {
59
+ dims.push({ exponent, value: n.value, unit: n.unit });
60
+ scalarChain.push({ exponent, value: n.value });
61
+ return;
62
+ }
63
+ opaque.push({ exponent, node: n });
64
+ }
65
+
66
+ for (const f of product.factors) {
67
+ processFactor(f.exponent, simplify(f.node));
68
+ }
69
+
70
+ // §10.2 typed division. Higher-power cancellation (`px^2 / px`) is left
71
+ // unreduced — consumers don't rely on it and the spec doesn't require it.
72
+ const cancelled = tryCancelPair(dims);
73
+ if (cancelled !== null) {
74
+ coeff *= cancelled.factor;
75
+ }
76
+ const remainingDims = cancelled ? cancelled.remaining : dims;
77
+
78
+ // §10.10 distributive multiplication: `0.5 * (100vw - 10px)` → `50vw - 5px`.
79
+ // Only distribute when every Sum term is Num/Dim — partial distribution
80
+ // over opaque terms matches neither the legacy implementation nor csstools.
81
+ if (
82
+ remainingDims.length === 0 &&
83
+ opaque.length === 1 &&
84
+ opaque[0].exponent === 1 &&
85
+ opaque[0].node.type === 'Sum' &&
86
+ opaque[0].node.terms.every(
87
+ (t) => t.node.type === 'Num' || t.node.type === 'Dim'
88
+ )
89
+ ) {
90
+ const sum = opaque[0].node;
91
+ const distributed = sum.terms.map((t) => ({
92
+ sign: t.sign,
93
+ node: simplify(
94
+ mkProduct([
95
+ { exponent: 1, node: num(coeff) },
96
+ { exponent: 1, node: t.node },
97
+ ])
98
+ ),
99
+ }));
100
+ return mkSum(distributed);
101
+ }
102
+
103
+ if (
104
+ remainingDims.length === 1 &&
105
+ remainingDims[0].exponent === 1 &&
106
+ opaque.length === 0
107
+ ) {
108
+ const d = remainingDims[0];
109
+ const value = scalarChain.reduce(
110
+ (acc, f) => (f.exponent === 1 ? acc * f.value : acc / f.value),
111
+ 1
112
+ );
113
+ return dim(value, d.unit);
114
+ }
115
+
116
+ if (remainingDims.length === 0 && opaque.length === 0) {
117
+ return num(coeff);
118
+ }
119
+
120
+ /** @type {ProductFactor[]} */
121
+ const factors = [];
122
+ if (coeff !== 1) {
123
+ factors.push({ exponent: 1, node: num(coeff) });
124
+ }
125
+ for (const d of remainingDims) {
126
+ factors.push({ exponent: d.exponent, node: dim(d.value, d.unit) });
127
+ }
128
+ factors.push(...opaque);
129
+
130
+ return mkProduct(factors);
131
+ }
132
+
133
+ module.exports = { simplifyProduct };
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ const { num, dim } = require('../node.js');
4
+ const { foldConstArgs } = require('./fold.js');
5
+
6
+ /** @typedef {import('../node.js').Node} Node */
7
+
8
+ const ROUND_STRATEGIES = new Set(['nearest', 'up', 'down', 'to-zero']);
9
+ /** @typedef {'nearest' | 'up' | 'down' | 'to-zero'} RoundStrategy */
10
+
11
+ /**
12
+ * @param {Node[]} args
13
+ * @return {Node}
14
+ */
15
+ function simplifyRound(args) {
16
+ /** @type {RoundStrategy} */ let strategy = 'nearest';
17
+ let rest = args;
18
+ const first = args[0];
19
+ if (first?.type === 'Ident') {
20
+ const n = first.name.toLowerCase();
21
+ if (!ROUND_STRATEGIES.has(n)) {
22
+ // Unrecognized strategy ident — opaque rather than guessing intent.
23
+ return { type: 'Call', name: 'round', args };
24
+ }
25
+ strategy = /** @type {RoundStrategy} */ (n);
26
+ rest = args.slice(1);
27
+ }
28
+
29
+ /** @type {() => Node} */
30
+ const passthrough = () => ({
31
+ type: 'Call',
32
+ name: 'round',
33
+ args:
34
+ strategy === 'nearest'
35
+ ? rest
36
+ : [{ type: 'Ident', name: strategy }, ...rest],
37
+ });
38
+
39
+ // B omitted: defaults to 1 when A is <number>; else opaque.
40
+ const argsForFold = argsForRoundFold(rest);
41
+ const fold = argsForFold && foldConstArgs(argsForFold);
42
+ if (!fold) {return passthrough();}
43
+
44
+ const [a, b] = /** @type {[number, number]} */ (fold.values);
45
+ // Spec §10.7.1 non-finite step: NaN propagates; both infinite cancels to
46
+ // NaN. Infinite step with finite A is strategy-dependent — the multiples
47
+ // of an infinite step are {-∞, 0, +∞}, so `up` (ceiling) lands on +∞ for
48
+ // positive A and `down` (floor) lands on -∞ for negative A; every other
49
+ // case folds to ±0 carrying A's sign. Infinite-A / finite-B falls through
50
+ // to applyRound, where floor*b===ceil*b===±∞ collapses back to A
51
+ // (§10.3.1 "result is the same infinity").
52
+ if (isNaN(b)) {return num(NaN);}
53
+ if (!isFinite(b)) {
54
+ if (!isFinite(a)) {return num(NaN);}
55
+ let result;
56
+ if (strategy === 'up' && a > 0) {
57
+ result = Infinity;
58
+ } else if (strategy === 'down' && a < 0) {
59
+ result = -Infinity;
60
+ } else {
61
+ result = a < 0 || Object.is(a, -0) ? -0 : 0;
62
+ }
63
+ return fold.unit === '' ? num(result) : dim(result, fold.unit);
64
+ }
65
+
66
+ const result = applyRound(strategy, a, b);
67
+ if (isNaN(result)) {return num(NaN);}
68
+ return fold.unit === '' ? num(result) : dim(result, fold.unit);
69
+ }
70
+
71
+ /**
72
+ * @param {Node[]} args
73
+ * @return {Node[] | null}
74
+ */
75
+ function argsForRoundFold(args) {
76
+ if (args.length === 2) {return args;}
77
+ if (args.length === 1 && args[0].type === 'Num') {
78
+ return [args[0], num(1)];
79
+ }
80
+ return null;
81
+ }
82
+
83
+ /**
84
+ * @param {RoundStrategy} strategy
85
+ * @param {number} a
86
+ * @param {number} b
87
+ * @return {number}
88
+ */
89
+ function applyRound(strategy, a, b) {
90
+ if (b === 0) {return NaN;}
91
+ const q = a / b;
92
+ const c1 = Math.floor(q) * b;
93
+ const c2 = Math.ceil(q) * b;
94
+ // With negative B, floor*B > ceil*B; spec defines lower as closer to -∞.
95
+ const lower = Math.min(c1, c2);
96
+ const upper = Math.max(c1, c2);
97
+ if (lower === upper) {return a;}
98
+ switch (strategy) {
99
+ case 'up':
100
+ return upper;
101
+ case 'down':
102
+ return lower;
103
+ case 'to-zero':
104
+ return Math.abs(lower) <= Math.abs(upper) ? lower : upper;
105
+ case 'nearest': {
106
+ const dl = a - lower;
107
+ const du = upper - a;
108
+ return du <= dl ? upper : lower; // tie → upper (§10.3 line 978)
109
+ }
110
+ }
111
+ }
112
+
113
+ module.exports = { simplifyRound };
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ const { num } = require('../node.js');
4
+
5
+ /** @typedef {import('../node.js').Node} Node */
6
+
7
+ /**
8
+ * @param {Node[]} args
9
+ * @return {Node}
10
+ */
11
+ function simplifySign(args) {
12
+ if (args.length !== 1) {
13
+ return { type: 'Call', name: 'sign', args };
14
+ }
15
+ const a = args[0];
16
+ if (a.type === 'Num') {return num(Math.sign(a.value));}
17
+ // %: sign is property-context-dependent (§10.6) — opaque.
18
+ if (a.type === 'Dim' && a.unit !== '%') {return num(Math.sign(a.value));}
19
+ return { type: 'Call', name: 'sign', args: [a] };
20
+ }
21
+
22
+ module.exports = { simplifySign };
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ const { num } = require('../node.js');
4
+
5
+ /** @typedef {import('../node.js').Node} Node */
6
+
7
+ /**
8
+ * @param {Node[]} args
9
+ * @return {Node}
10
+ */
11
+ function simplifySqrt(args) {
12
+ if (args.length !== 1 || args[0].type !== 'Num') {
13
+ return { type: 'Call', name: 'sqrt', args };
14
+ }
15
+ return num(Math.sqrt(args[0].value));
16
+ }
17
+
18
+ module.exports = { simplifySqrt };
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ const { mkSum, num, dim } = require('../node.js');
4
+ const { baseOf } = require('../convertUnits.js');
5
+ const { mergeConvertibleBuckets } = require('./bucket.js');
6
+
7
+ /**
8
+ * @typedef {import('../node.js').Node} Node
9
+ * @typedef {import('../node.js').Sum} Sum
10
+ * @typedef {import('../node.js').SumTerm} SumTerm
11
+ * @typedef {import('../simplify.js').SimplifyFn} SimplifyFn
12
+ * @typedef {import('./bucket.js').UnitBucket} UnitBucket
13
+ */
14
+
15
+ /**
16
+ * @param {Sum} sum
17
+ * @param {SimplifyFn} simplify
18
+ * @return {Node}
19
+ */
20
+ function simplifySum(sum, simplify) {
21
+ // §10.10 two-phase dim handling: phase 1 buckets by exact unit (`1em + 1em`
22
+ // → `2em`); phase 2 merges convertible same-base buckets into the first-
23
+ // encountered unit. `100vh - 5rem - 10rem - 100px` → `-15rem` in phase 1,
24
+ // then vh/rem/px stay separate in phase 2 (none convert to each other).
25
+ let numTotal = 0;
26
+ /** @type {Map<string, UnitBucket>} */
27
+ const byUnit = new Map();
28
+ /** @type {SumTerm[]} */
29
+ const opaque = [];
30
+ let bucketOrder = 0;
31
+
32
+ /**
33
+ * @param {1 | -1} sign
34
+ * @param {Node} n
35
+ * @return {void}
36
+ */
37
+ function processTerm(sign, n) {
38
+ if (n.type === 'Sum') {
39
+ for (const inner of n.terms) {
40
+ processTerm(/** @type {1 | -1} */ (sign * inner.sign), inner.node);
41
+ }
42
+ return;
43
+ }
44
+ if (n.type === 'Num') {
45
+ numTotal += sign * n.value;
46
+ return;
47
+ }
48
+ if (n.type === 'Dim') {
49
+ const key = n.unit.toLowerCase();
50
+ const existing = byUnit.get(key);
51
+ if (existing) {
52
+ existing.total += sign * n.value;
53
+ } else {
54
+ byUnit.set(key, {
55
+ unit: n.unit,
56
+ total: sign * n.value,
57
+ base: baseOf(n.unit),
58
+ order: bucketOrder++,
59
+ });
60
+ }
61
+ return;
62
+ }
63
+ opaque.push({ sign, node: n });
64
+ }
65
+
66
+ for (const t of sum.terms) {
67
+ processTerm(t.sign, simplify(t.node));
68
+ }
69
+
70
+ // mkSum drops zero-valued Nums, so pushing the numeric total
71
+ // unconditionally is harmless. Zero-valued unit buckets are kept for
72
+ // type info (WPT calc-serialization-002).
73
+ /** @type {SumTerm[]} */
74
+ const terms = [{ sign: 1, node: num(numTotal) }];
75
+ for (const bucket of mergeConvertibleBuckets([...byUnit.values()])) {
76
+ terms.push({ sign: 1, node: dim(bucket.total, bucket.unit) });
77
+ }
78
+ terms.push(...opaque);
79
+
80
+ return mkSum(terms);
81
+ }
82
+
83
+ module.exports = { simplifySum };