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.
- package/README.md +56 -1
- package/package.json +20 -13
- package/src/index.js +122 -27
- package/src/lib/convertUnits.js +105 -0
- package/src/lib/node.js +189 -0
- package/src/lib/parser.js +314 -0
- package/src/lib/serialize.js +239 -0
- package/src/lib/simplify/abs.js +21 -0
- package/src/lib/simplify/atan2.js +25 -0
- package/src/lib/simplify/bucket.js +49 -0
- package/src/lib/simplify/call.js +59 -0
- package/src/lib/simplify/cancel.js +39 -0
- package/src/lib/simplify/clamp.js +26 -0
- package/src/lib/simplify/exp.js +18 -0
- package/src/lib/simplify/fold.js +63 -0
- package/src/lib/simplify/hypot.js +22 -0
- package/src/lib/simplify/inverse-trig.js +29 -0
- package/src/lib/simplify/log.js +25 -0
- package/src/lib/simplify/min-max.js +23 -0
- package/src/lib/simplify/mod-rem.js +47 -0
- package/src/lib/simplify/pow.js +20 -0
- package/src/lib/simplify/product.js +133 -0
- package/src/lib/simplify/round.js +113 -0
- package/src/lib/simplify/sign.js +22 -0
- package/src/lib/simplify/sqrt.js +18 -0
- package/src/lib/simplify/sum.js +100 -0
- package/src/lib/simplify/trig.js +40 -0
- package/src/lib/simplify.js +40 -0
- package/src/lib/tokenizer.js +115 -0
- package/types/index.d.ts +15 -17
- package/types/lib/convertUnits.d.ts +16 -0
- package/types/lib/node.d.ts +90 -0
- package/types/lib/parser.d.ts +39 -0
- package/types/lib/serialize.d.ts +20 -0
- package/types/lib/simplify/abs.d.ts +7 -0
- package/types/lib/simplify/atan2.d.ts +7 -0
- package/types/lib/simplify/bucket.d.ts +24 -0
- package/types/lib/simplify/call.d.ts +12 -0
- package/types/lib/simplify/cancel.d.ts +19 -0
- package/types/lib/simplify/clamp.d.ts +7 -0
- package/types/lib/simplify/exp.d.ts +7 -0
- package/types/lib/simplify/fold.d.ts +12 -0
- package/types/lib/simplify/hypot.d.ts +7 -0
- package/types/lib/simplify/inverse-trig.d.ts +7 -0
- package/types/lib/simplify/log.d.ts +7 -0
- package/types/lib/simplify/min-max.d.ts +8 -0
- package/types/lib/simplify/mod-rem.d.ts +8 -0
- package/types/lib/simplify/pow.d.ts +7 -0
- package/types/lib/simplify/product.d.ts +16 -0
- package/types/lib/simplify/round.d.ts +8 -0
- package/types/lib/simplify/sign.d.ts +7 -0
- package/types/lib/simplify/sqrt.d.ts +7 -0
- package/types/lib/simplify/sum.d.ts +11 -0
- package/types/lib/simplify/trig.d.ts +7 -0
- package/types/lib/simplify.d.ts +18 -0
- package/types/lib/tokenizer.d.ts +19 -0
- package/types/lib/type.d.ts +16 -0
- package/src/lib/convertUnit.js +0 -160
- package/src/lib/reducer.js +0 -396
- package/src/lib/stringifier.js +0 -106
- package/src/lib/transform.js +0 -109
- package/src/parser.d.ts +0 -89
- package/src/parser.js +0 -4222
- package/types/lib/convertUnit.d.ts +0 -8
- package/types/lib/reducer.d.ts +0 -14
- package/types/lib/stringifier.d.ts +0 -5
- 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 };
|