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.
- package/README.md +56 -1
- package/package.json +19 -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 +226 -0
- package/src/lib/simplify/abs.js +21 -0
- package/src/lib/simplify/atan2.js +25 -0
- package/src/lib/simplify/bucket.js +48 -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 +83 -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 +19 -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 +18 -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 -390
- package/src/lib/stringifier.js +0 -98
- package/src/lib/transform.js +0 -109
- package/src/parser.d.ts +0 -79
- package/src/parser.js +0 -4200
- 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,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 };
|