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,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,226 @@
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
+ /**
19
+ * @param {number} v
20
+ * @param {number | false} prec
21
+ * @return {number}
22
+ */
23
+ function round(v, prec) {
24
+ if (prec === false) {
25
+ return v;
26
+ }
27
+ const m = Math.pow(10, prec);
28
+ return Math.round(v * m) / m;
29
+ }
30
+
31
+ // §10.13 / §10.7.2: Infinity/NaN serialize as canonical keywords.
32
+ /**
33
+ * @param {number} v
34
+ * @return {boolean}
35
+ */
36
+ function isDegenerate(v) {
37
+ return !isFinite(v) || isNaN(v);
38
+ }
39
+
40
+ /**
41
+ * @param {number} v
42
+ * @return {string}
43
+ */
44
+ function degenerateKeyword(v) {
45
+ if (isNaN(v)) {return 'NaN';}
46
+ return v > 0 ? 'infinity' : '-infinity';
47
+ }
48
+
49
+ /**
50
+ * @param {Node} node
51
+ * @param {SerializeOptions} [opts]
52
+ * @return {string}
53
+ */
54
+ function serialize(node, opts = {}) {
55
+ const prec = opts.precision ?? 5;
56
+ const calcName = opts.calcName ?? 'calc';
57
+
58
+ // §10.13: top-level Infinity/NaN wrap in calc(); dim degenerates carry
59
+ // the unit as `<keyword> * 1<unit>` so the result keeps its type.
60
+ if (node.type === 'Num' && isDegenerate(node.value)) {
61
+ return `${calcName}(${degenerateKeyword(node.value)})`;
62
+ }
63
+ if (node.type === 'Dim' && isDegenerate(node.value)) {
64
+ return `${calcName}(${degenerateKeyword(node.value)} * 1${node.unit})`;
65
+ }
66
+
67
+ if (
68
+ node.type === 'Num' ||
69
+ node.type === 'Dim' ||
70
+ node.type === 'Ident' ||
71
+ node.type === 'Call'
72
+ ) {
73
+ return serializeExpr(node, prec);
74
+ }
75
+
76
+ // Single-term Sum is the canonical form for `-var(--x)` / `-(a*b)` —
77
+ // sign=-1 around an opaque node. Signed leaves live in Num/Dim directly.
78
+ if (node.type === 'Sum' && node.terms.length === 1) {
79
+ return `${calcName}(${serializeLeadingNeg(node.terms[0].node, prec)})`;
80
+ }
81
+
82
+ return `${calcName}(${serializeExpr(node, prec)})`;
83
+ }
84
+
85
+ // --- Inside calc() expression --------------------------------------------
86
+
87
+ /**
88
+ * @param {Node} node
89
+ * @param {number | false} prec
90
+ * @return {string}
91
+ */
92
+ function serializeExpr(node, prec) {
93
+ switch (node.type) {
94
+ case 'Num':
95
+ if (isDegenerate(node.value)) {return degenerateKeyword(node.value);}
96
+ return String(round(node.value, prec));
97
+ case 'Dim':
98
+ if (isDegenerate(node.value)) {
99
+ // Nested degenerate Dim wraps in calc() so the `<kw> * 1<unit>` form
100
+ // parses back as one Dim factor. The bare form round-trips wrong
101
+ // inside a Product — `0 * Dim(Infinity, px)` would re-fold as NaN.
102
+ return `calc(${degenerateKeyword(node.value)} * 1${node.unit})`;
103
+ }
104
+ return `${round(node.value, prec)}${node.unit}`;
105
+ case 'Ident':
106
+ return node.name;
107
+ case 'Call': {
108
+ const args = node.args.map((a) => serializeExpr(a, prec)).join(', ');
109
+ return `${node.name}(${args})`;
110
+ }
111
+ case 'Sum':
112
+ return serializeSum(node, prec);
113
+ case 'Product':
114
+ return serializeProduct(node, prec);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Combine the term's sign with a negative Num/Dim value's sign so
120
+ * `{sign:+1, Num(-5)}` renders as `-5`, not `+ -5`. Skip degenerate
121
+ * (Infinity/NaN) values — the `degenerateKeyword` path emits `-infinity`
122
+ * inline, and a leading minus on `calc(infinity*1<unit>)` would now
123
+ * tokenize as a `-calc` function.
124
+ * @param {{sign: 1 | -1, node: Node}} term
125
+ * @return {{sign: 1 | -1, magnitude: Node}}
126
+ */
127
+ function displaySign(term) {
128
+ const { sign, node } = term;
129
+ if (node.type === 'Num' && isFinite(node.value) && node.value < 0) {
130
+ return {
131
+ sign: /** @type {1 | -1} */ (-sign),
132
+ magnitude: { type: 'Num', value: -node.value },
133
+ };
134
+ }
135
+ if (node.type === 'Dim' && isFinite(node.value) && node.value < 0) {
136
+ return {
137
+ sign: /** @type {1 | -1} */ (-sign),
138
+ magnitude: { type: 'Dim', value: -node.value, unit: node.unit },
139
+ };
140
+ }
141
+ return { sign, magnitude: node };
142
+ }
143
+
144
+ /**
145
+ * @param {Sum} sum
146
+ * @param {number | false} prec
147
+ * @return {string}
148
+ */
149
+ function serializeSum(sum, prec) {
150
+ let out = '';
151
+ sum.terms.forEach((t, i) => {
152
+ const { sign, magnitude } = displaySign(t);
153
+ if (i === 0) {
154
+ out = sign === 1
155
+ ? serializeExpr(magnitude, prec)
156
+ : serializeLeadingNeg(magnitude, prec);
157
+ } else {
158
+ // `-` binds looser than `*`/`/` so the right side never needs parens.
159
+ const body = serializeExpr(magnitude, prec);
160
+ out += sign === 1 ? ` + ${body}` : ` - ${body}`;
161
+ }
162
+ });
163
+ return out;
164
+ }
165
+
166
+ /**
167
+ * Fold a leading negation into a finite leading Num if there is one
168
+ * (`-(0.5 * x)` → `-0.5 * x`); else use `-(…)` for Sum/Product or `-x`.
169
+ * @param {Node} node
170
+ * @param {number | false} prec
171
+ * @return {string}
172
+ */
173
+ function serializeLeadingNeg(node, prec) {
174
+ if (
175
+ node.type === 'Product' &&
176
+ node.factors.length > 0 &&
177
+ node.factors[0].exponent === 1 &&
178
+ node.factors[0].node.type === 'Num' &&
179
+ isFinite(node.factors[0].node.value) &&
180
+ node.factors[0].node.value !== 0
181
+ ) {
182
+ const head = node.factors[0].node;
183
+ const negatedValue = -head.value;
184
+ const rest = node.factors.slice(1);
185
+ // A coefficient of 1 is a no-op factor, matching mkProduct.
186
+ /** @type {ProductFactor[]} */
187
+ const negatedFactors =
188
+ negatedValue === 1
189
+ ? rest
190
+ : [
191
+ { exponent: 1, node: { type: 'Num', value: negatedValue } },
192
+ ...rest,
193
+ ];
194
+ return serializeProduct({ type: 'Product', factors: negatedFactors }, prec);
195
+ }
196
+ const body = serializeExpr(node, prec);
197
+ return node.type === 'Sum' || node.type === 'Product' ? `-(${body})` : `-${body}`;
198
+ }
199
+
200
+ /**
201
+ * @param {Product} product
202
+ * @param {number | false} prec
203
+ * @return {string}
204
+ */
205
+ function serializeProduct(product, prec) {
206
+ let out = '';
207
+ product.factors.forEach((f, i) => {
208
+ let body = serializeExpr(f.node, prec);
209
+ // A Sum factor needs parens: `a * (b + c)`. Flat canonical form means
210
+ // this is the only place parens are required.
211
+ if (f.node.type === 'Sum') {
212
+ body = `(${body})`;
213
+ }
214
+ if (i === 0) {
215
+ // Leading denominator: implicit 1 so we emit `1 / 2px`, not `/ 2px`.
216
+ out = f.exponent === 1 ? body : `1 / ${body}`;
217
+ } else {
218
+ out += f.exponent === 1 ? ` * ${body}` : ` / ${body}`;
219
+ }
220
+ });
221
+ return out;
222
+ }
223
+
224
+ // Stryker disable next-line all: instrumenting this line breaks Node's
225
+ // cjs-module-lexer named-export detection for .mjs `import { x } from` consumers.
226
+ 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,48 @@
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 {import('../convertUnits.js').BaseType | null} base
15
+ * @property {number} order
16
+ */
17
+
18
+ /** Mutates `buckets` in place — totals of survivor buckets accumulate the
19
+ * converted values of merged neighbors. Caller must not reuse the input.
20
+ * @param {UnitBucket[]} buckets
21
+ * @return {UnitBucket[]}
22
+ */
23
+ function mergeConvertibleBuckets(buckets) {
24
+ const ordered = [...buckets].sort((a, b) => a.order - b.order);
25
+ /** @type {Set<string>} */ const merged = new Set();
26
+ /** @type {UnitBucket[]} */ const out = [];
27
+ for (const b of ordered) {
28
+ const keyB = b.unit.toLowerCase();
29
+ if (merged.has(keyB)) {continue;}
30
+ merged.add(keyB);
31
+ if (b.base !== null) {
32
+ for (const other of ordered) {
33
+ const keyO = other.unit.toLowerCase();
34
+ if (merged.has(keyO)) {continue;}
35
+ if (other.base !== b.base) {continue;}
36
+ const converted = convert(other.total, other.unit, b.unit);
37
+ if (converted !== null) {
38
+ b.total += converted;
39
+ merged.add(keyO);
40
+ }
41
+ }
42
+ }
43
+ out.push(b);
44
+ }
45
+ return out;
46
+ }
47
+
48
+ 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 };