postcss-calc 10.1.1 → 11.0.0-rc.1

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 +20 -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 +239 -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 +49 -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 +100 -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 +24 -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 +11 -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 -396
  60. package/src/lib/stringifier.js +0 -106
  61. package/src/lib/transform.js +0 -109
  62. package/src/parser.d.ts +0 -89
  63. package/src/parser.js +0 -4222
  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,314 @@
1
+ 'use strict';
2
+
3
+ // Pratt parser. +/- emit Sum nodes; */÷ emit Product nodes. node.js
4
+ // constructors flatten and normalize on construction, so the parser
5
+ // never produces a Binary node.
6
+
7
+ const { mkSum, mkProduct, negate } = require('./node.js');
8
+
9
+ /**
10
+ * @typedef {import('./tokenizer.js').Token} Token
11
+ * @typedef {import('./tokenizer.js').TokenType} TokenType
12
+ * @typedef {import('./node.js').Node} Node
13
+ * @typedef {(p: Parser, token: Token) => Node} PrefixParselet
14
+ * @typedef {{lbp: number, parse: (p: Parser, left: Node, token: Token) => Node}} InfixParselet
15
+ */
16
+
17
+ /**
18
+ * @param {Token} t
19
+ * @param {string} value
20
+ * @return {boolean}
21
+ */
22
+ function isPunct(t, value) {
23
+ return t.type === 'punct' && t.value === value;
24
+ }
25
+
26
+ /**
27
+ * §10.9 — case-insensitive except for NaN.
28
+ * @param {string} name
29
+ * @return {Node | null}
30
+ */
31
+ function foldCalcKeyword(name) {
32
+ // `NaN` and `-NaN` are spec-defined math constants (§10.7.1). The signed
33
+ // form arrives as a single ident because CSS Syntax tokenizes leading
34
+ // `-` + ident-start as one ident-token.
35
+ if (name === 'NaN' || name === '-NaN') {
36
+ return { type: 'Num', value: NaN };
37
+ }
38
+ switch (name.toLowerCase()) {
39
+ case 'pi':
40
+ return { type: 'Num', value: Math.PI };
41
+ case 'e':
42
+ return { type: 'Num', value: Math.E };
43
+ case 'infinity':
44
+ return { type: 'Num', value: Infinity };
45
+ case '-infinity':
46
+ return { type: 'Num', value: -Infinity };
47
+ }
48
+ return null;
49
+ }
50
+
51
+ class Parser {
52
+ /**
53
+ * @param {Token[]} tokens
54
+ */
55
+ constructor(tokens) {
56
+ /** @private */
57
+ this.i = 0;
58
+ /** @private @readonly */
59
+ this.tokens = tokens;
60
+ }
61
+
62
+ /** @return {Token} */
63
+ peek() {
64
+ return this.tokens[this.i];
65
+ }
66
+
67
+ /** @return {Token} */
68
+ next() {
69
+ return this.tokens[this.i++];
70
+ }
71
+
72
+ /**
73
+ * @param {TokenType} type
74
+ * @param {string} [value]
75
+ * @return {Token}
76
+ */
77
+ expect(type, value) {
78
+ const t = this.next();
79
+ if (t.type !== type || (value !== undefined && t.value !== value)) {
80
+ const want = value ?? type;
81
+ throw new Error(
82
+ `Expected ${want} at position ${t.pos}, got "${t.value}"`
83
+ );
84
+ }
85
+ return t;
86
+ }
87
+
88
+ /**
89
+ * @param {number} [minBp]
90
+ * @return {Node}
91
+ */
92
+ parseExpr(minBp = 0) {
93
+ const t = this.next();
94
+ const key = t.type === 'punct' ? t.value : t.type;
95
+ const prefix = PREFIX[key];
96
+ if (!prefix) {
97
+ throw new Error(`Unexpected token "${t.value}" at position ${t.pos}`);
98
+ }
99
+ let left = prefix(this, t);
100
+
101
+ while (true) {
102
+ const nxt = this.peek();
103
+ const infixKey = nxt.type === 'punct' ? nxt.value : nxt.type;
104
+ const rule = INFIX[infixKey];
105
+ if (!rule || rule.lbp < minBp) {
106
+ break;
107
+ }
108
+ this.next();
109
+ left = rule.parse(this, left, nxt);
110
+ }
111
+
112
+ return left;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * @param {Node} left
118
+ * @param {Node} right
119
+ * @param {1 | -1} rightSign
120
+ * @return {Node}
121
+ */
122
+ function addTerm(left, right, rightSign) {
123
+ return mkSum([
124
+ { sign: 1, node: left },
125
+ { sign: rightSign, node: right },
126
+ ]);
127
+ }
128
+
129
+ /**
130
+ * @param {Node} left
131
+ * @param {Node} right
132
+ * @param {1 | -1} rightExp
133
+ * @return {Node}
134
+ */
135
+ function mulFactor(left, right, rightExp) {
136
+ return mkProduct([
137
+ { exponent: 1, node: left },
138
+ { exponent: rightExp, node: right },
139
+ ]);
140
+ }
141
+
142
+ const ADD_BP = 1;
143
+ const MUL_BP = 3;
144
+ const UNARY_BP = 7;
145
+
146
+ /**
147
+ * Functions whose argument list isn't a comma-separated list of calc
148
+ * expressions. Their bodies are slurped as opaque space-separated tokens
149
+ * and round-tripped verbatim. anchor() / anchor-size() use the
150
+ * `<anchor-name> <anchor-side>` shape (CSS Anchor Positioning).
151
+ */
152
+ const OPAQUE_ARG_FUNCTIONS = new Set(['anchor', 'anchor-size']);
153
+
154
+ /**
155
+ * Reconstruct a token's source text for opaque-arg slurping.
156
+ * @param {Token} t
157
+ * @return {string}
158
+ */
159
+ function tokenText(t) {
160
+ if (t.type === 'dimension') {return `${t.value}${t.unit ?? ''}`;}
161
+ return t.value;
162
+ }
163
+
164
+ /**
165
+ * Parse the body of an opaque-arg call, with `(` already consumed.
166
+ * @param {Parser} p
167
+ * @param {string} name
168
+ * @return {Node}
169
+ */
170
+ function parseOpaqueCall(p, name) {
171
+ /** @type {Node[]} */
172
+ const args = [];
173
+ let buf = '';
174
+ let depth = 1;
175
+ const flush = () => {
176
+ const trimmed = buf.trim();
177
+ if (trimmed) {args.push({ type: 'Ident', name: trimmed });}
178
+ buf = '';
179
+ };
180
+ while (true) {
181
+ const tk = p.peek();
182
+ if (tk.type === 'eof') {
183
+ throw new Error(`Unclosed ${name}( at position ${tk.pos}`);
184
+ }
185
+ if (tk.type === 'punct') {
186
+ if (tk.value === '(') {depth++;}
187
+ else if (tk.value === ')') {
188
+ depth--;
189
+ if (depth === 0) {
190
+ p.next();
191
+ flush();
192
+ return { type: 'Call', name, args };
193
+ }
194
+ } else if (tk.value === ',' && depth === 1) {
195
+ p.next();
196
+ flush();
197
+ continue;
198
+ }
199
+ }
200
+ if (tk.ws && buf) {buf += ' ';}
201
+ buf += tokenText(tk);
202
+ p.next();
203
+ }
204
+ }
205
+
206
+ /**
207
+ * §10.1 requires whitespace on both sides of a binary `+` / `-`. Without
208
+ * it, CSS tokenization treats `1px+2px` as two tokens with no operator
209
+ * between them (browsers reject this). We enforce the rule here by
210
+ * checking the token's `ws` flag (whitespace before the `+`/`-`) and the
211
+ * following token's flag (whitespace after).
212
+ * @param {Parser} p
213
+ * @param {Token} token
214
+ * @return {void}
215
+ */
216
+ function requireSurroundingWs(p, token) {
217
+ const next = p.peek();
218
+ if (!token.ws || !next.ws) {
219
+ throw new Error(
220
+ `"${token.value}" must be surrounded by whitespace at position ${token.pos}`
221
+ );
222
+ }
223
+ }
224
+
225
+ /** @type {Record<string, PrefixParselet>} */
226
+ const PREFIX = {
227
+ number: (_p, t) => ({ type: 'Num', value: parseFloat(t.value) }),
228
+
229
+ // Unit case normalization per §10.12: `1PX` serializes as `1px`.
230
+ dimension: (_p, t) => ({
231
+ type: 'Dim',
232
+ value: parseFloat(t.value),
233
+ unit: t.unit === '%' ? '%' : /** @type {string} */ (t.unit).toLowerCase(),
234
+ }),
235
+
236
+ ident: (p, t) => {
237
+ const nxt = p.peek();
238
+ if (nxt.type === 'punct' && nxt.value === '(') {
239
+ p.next();
240
+ if (OPAQUE_ARG_FUNCTIONS.has(t.value.toLowerCase())) {
241
+ return parseOpaqueCall(p, t.value);
242
+ }
243
+ /** @type {Node[]} */
244
+ const args = [];
245
+ if (!isPunct(p.peek(), ')')) {
246
+ args.push(p.parseExpr(0));
247
+ while (isPunct(p.peek(), ',')) {
248
+ p.next();
249
+ args.push(p.parseExpr(0));
250
+ }
251
+ }
252
+ p.expect('punct', ')');
253
+ return { type: 'Call', name: t.value, args };
254
+ }
255
+ const kw = foldCalcKeyword(t.value);
256
+ if (kw) {
257
+ return kw;
258
+ }
259
+ return { type: 'Ident', name: t.value };
260
+ },
261
+
262
+ '(': (p) => {
263
+ const e = p.parseExpr(0);
264
+ p.expect('punct', ')');
265
+ return e;
266
+ },
267
+
268
+ '-': (p) => negate(p.parseExpr(UNARY_BP)),
269
+ '+': (p) => p.parseExpr(UNARY_BP),
270
+ };
271
+
272
+ /** @type {Record<string, InfixParselet>} */
273
+ const INFIX = {
274
+ '+': {
275
+ lbp: ADD_BP,
276
+ parse: (p, left, token) => {
277
+ requireSurroundingWs(p, token);
278
+ return addTerm(left, p.parseExpr(ADD_BP + 1), 1);
279
+ },
280
+ },
281
+ '-': {
282
+ lbp: ADD_BP,
283
+ parse: (p, left, token) => {
284
+ requireSurroundingWs(p, token);
285
+ return addTerm(left, p.parseExpr(ADD_BP + 1), -1);
286
+ },
287
+ },
288
+ '*': {
289
+ lbp: MUL_BP,
290
+ parse: (p, left) => mulFactor(left, p.parseExpr(MUL_BP + 1), 1),
291
+ },
292
+ '/': {
293
+ lbp: MUL_BP,
294
+ parse: (p, left) => mulFactor(left, p.parseExpr(MUL_BP + 1), -1),
295
+ },
296
+ };
297
+
298
+ /**
299
+ * @param {Token[]} tokens
300
+ * @return {Node}
301
+ */
302
+ function parse(tokens) {
303
+ const p = new Parser(tokens);
304
+ const ast = p.parseExpr(0);
305
+ const trailing = p.peek();
306
+ if (trailing.type !== 'eof') {
307
+ throw new Error(
308
+ `Unexpected token "${trailing.value}" at position ${trailing.pos}`
309
+ );
310
+ }
311
+ return ast;
312
+ }
313
+
314
+ module.exports = { parse };
@@ -0,0 +1,239 @@
1
+ 'use strict';
2
+
3
+ // Spec: https://www.w3.org/TR/css-values-4/#serialize-a-calculation-tree
4
+ // Outer calc() is added only when the top-level result contains an
5
+ // arithmetic operator. A Sum inside a Product is the only place parens
6
+ // are ever required on valid canonical input.
7
+
8
+ /**
9
+ * @typedef {import('./node.js').Node} Node
10
+ * @typedef {import('./node.js').Sum} Sum
11
+ * @typedef {import('./node.js').Product} Product
12
+ * @typedef {import('./node.js').ProductFactor} ProductFactor
13
+ * @typedef {object} SerializeOptions
14
+ * @property {number | false} [precision] Decimal places for numbers. `false` disables rounding. Default 5.
15
+ * @property {string} [calcName] Wrapper name to use when `calc()` is needed. Default `'calc'`.
16
+ */
17
+
18
+ // Below this is float noise, not a value: `0.1 + 0.2 - 0.3` is 5.5e-17.
19
+ const NOISE_FLOOR = 1e-12;
20
+
21
+ /**
22
+ * Rounding to `prec` decimal places turns `calc(1/1000000)` into `0`, and a
23
+ * `0` in CSS is often a switch, not a small number (`flex-grow: 0` never
24
+ * grows). So when a value is too small for `prec`, keep its significant digits
25
+ * instead: `1/1000000` -> `0.000001`, `1/3000000` -> `3.3333e-7`.
26
+ *
27
+ * @param {number} v
28
+ * @param {number | false} prec
29
+ * @return {number}
30
+ */
31
+ function round(v, prec) {
32
+ if (prec === false) {
33
+ return v;
34
+ }
35
+ const m = Math.pow(10, prec);
36
+ const rounded = Math.round(v * m) / m;
37
+ if (rounded === 0 && Math.abs(v) > NOISE_FLOOR) {
38
+ // toPrecision needs at least one significant digit; `prec` may be 0.
39
+ return Number(v.toPrecision(Math.max(prec, 1)));
40
+ }
41
+ return rounded;
42
+ }
43
+
44
+ // §10.13 / §10.7.2: Infinity/NaN serialize as canonical keywords.
45
+ /**
46
+ * @param {number} v
47
+ * @return {boolean}
48
+ */
49
+ function isDegenerate(v) {
50
+ return !isFinite(v) || isNaN(v);
51
+ }
52
+
53
+ /**
54
+ * @param {number} v
55
+ * @return {string}
56
+ */
57
+ function degenerateKeyword(v) {
58
+ if (isNaN(v)) {return 'NaN';}
59
+ return v > 0 ? 'infinity' : '-infinity';
60
+ }
61
+
62
+ /**
63
+ * @param {Node} node
64
+ * @param {SerializeOptions} [opts]
65
+ * @return {string}
66
+ */
67
+ function serialize(node, opts = {}) {
68
+ const prec = opts.precision ?? 5;
69
+ const calcName = opts.calcName ?? 'calc';
70
+
71
+ // §10.13: top-level Infinity/NaN wrap in calc(); dim degenerates carry
72
+ // the unit as `<keyword> * 1<unit>` so the result keeps its type.
73
+ if (node.type === 'Num' && isDegenerate(node.value)) {
74
+ return `${calcName}(${degenerateKeyword(node.value)})`;
75
+ }
76
+ if (node.type === 'Dim' && isDegenerate(node.value)) {
77
+ return `${calcName}(${degenerateKeyword(node.value)} * 1${node.unit})`;
78
+ }
79
+
80
+ if (
81
+ node.type === 'Num' ||
82
+ node.type === 'Dim' ||
83
+ node.type === 'Ident' ||
84
+ node.type === 'Call'
85
+ ) {
86
+ return serializeExpr(node, prec);
87
+ }
88
+
89
+ // Single-term Sum is the canonical form for `-var(--x)` / `-(a*b)` —
90
+ // sign=-1 around an opaque node. Signed leaves live in Num/Dim directly.
91
+ if (node.type === 'Sum' && node.terms.length === 1) {
92
+ return `${calcName}(${serializeLeadingNeg(node.terms[0].node, prec)})`;
93
+ }
94
+
95
+ return `${calcName}(${serializeExpr(node, prec)})`;
96
+ }
97
+
98
+ // --- Inside calc() expression --------------------------------------------
99
+
100
+ /**
101
+ * @param {Node} node
102
+ * @param {number | false} prec
103
+ * @return {string}
104
+ */
105
+ function serializeExpr(node, prec) {
106
+ switch (node.type) {
107
+ case 'Num':
108
+ if (isDegenerate(node.value)) {return degenerateKeyword(node.value);}
109
+ return String(round(node.value, prec));
110
+ case 'Dim':
111
+ if (isDegenerate(node.value)) {
112
+ // Nested degenerate Dim wraps in calc() so the `<kw> * 1<unit>` form
113
+ // parses back as one Dim factor. The bare form round-trips wrong
114
+ // inside a Product — `0 * Dim(Infinity, px)` would re-fold as NaN.
115
+ return `calc(${degenerateKeyword(node.value)} * 1${node.unit})`;
116
+ }
117
+ return `${round(node.value, prec)}${node.unit}`;
118
+ case 'Ident':
119
+ return node.name;
120
+ case 'Call': {
121
+ const args = node.args.map((a) => serializeExpr(a, prec)).join(', ');
122
+ return `${node.name}(${args})`;
123
+ }
124
+ case 'Sum':
125
+ return serializeSum(node, prec);
126
+ case 'Product':
127
+ return serializeProduct(node, prec);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Combine the term's sign with a negative Num/Dim value's sign so
133
+ * `{sign:+1, Num(-5)}` renders as `-5`, not `+ -5`. Skip degenerate
134
+ * (Infinity/NaN) values — the `degenerateKeyword` path emits `-infinity`
135
+ * inline, and a leading minus on `calc(infinity*1<unit>)` would now
136
+ * tokenize as a `-calc` function.
137
+ * @param {{sign: 1 | -1, node: Node}} term
138
+ * @return {{sign: 1 | -1, magnitude: Node}}
139
+ */
140
+ function displaySign(term) {
141
+ const { sign, node } = term;
142
+ if (node.type === 'Num' && isFinite(node.value) && node.value < 0) {
143
+ return {
144
+ sign: /** @type {1 | -1} */ (-sign),
145
+ magnitude: { type: 'Num', value: -node.value },
146
+ };
147
+ }
148
+ if (node.type === 'Dim' && isFinite(node.value) && node.value < 0) {
149
+ return {
150
+ sign: /** @type {1 | -1} */ (-sign),
151
+ magnitude: { type: 'Dim', value: -node.value, unit: node.unit },
152
+ };
153
+ }
154
+ return { sign, magnitude: node };
155
+ }
156
+
157
+ /**
158
+ * @param {Sum} sum
159
+ * @param {number | false} prec
160
+ * @return {string}
161
+ */
162
+ function serializeSum(sum, prec) {
163
+ let out = '';
164
+ sum.terms.forEach((t, i) => {
165
+ const { sign, magnitude } = displaySign(t);
166
+ if (i === 0) {
167
+ out = sign === 1
168
+ ? serializeExpr(magnitude, prec)
169
+ : serializeLeadingNeg(magnitude, prec);
170
+ } else {
171
+ // `-` binds looser than `*`/`/` so the right side never needs parens.
172
+ const body = serializeExpr(magnitude, prec);
173
+ out += sign === 1 ? ` + ${body}` : ` - ${body}`;
174
+ }
175
+ });
176
+ return out;
177
+ }
178
+
179
+ /**
180
+ * Fold a leading negation into a finite leading Num if there is one
181
+ * (`-(0.5 * x)` → `-0.5 * x`); else use `-(…)` for Sum/Product or `-x`.
182
+ * @param {Node} node
183
+ * @param {number | false} prec
184
+ * @return {string}
185
+ */
186
+ function serializeLeadingNeg(node, prec) {
187
+ if (
188
+ node.type === 'Product' &&
189
+ node.factors.length > 0 &&
190
+ node.factors[0].exponent === 1 &&
191
+ node.factors[0].node.type === 'Num' &&
192
+ isFinite(node.factors[0].node.value) &&
193
+ node.factors[0].node.value !== 0
194
+ ) {
195
+ const head = node.factors[0].node;
196
+ const negatedValue = -head.value;
197
+ const rest = node.factors.slice(1);
198
+ // A coefficient of 1 is a no-op factor, matching mkProduct.
199
+ /** @type {ProductFactor[]} */
200
+ const negatedFactors =
201
+ negatedValue === 1
202
+ ? rest
203
+ : [
204
+ { exponent: 1, node: { type: 'Num', value: negatedValue } },
205
+ ...rest,
206
+ ];
207
+ return serializeProduct({ type: 'Product', factors: negatedFactors }, prec);
208
+ }
209
+ const body = serializeExpr(node, prec);
210
+ return node.type === 'Sum' || node.type === 'Product' ? `-(${body})` : `-${body}`;
211
+ }
212
+
213
+ /**
214
+ * @param {Product} product
215
+ * @param {number | false} prec
216
+ * @return {string}
217
+ */
218
+ function serializeProduct(product, prec) {
219
+ let out = '';
220
+ product.factors.forEach((f, i) => {
221
+ let body = serializeExpr(f.node, prec);
222
+ // A Sum factor needs parens: `a * (b + c)`. Flat canonical form means
223
+ // this is the only place parens are required.
224
+ if (f.node.type === 'Sum') {
225
+ body = `(${body})`;
226
+ }
227
+ if (i === 0) {
228
+ // Leading denominator: implicit 1 so we emit `1 / 2px`, not `/ 2px`.
229
+ out = f.exponent === 1 ? body : `1 / ${body}`;
230
+ } else {
231
+ out += f.exponent === 1 ? ` * ${body}` : ` / ${body}`;
232
+ }
233
+ });
234
+ return out;
235
+ }
236
+
237
+ // Stryker disable next-line all: instrumenting this line breaks Node's
238
+ // cjs-module-lexer named-export detection for .mjs `import { x } from` consumers.
239
+ module.exports = { serialize };
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ const { num, dim } = require('../node.js');
4
+
5
+ /** @typedef {import('../node.js').Node} Node */
6
+
7
+ /**
8
+ * @param {Node[]} args
9
+ * @return {Node}
10
+ */
11
+ function simplifyAbs(args) {
12
+ if (args.length !== 1) {
13
+ return { type: 'Call', name: 'abs', args };
14
+ }
15
+ const a = args[0];
16
+ if (a.type === 'Num') {return num(Math.abs(a.value));}
17
+ if (a.type === 'Dim' && a.unit !== '%') {return dim(Math.abs(a.value), a.unit);}
18
+ return { type: 'Call', name: 'abs', args: [a] };
19
+ }
20
+
21
+ module.exports = { simplifyAbs };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ // §10.4 — atan2. foldConstArgs already rejects percentages (property-
4
+ // context-resolved) and enforces shared base + static convertibility.
5
+
6
+ const { num, dim } = require('../node.js');
7
+ const { foldConstArgs } = require('./fold.js');
8
+
9
+ /** @typedef {import('../node.js').Node} Node */
10
+
11
+ /**
12
+ * @param {Node[]} args
13
+ * @return {Node}
14
+ */
15
+ function simplifyAtan2(args) {
16
+ if (args.length !== 2) {return { type: 'Call', name: 'atan2', args };}
17
+ const fold = foldConstArgs(args);
18
+ if (fold === null) {return { type: 'Call', name: 'atan2', args };}
19
+ const [y, x] = /** @type {[number, number]} */ (fold.values);
20
+ const radians = Math.atan2(y, x);
21
+ if (isNaN(radians)) {return num(NaN);}
22
+ return dim((radians * 180) / Math.PI, 'deg');
23
+ }
24
+
25
+ module.exports = { simplifyAtan2 };
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ // §10.10 phase 2: merge convertible same-base unit buckets into the
4
+ // first-encountered unit (px absorbs cm/in/pt/pc, deg absorbs
5
+ // rad/grad/turn, …). Buckets with `base === null` (relative or unknown
6
+ // units) keep their own slot.
7
+
8
+ const { convert } = require('../convertUnits.js');
9
+
10
+ /**
11
+ * @typedef {object} UnitBucket
12
+ * @property {string} unit
13
+ * @property {number} total
14
+ * @property {number} scale largest |term| accumulated into `total`, for noise detection
15
+ * @property {import('../convertUnits.js').BaseType | null} base
16
+ * @property {number} order
17
+ */
18
+
19
+ /** Mutates `buckets` in place — totals of survivor buckets accumulate the
20
+ * converted values of merged neighbors. Caller must not reuse the input.
21
+ * @param {UnitBucket[]} buckets
22
+ * @return {UnitBucket[]}
23
+ */
24
+ function mergeConvertibleBuckets(buckets) {
25
+ const ordered = [...buckets].sort((a, b) => a.order - b.order);
26
+ /** @type {Set<string>} */ const merged = new Set();
27
+ /** @type {UnitBucket[]} */ const out = [];
28
+ for (const b of ordered) {
29
+ const keyB = b.unit.toLowerCase();
30
+ if (merged.has(keyB)) {continue;}
31
+ merged.add(keyB);
32
+ if (b.base !== null) {
33
+ for (const other of ordered) {
34
+ const keyO = other.unit.toLowerCase();
35
+ if (merged.has(keyO)) {continue;}
36
+ if (other.base !== b.base) {continue;}
37
+ const converted = convert(other.total, other.unit, b.unit);
38
+ if (converted !== null) {
39
+ b.total += converted;
40
+ merged.add(keyO);
41
+ }
42
+ }
43
+ }
44
+ out.push(b);
45
+ }
46
+ return out;
47
+ }
48
+
49
+ module.exports = { mergeConvertibleBuckets };
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ // Pre-simplify args once, route by name. Leaf folds receive simplified
4
+ // args so they don't need to recurse into `simplify` themselves.
5
+
6
+ const { simplifyMinMax } = require('./min-max.js');
7
+ const { simplifyClamp } = require('./clamp.js');
8
+ const { simplifyAbs } = require('./abs.js');
9
+ const { simplifySign } = require('./sign.js');
10
+ const { simplifyModRem } = require('./mod-rem.js');
11
+ const { simplifyRound } = require('./round.js');
12
+ const { simplifyTrig } = require('./trig.js');
13
+ const { simplifyInverseTrig } = require('./inverse-trig.js');
14
+ const { simplifyAtan2 } = require('./atan2.js');
15
+ const { simplifyPow } = require('./pow.js');
16
+ const { simplifySqrt } = require('./sqrt.js');
17
+ const { simplifyExp } = require('./exp.js');
18
+ const { simplifyLog } = require('./log.js');
19
+ const { simplifyHypot } = require('./hypot.js');
20
+
21
+ /** @typedef {import('../node.js').Node} Node */
22
+ /** @typedef {import('../simplify.js').SimplifyFn} SimplifyFn */
23
+
24
+ /**
25
+ * @param {Extract<Node, { type: 'Call' }>} node
26
+ * @param {SimplifyFn} simplify
27
+ * @return {Node}
28
+ */
29
+ function simplifyCall(node, simplify) {
30
+ const name = node.name.toLowerCase();
31
+
32
+ if (name === 'calc' || name === '-webkit-calc' || name === '-moz-calc') {
33
+ if (node.args.length !== 1) {
34
+ throw new Error(`${node.name}() takes exactly one argument`);
35
+ }
36
+ return simplify(node.args[0]);
37
+ }
38
+
39
+ const args = node.args.map((a) => simplify(a));
40
+
41
+ if (name === 'min' || name === 'max') {return simplifyMinMax(node.name, args);}
42
+ if (name === 'clamp') {return simplifyClamp(args);}
43
+ if (name === 'abs') {return simplifyAbs(args);}
44
+ if (name === 'sign') {return simplifySign(args);}
45
+ if (name === 'mod' || name === 'rem') {return simplifyModRem(name, args);}
46
+ if (name === 'round') {return simplifyRound(args);}
47
+ if (name === 'sin' || name === 'cos' || name === 'tan') {return simplifyTrig(name, args);}
48
+ if (name === 'asin' || name === 'acos' || name === 'atan') {return simplifyInverseTrig(name, args);}
49
+ if (name === 'atan2') {return simplifyAtan2(args);}
50
+ if (name === 'pow') {return simplifyPow(args);}
51
+ if (name === 'sqrt') {return simplifySqrt(args);}
52
+ if (name === 'hypot') {return simplifyHypot(args);}
53
+ if (name === 'log') {return simplifyLog(args);}
54
+ if (name === 'exp') {return simplifyExp(args);}
55
+
56
+ return { type: 'Call', name: node.name, args };
57
+ }
58
+
59
+ module.exports = { simplifyCall };