postcss-calc 11.2.1 → 11.2.2
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/LICENSE +0 -0
- package/README.md +44 -16
- package/package.json +13 -13
- package/src/index.js +2 -1
- package/src/lib/analyze.js +0 -6
- package/src/lib/compile.js +1 -1
- package/src/lib/node.js +61 -4
- package/src/lib/parser/opaque.js +151 -0
- package/src/lib/parser/tokens.js +242 -0
- package/src/lib/parser.js +32 -377
- package/src/lib/serialize/expression.js +376 -0
- package/src/lib/serialize/precision.js +244 -0
- package/src/lib/serialize.js +24 -561
- package/src/lib/simplify/bucket.js +22 -5
- package/src/lib/simplify/cancel.js +35 -11
- package/src/lib/simplify/clamp.js +2 -2
- package/src/lib/simplify/exact.js +18 -0
- package/src/lib/simplify/fold.js +15 -1
- package/src/lib/simplify/min-max.js +2 -2
- package/src/lib/simplify/product.js +263 -48
- package/src/lib/simplify/sum.js +6 -2
- package/src/lib/simplify.js +22 -5
- package/src/reduce.js +2 -1
- package/types/lib/node.d.ts +17 -2
- package/types/lib/parser/opaque.d.ts +12 -0
- package/types/lib/parser/tokens.d.ts +83 -0
- package/types/lib/parser.d.ts +4 -44
- package/types/lib/serialize/expression.d.ts +95 -0
- package/types/lib/serialize/precision.d.ts +101 -0
- package/types/lib/simplify/bucket.d.ts +4 -1
- package/types/lib/simplify/cancel.d.ts +11 -7
- package/types/lib/simplify/exact.d.ts +9 -0
- package/types/lib/simplify/fold.d.ts +14 -1
- package/types/lib/simplify/product.d.ts +3 -7
- package/types/lib/simplify/sum.d.ts +2 -1
- package/types/lib/simplify.d.ts +3 -8
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { baseOf, convert } from '../convertUnits.js';
|
|
2
|
+
import { isExact } from './exact.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* If `dims` contain exactly one numerator / one denominator pair with the
|
|
5
|
-
* same base type and convertible units, return the
|
|
6
|
-
*
|
|
7
|
-
* Otherwise return null. Used by `simplifyProduct` for typed
|
|
8
|
-
* (§10.2). More complex cancellation (e.g. `px^2 / px`) is left
|
|
6
|
+
* same base type and convertible units, return the two sides expressed in a
|
|
7
|
+
* common unit, so the caller can fold them into its own numerator and
|
|
8
|
+
* denominator. Otherwise return null. Used by `simplifyProduct` for typed
|
|
9
|
+
* division (§10.2). More complex cancellation (e.g. `px^2 / px`) is left
|
|
9
10
|
* unreduced — consumers rarely rely on it and the spec doesn't require it.
|
|
10
11
|
* @template {{ exponent: 1 | -1, value: number, unit: string }} D
|
|
11
12
|
* @param {D[]} dims
|
|
12
|
-
* @
|
|
13
|
+
* @param {number | false} [precision] The denominator is converted into the
|
|
14
|
+
* numerator's unit when that is exact at this precision (`1px / 1in` →
|
|
15
|
+
* `1 / 96`); otherwise the numerator is converted (`1in / 1px` → `96 / 1`).
|
|
16
|
+
* @return {{ numerator: number, denominator: number, remaining: D[] } | null}
|
|
13
17
|
*/
|
|
14
|
-
function tryCancelPair(dims) {
|
|
18
|
+
function tryCancelPair(dims, precision = false) {
|
|
15
19
|
if (dims.length !== 2) {
|
|
16
20
|
return null;
|
|
17
21
|
}
|
|
@@ -26,12 +30,32 @@ function tryCancelPair(dims) {
|
|
|
26
30
|
if (!numBase || numBase !== denBase) {
|
|
27
31
|
return null;
|
|
28
32
|
}
|
|
29
|
-
const converted = convert(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
const converted = convert(
|
|
34
|
+
denominator.value,
|
|
35
|
+
denominator.unit,
|
|
36
|
+
numerator.unit
|
|
37
|
+
);
|
|
33
38
|
// denominator.value === 0 yields ±Infinity / NaN naturally (§10.9.1).
|
|
34
|
-
|
|
39
|
+
if (converted !== null && isExact(converted, precision)) {
|
|
40
|
+
return {
|
|
41
|
+
numerator: numerator.value,
|
|
42
|
+
denominator: converted,
|
|
43
|
+
remaining: [],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const numConverted = convert(
|
|
47
|
+
numerator.value,
|
|
48
|
+
numerator.unit,
|
|
49
|
+
denominator.unit
|
|
50
|
+
);
|
|
51
|
+
if (numConverted !== null && isExact(numConverted, precision)) {
|
|
52
|
+
return {
|
|
53
|
+
numerator: numConverted,
|
|
54
|
+
denominator: denominator.value,
|
|
55
|
+
remaining: [],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
35
59
|
}
|
|
36
60
|
|
|
37
61
|
export { tryCancelPair };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { call } from '../node.js';
|
|
2
|
-
import { foldConstArgs,
|
|
2
|
+
import { foldConstArgs, chosenArg } from './fold.js';
|
|
3
3
|
import { simplifyMinMax } from './min-max.js';
|
|
4
4
|
|
|
5
5
|
/** @typedef {import('../node.js').Node} Node */
|
|
@@ -29,7 +29,7 @@ function simplifyClamp(args) {
|
|
|
29
29
|
// Spec §10.8: clamp(MIN, VAL, MAX) = max(MIN, min(VAL, MAX)). The
|
|
30
30
|
// outer max(MIN, …) means MIN wins when MIN > MAX — not MAX.
|
|
31
31
|
const clamped = Math.max(lo, Math.min(v, hi));
|
|
32
|
-
return
|
|
32
|
+
return chosenArg(args, fold, clamped);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
return call('clamp', args);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { round } from '../serialize/precision.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A value is exact at `precision` when rounding it for output loses nothing
|
|
5
|
+
* beyond float noise.
|
|
6
|
+
* @param {number} value
|
|
7
|
+
* @param {number | false} precision
|
|
8
|
+
* @return {boolean}
|
|
9
|
+
*/
|
|
10
|
+
function isExact(value, precision) {
|
|
11
|
+
if (precision === false || !Number.isFinite(value)) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
const snapped = Number(value.toPrecision(15));
|
|
15
|
+
return round(snapped, precision) === snapped;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { isExact };
|
package/src/lib/simplify/fold.js
CHANGED
|
@@ -16,6 +16,20 @@ function foldResult(fold, value) {
|
|
|
16
16
|
return fold.unit === '' ? num(value) : dim(value, fold.unit);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Return the argument that produced a folded min/max/clamp value, so the
|
|
21
|
+
* result keeps its own unit, spelling and zero sign. Falls back to a fresh
|
|
22
|
+
* node when no argument matches (NaN).
|
|
23
|
+
* @param {Node[]} args
|
|
24
|
+
* @param {{ values: number[], unit: string }} fold
|
|
25
|
+
* @param {number} value
|
|
26
|
+
* @return {Node}
|
|
27
|
+
*/
|
|
28
|
+
function chosenArg(args, fold, value) {
|
|
29
|
+
const index = fold.values.findIndex((v) => Object.is(v, value));
|
|
30
|
+
return index === -1 ? foldResult(fold, value) : args[index];
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
/**
|
|
20
34
|
* @param {Node[]} args
|
|
21
35
|
* @return {{ values: number[], unit: string } | null}
|
|
@@ -75,4 +89,4 @@ function foldDimArgs(args, unit, base) {
|
|
|
75
89
|
return { values, unit };
|
|
76
90
|
}
|
|
77
91
|
|
|
78
|
-
export { foldConstArgs, foldResult };
|
|
92
|
+
export { foldConstArgs, foldResult, chosenArg };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { call } from '../node.js';
|
|
2
|
-
import { foldConstArgs,
|
|
2
|
+
import { foldConstArgs, chosenArg } from './fold.js';
|
|
3
3
|
|
|
4
4
|
/** @typedef {import('../node.js').Node} Node */
|
|
5
5
|
|
|
@@ -13,7 +13,7 @@ function simplifyMinMax(name, args) {
|
|
|
13
13
|
if (fold !== null) {
|
|
14
14
|
const fn = name.toLowerCase() === 'min' ? Math.min : Math.max;
|
|
15
15
|
const value = fn(...fold.values);
|
|
16
|
-
return
|
|
16
|
+
return chosenArg(args, fold, value);
|
|
17
17
|
}
|
|
18
18
|
return call(name, args);
|
|
19
19
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { mkSum, mkProduct, num, dim } from '../node.js';
|
|
1
|
+
import { mkSum, mkProduct, mkGroup, num, dim } from '../node.js';
|
|
2
2
|
import { tryCancelPair } from './cancel.js';
|
|
3
|
+
import { isExact } from './exact.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @typedef {import('../node.js').Node} Node
|
|
@@ -9,12 +10,150 @@ import { tryCancelPair } from './cancel.js';
|
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* @param {
|
|
13
|
+
* @param {number} a non-negative
|
|
14
|
+
* @param {number} b non-negative
|
|
15
|
+
* @return {number}
|
|
16
|
+
*/
|
|
17
|
+
function gcd(a, b) {
|
|
18
|
+
while (b !== 0) {
|
|
19
|
+
[a, b] = [b, a % b];
|
|
20
|
+
}
|
|
21
|
+
return a || 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {{exponent: 1 | -1, value: number}[]} chain
|
|
26
|
+
* @return {number}
|
|
27
|
+
*/
|
|
28
|
+
function chainValue(chain) {
|
|
29
|
+
let value = 1;
|
|
30
|
+
for (const f of chain) {
|
|
31
|
+
value = f.exponent === 1 ? value * f.value : value / f.value;
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether the opaque factors are a single Sum of only Num/Dim terms.
|
|
38
|
+
* @param {ProductFactor[]} opaque
|
|
39
|
+
* @return {boolean}
|
|
40
|
+
*/
|
|
41
|
+
function isScalarSum(opaque) {
|
|
42
|
+
return (
|
|
43
|
+
opaque.length === 1 &&
|
|
44
|
+
opaque[0].exponent === 1 &&
|
|
45
|
+
opaque[0].node.type === 'Sum' &&
|
|
46
|
+
opaque[0].node.terms.every(
|
|
47
|
+
(t) => t.node.type === 'Num' || t.node.type === 'Dim'
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether a folded quotient prints exactly. A quotient distributed over a
|
|
54
|
+
* scalar Sum is judged term by term, so `(3px + 6em) / 3` still folds.
|
|
55
|
+
* @param {number} value
|
|
56
|
+
* @param {ProductFactor[]} opaque
|
|
57
|
+
* @param {boolean} distributable
|
|
58
|
+
* @param {number | false} precision
|
|
59
|
+
* @return {boolean}
|
|
60
|
+
*/
|
|
61
|
+
function foldsExactly(value, opaque, distributable, precision) {
|
|
62
|
+
if (!Number.isFinite(value) || isExact(value, precision)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (!distributable) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const sum = /** @type {import('../node.js').Sum} */ (opaque[0].node);
|
|
69
|
+
return sum.terms.every((t) =>
|
|
70
|
+
isExact(
|
|
71
|
+
value *
|
|
72
|
+
/** @type {import('../node.js').Num | import('../node.js').Dim} */ (
|
|
73
|
+
t.node
|
|
74
|
+
).value,
|
|
75
|
+
precision
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Emit an inexact quotient as `numerator * dims * opaque / denominator`, with
|
|
82
|
+
* integer parts reduced by their gcd. A lone Dim absorbs the numerator.
|
|
83
|
+
* @param {number} numerator
|
|
84
|
+
* @param {number} denominator
|
|
85
|
+
* @param {{value: number, unit: string, rawUnit?: string} | null} loneDim
|
|
86
|
+
* @param {{exponent: 1 | -1, value: number, unit: string, rawUnit?: string}[]} dims
|
|
87
|
+
* @param {ProductFactor[]} opaque
|
|
88
|
+
* @param {number | false} precision
|
|
89
|
+
* @return {Node | null} null when a part would be rounded on output
|
|
90
|
+
*/
|
|
91
|
+
function rationalProduct(
|
|
92
|
+
numerator,
|
|
93
|
+
denominator,
|
|
94
|
+
loneDim,
|
|
95
|
+
dims,
|
|
96
|
+
opaque,
|
|
97
|
+
precision
|
|
98
|
+
) {
|
|
99
|
+
// Drop float noise (`1.9999999999999993 * 100`) so the parts print as
|
|
100
|
+
// written and the gcd below can reduce them.
|
|
101
|
+
let n = Number(
|
|
102
|
+
(loneDim === null ? numerator : numerator * loneDim.value).toPrecision(15)
|
|
103
|
+
);
|
|
104
|
+
let d = Number(denominator.toPrecision(15));
|
|
105
|
+
if (!isExact(n, precision) || !isExact(d, precision)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
if (Number.isSafeInteger(n) && Number.isSafeInteger(d)) {
|
|
109
|
+
const g = gcd(Math.abs(n), Math.abs(d));
|
|
110
|
+
n /= g;
|
|
111
|
+
d /= g;
|
|
112
|
+
}
|
|
113
|
+
if (d < 0) {
|
|
114
|
+
n = -n;
|
|
115
|
+
d = -d;
|
|
116
|
+
}
|
|
117
|
+
/** @type {ProductFactor[]} */
|
|
118
|
+
const factors = [];
|
|
119
|
+
if (loneDim !== null) {
|
|
120
|
+
factors.push({
|
|
121
|
+
exponent: 1,
|
|
122
|
+
node: dim(n, loneDim.unit, loneDim.rawUnit),
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
if (n !== 1) {
|
|
126
|
+
factors.push({ exponent: 1, node: num(n) });
|
|
127
|
+
}
|
|
128
|
+
for (const dm of dims) {
|
|
129
|
+
factors.push({
|
|
130
|
+
exponent: dm.exponent,
|
|
131
|
+
node: dim(dm.value, dm.unit, dm.rawUnit),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
factors.push(...opaque);
|
|
136
|
+
if (d !== 1) {
|
|
137
|
+
factors.push({ exponent: -1, node: num(d) });
|
|
138
|
+
}
|
|
139
|
+
return mkProduct(factors);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Fold a run of factors that contains no substitution barrier.
|
|
144
|
+
* @param {ProductFactor[]} items Simplified factors, flattened by the caller.
|
|
145
|
+
* @param {number} start
|
|
146
|
+
* @param {number} end
|
|
13
147
|
* @param {SimplifyFn} simplify
|
|
148
|
+
* @param {number | false} precision
|
|
14
149
|
* @return {Node}
|
|
15
150
|
*/
|
|
16
|
-
function
|
|
151
|
+
function foldFactors(items, start, end, simplify, precision) {
|
|
17
152
|
let coeff = 1;
|
|
153
|
+
// Num factors by exponent, so an inexact quotient can be re-emitted as a
|
|
154
|
+
// reduced `numerator / denominator`.
|
|
155
|
+
let numerator = 1;
|
|
156
|
+
let denominator = 1;
|
|
18
157
|
/** @type {{exponent: 1 | -1, value: number, unit: string, rawUnit?: string}[]} */
|
|
19
158
|
const dims = [];
|
|
20
159
|
/** @type {ProductFactor[]} */
|
|
@@ -28,27 +167,13 @@ function simplifyProduct(product, simplify) {
|
|
|
28
167
|
* @return {void}
|
|
29
168
|
*/
|
|
30
169
|
function processFactor(exponent, n) {
|
|
31
|
-
if (n.type === 'Product') {
|
|
32
|
-
for (const inner of n.factors) {
|
|
33
|
-
processFactor(
|
|
34
|
-
/** @type {1 | -1} */ (exponent * inner.exponent),
|
|
35
|
-
inner.node
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
// Canonical negation form (see node.js's `negate`); flatten through it
|
|
41
|
-
// like a nested Product so cancellation below can see what's inside.
|
|
42
|
-
if (n.type === 'Sum' && n.terms.length === 1) {
|
|
43
|
-
processFactor(exponent, num(-1));
|
|
44
|
-
processFactor(exponent, n.terms[0].node);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
170
|
if (n.type === 'Num') {
|
|
48
171
|
if (exponent === 1) {
|
|
49
172
|
coeff *= n.value;
|
|
173
|
+
numerator *= n.value;
|
|
50
174
|
} else {
|
|
51
175
|
coeff /= n.value; // §10.9.1: 1/0 → ±Infinity, 0/0 → NaN per IEEE-754
|
|
176
|
+
denominator *= n.value;
|
|
52
177
|
}
|
|
53
178
|
scalarChain.push({ exponent, value: n.value });
|
|
54
179
|
return;
|
|
@@ -61,31 +186,50 @@ function simplifyProduct(product, simplify) {
|
|
|
61
186
|
opaque.push({ exponent, node: n });
|
|
62
187
|
}
|
|
63
188
|
|
|
64
|
-
for (
|
|
65
|
-
processFactor(
|
|
189
|
+
for (let i = start; i < end; i++) {
|
|
190
|
+
processFactor(items[i].exponent, items[i].node);
|
|
66
191
|
}
|
|
67
192
|
|
|
68
193
|
// §10.2 typed division. Higher-power cancellation (`px^2 / px`) is left
|
|
69
194
|
// unreduced — consumers don't rely on it and the spec doesn't require it.
|
|
70
|
-
const cancelled = tryCancelPair(dims);
|
|
195
|
+
const cancelled = tryCancelPair(dims, precision);
|
|
71
196
|
if (cancelled !== null) {
|
|
72
|
-
coeff *= cancelled.
|
|
197
|
+
coeff *= cancelled.numerator / cancelled.denominator;
|
|
198
|
+
numerator *= cancelled.numerator;
|
|
199
|
+
denominator *= cancelled.denominator;
|
|
73
200
|
}
|
|
201
|
+
const divides = denominator !== 1 || cancelled !== null;
|
|
74
202
|
const remainingDims = cancelled ? cancelled.remaining : dims;
|
|
203
|
+
const loneDim =
|
|
204
|
+
remainingDims.length === 1 &&
|
|
205
|
+
remainingDims[0].exponent === 1 &&
|
|
206
|
+
opaque.length === 0
|
|
207
|
+
? remainingDims[0]
|
|
208
|
+
: null;
|
|
209
|
+
const value = loneDim === null ? coeff : chainValue(scalarChain);
|
|
210
|
+
const distributable = remainingDims.length === 0 && isScalarSum(opaque);
|
|
211
|
+
|
|
212
|
+
// An inexact quotient stays symbolic when its numerator and denominator
|
|
213
|
+
// print exactly; otherwise it is folded and rounded at serialization.
|
|
214
|
+
if (divides && !foldsExactly(value, opaque, distributable, precision)) {
|
|
215
|
+
const rational = rationalProduct(
|
|
216
|
+
numerator,
|
|
217
|
+
denominator,
|
|
218
|
+
loneDim,
|
|
219
|
+
remainingDims,
|
|
220
|
+
opaque,
|
|
221
|
+
precision
|
|
222
|
+
);
|
|
223
|
+
if (rational !== null) {
|
|
224
|
+
return rational;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
75
227
|
|
|
76
228
|
// §10.10 distributive multiplication: `0.5 * (100vw - 10px)` → `50vw - 5px`.
|
|
77
229
|
// Only distribute when every Sum term is Num/Dim — partial distribution
|
|
78
230
|
// over opaque terms matches neither the legacy implementation nor csstools.
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
opaque.length === 1 &&
|
|
82
|
-
opaque[0].exponent === 1 &&
|
|
83
|
-
opaque[0].node.type === 'Sum' &&
|
|
84
|
-
opaque[0].node.terms.every(
|
|
85
|
-
(t) => t.node.type === 'Num' || t.node.type === 'Dim'
|
|
86
|
-
)
|
|
87
|
-
) {
|
|
88
|
-
const sum = opaque[0].node;
|
|
231
|
+
if (distributable) {
|
|
232
|
+
const sum = /** @type {import('../node.js').Sum} */ (opaque[0].node);
|
|
89
233
|
const distributed = sum.terms.map((t) => ({
|
|
90
234
|
sign: t.sign,
|
|
91
235
|
node: simplify(
|
|
@@ -98,21 +242,8 @@ function simplifyProduct(product, simplify) {
|
|
|
98
242
|
return mkSum(distributed);
|
|
99
243
|
}
|
|
100
244
|
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
remainingDims[0].exponent === 1 &&
|
|
104
|
-
opaque.length === 0
|
|
105
|
-
) {
|
|
106
|
-
const d = remainingDims[0];
|
|
107
|
-
let value = 1;
|
|
108
|
-
for (const f of scalarChain) {
|
|
109
|
-
if (f.exponent === 1) {
|
|
110
|
-
value = value * f.value;
|
|
111
|
-
} else {
|
|
112
|
-
value = value / f.value;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return dim(value, d.unit, d.rawUnit);
|
|
245
|
+
if (loneDim !== null) {
|
|
246
|
+
return dim(value, loneDim.unit, loneDim.rawUnit);
|
|
116
247
|
}
|
|
117
248
|
|
|
118
249
|
if (remainingDims.length === 0 && opaque.length === 0) {
|
|
@@ -135,4 +266,88 @@ function simplifyProduct(product, simplify) {
|
|
|
135
266
|
return mkProduct(factors);
|
|
136
267
|
}
|
|
137
268
|
|
|
269
|
+
/**
|
|
270
|
+
* A substitution function (`var()`, `env()`, `attr()`, ...) is replaced by
|
|
271
|
+
* tokens, not by a value, so factors cannot move or cancel across it. Any
|
|
272
|
+
* function the parser does not recognise (`anchor()`, `foo()`, ...) is also an
|
|
273
|
+
* OpaqueCall and is treated the same, conservatively. A grouped Product is a
|
|
274
|
+
* parenthesized substitution and is equally opaque.
|
|
275
|
+
* @param {Node} node
|
|
276
|
+
* @return {boolean}
|
|
277
|
+
*/
|
|
278
|
+
function isBarrier(node) {
|
|
279
|
+
return (
|
|
280
|
+
node.type === 'OpaqueCall' ||
|
|
281
|
+
(node.type === 'Product' && node.grouped === true)
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Flatten a simplified factor through nested ungrouped Products and the
|
|
287
|
+
* canonical negation form, so barriers are visible at the top level.
|
|
288
|
+
* @param {ProductFactor[]} out
|
|
289
|
+
* @param {1 | -1} exponent
|
|
290
|
+
* @param {Node} node
|
|
291
|
+
* @return {boolean} Whether a barrier was pushed.
|
|
292
|
+
*/
|
|
293
|
+
function flattenFactor(out, exponent, node) {
|
|
294
|
+
if (node.type === 'Product' && !node.grouped) {
|
|
295
|
+
let barrier = false;
|
|
296
|
+
for (const inner of node.factors) {
|
|
297
|
+
if (
|
|
298
|
+
flattenFactor(
|
|
299
|
+
out,
|
|
300
|
+
/** @type {1 | -1} */ (exponent * inner.exponent),
|
|
301
|
+
inner.node
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
barrier = true;
|
|
305
|
+
}
|
|
306
|
+
return barrier;
|
|
307
|
+
}
|
|
308
|
+
if (node.type === 'Sum' && node.terms.length === 1) {
|
|
309
|
+
out.push({ exponent, node: num(-1) });
|
|
310
|
+
return flattenFactor(out, exponent, node.terms[0].node);
|
|
311
|
+
}
|
|
312
|
+
out.push({ exponent, node });
|
|
313
|
+
return isBarrier(node);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* @param {Product} product
|
|
318
|
+
* @param {SimplifyFn} simplify
|
|
319
|
+
* @param {number | false} [precision] A quotient that is not exact at this
|
|
320
|
+
* precision stays symbolic (`100% / 3`) instead of being rounded.
|
|
321
|
+
* @return {Node}
|
|
322
|
+
*/
|
|
323
|
+
function simplifyProduct(product, simplify, precision = false) {
|
|
324
|
+
/** @type {ProductFactor[]} */
|
|
325
|
+
const items = [];
|
|
326
|
+
let hasBarrier = false;
|
|
327
|
+
for (const f of product.factors) {
|
|
328
|
+
if (flattenFactor(items, f.exponent, simplify(f.node))) hasBarrier = true;
|
|
329
|
+
}
|
|
330
|
+
if (!hasBarrier)
|
|
331
|
+
return foldFactors(items, 0, items.length, simplify, precision);
|
|
332
|
+
|
|
333
|
+
// Fold each run between barriers on its own; never move or cancel a factor
|
|
334
|
+
// across a barrier, and keep each barrier's own operator.
|
|
335
|
+
/** @type {ProductFactor[]} */
|
|
336
|
+
const factors = [];
|
|
337
|
+
let start = 0;
|
|
338
|
+
for (let i = 0; i <= items.length; i++) {
|
|
339
|
+
if (i < items.length && !isBarrier(items[i].node)) continue;
|
|
340
|
+
if (i - start === 1) factors.push(items[start]);
|
|
341
|
+
else if (i - start > 1)
|
|
342
|
+
factors.push({
|
|
343
|
+
exponent: 1,
|
|
344
|
+
node: foldFactors(items, start, i, simplify, precision),
|
|
345
|
+
});
|
|
346
|
+
if (i < items.length) factors.push(items[i]);
|
|
347
|
+
start = i + 1;
|
|
348
|
+
}
|
|
349
|
+
const result = mkProduct(factors);
|
|
350
|
+
return product.grouped === true ? mkGroup(result) : result;
|
|
351
|
+
}
|
|
352
|
+
|
|
138
353
|
export { simplifyProduct };
|
package/src/lib/simplify/sum.js
CHANGED
|
@@ -26,9 +26,10 @@ function denoise(total, scale) {
|
|
|
26
26
|
/**
|
|
27
27
|
* @param {Sum} sum
|
|
28
28
|
* @param {SimplifyFn} simplify
|
|
29
|
+
* @param {number | false} [precision]
|
|
29
30
|
* @return {Node}
|
|
30
31
|
*/
|
|
31
|
-
function simplifySum(sum, simplify) {
|
|
32
|
+
function simplifySum(sum, simplify, precision = false) {
|
|
32
33
|
// §10.10 two-phase dim handling: phase 1 buckets by exact unit (`1em + 1em`
|
|
33
34
|
// → `2em`); phase 2 merges convertible same-base buckets into the first-
|
|
34
35
|
// encountered unit. `100vh - 5rem - 10rem - 100px` → `-15rem` in phase 1,
|
|
@@ -106,7 +107,10 @@ function simplifySum(sum, simplify) {
|
|
|
106
107
|
const terms = hasNum
|
|
107
108
|
? [{ sign: /** @type {1} */ (1), node: num(denoise(numTotal, numScale)) }]
|
|
108
109
|
: [];
|
|
109
|
-
for (const bucket of mergeConvertibleBuckets(
|
|
110
|
+
for (const bucket of mergeConvertibleBuckets(
|
|
111
|
+
[...byUnit.values()],
|
|
112
|
+
precision
|
|
113
|
+
)) {
|
|
110
114
|
terms.push({
|
|
111
115
|
sign: 1,
|
|
112
116
|
node: dim(
|
package/src/lib/simplify.js
CHANGED
|
@@ -6,7 +6,8 @@ import { simplifySum } from './simplify/sum.js';
|
|
|
6
6
|
import { simplifyProduct } from './simplify/product.js';
|
|
7
7
|
import { simplifyCall } from './simplify/call.js';
|
|
8
8
|
import { simplifyComponents } from './opaque.js';
|
|
9
|
-
import { opaqueCall } from './node.js';
|
|
9
|
+
import { groupSubstitution, opaqueCall } from './node.js';
|
|
10
|
+
import { isCalculationFunction } from './functions.js';
|
|
10
11
|
import { assertDepth } from './limits.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -17,17 +18,33 @@ import { assertDepth } from './limits.js';
|
|
|
17
18
|
* @typedef {(node: Node) => Node} SimplifyFn
|
|
18
19
|
*/
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* A nested calc() is a parenthesized group: with unresolved tokens inside,
|
|
23
|
+
* its operands must not merge into the surrounding sum or product.
|
|
24
|
+
* @param {Node} value
|
|
25
|
+
* @param {(value: Node) => Node} child
|
|
26
|
+
* @return {Node}
|
|
27
|
+
*/
|
|
28
|
+
function operand(value, child) {
|
|
29
|
+
const result = child(value);
|
|
30
|
+
return value.type === 'Call' && isCalculationFunction(value.name)
|
|
31
|
+
? groupSubstitution(result)
|
|
32
|
+
: result;
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
/**
|
|
21
36
|
* Simplify is an independent, composable AST transformation. It may
|
|
22
37
|
* synthesize canonical nodes while preserving the Node -> Node contract.
|
|
23
38
|
* @param {Node} node
|
|
39
|
+
* @param {number | false} [precision] When set, divisions and unit
|
|
40
|
+
* conversions that are not exact at this precision stay symbolic.
|
|
24
41
|
* @param {number} [depth]
|
|
25
42
|
* @return {Node}
|
|
26
43
|
*/
|
|
27
|
-
function simplify(node, depth = 0) {
|
|
44
|
+
function simplify(node, precision = false, depth = 0) {
|
|
28
45
|
assertDepth(depth);
|
|
29
46
|
/** @param {Node} value */
|
|
30
|
-
const child = (value) => simplify(value, depth + 1);
|
|
47
|
+
const child = (value) => simplify(value, precision, depth + 1);
|
|
31
48
|
switch (node.type) {
|
|
32
49
|
case 'Num':
|
|
33
50
|
case 'Dim':
|
|
@@ -42,9 +59,9 @@ function simplify(node, depth = 0) {
|
|
|
42
59
|
node.rawName
|
|
43
60
|
);
|
|
44
61
|
case 'Sum':
|
|
45
|
-
return simplifySum(node, child);
|
|
62
|
+
return simplifySum(node, (value) => operand(value, child), precision);
|
|
46
63
|
case 'Product':
|
|
47
|
-
return simplifyProduct(node, child);
|
|
64
|
+
return simplifyProduct(node, (value) => operand(value, child), precision);
|
|
48
65
|
}
|
|
49
66
|
}
|
|
50
67
|
|
package/src/reduce.js
CHANGED
|
@@ -61,11 +61,12 @@ function reduceCalc(value, opts) {
|
|
|
61
61
|
|
|
62
62
|
/** @type {ResolvedReduceCalcOptions} */
|
|
63
63
|
const options = {
|
|
64
|
-
precision: 5,
|
|
65
64
|
warnWhenCannotResolve: false,
|
|
66
65
|
unwrapSingleNegativeNumber: false,
|
|
67
66
|
unwrapSingleValue: false,
|
|
68
67
|
...opts,
|
|
68
|
+
// An explicit `precision: undefined` keeps the default.
|
|
69
|
+
precision: opts?.precision ?? 5,
|
|
69
70
|
};
|
|
70
71
|
/** @type {import('@csstools/css-tokenizer').CSSToken[]} */
|
|
71
72
|
let tokens;
|
package/types/lib/node.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ export type ProductFactor = {
|
|
|
42
42
|
export type Product = {
|
|
43
43
|
type: 'Product';
|
|
44
44
|
factors: ProductFactor[];
|
|
45
|
+
grouped?: boolean;
|
|
45
46
|
};
|
|
46
47
|
export type Node = Num | Dim | Ident | Call | OpaqueCall | Sum | Product;
|
|
47
48
|
/**
|
|
@@ -54,7 +55,7 @@ export type Node = Num | Dim | Ident | Call | OpaqueCall | Sum | Product;
|
|
|
54
55
|
* @typedef {{sign: 1 | -1, node: Node}} SumTerm Sign is always +1 when node is Num or Dim.
|
|
55
56
|
* @typedef {{type: 'Sum', terms: SumTerm[], grouped?: boolean}} Sum
|
|
56
57
|
* @typedef {{exponent: 1 | -1, node: Node}} ProductFactor exponent +1 = numerator, -1 = denominator.
|
|
57
|
-
* @typedef {{type: 'Product', factors: ProductFactor[]}} Product
|
|
58
|
+
* @typedef {{type: 'Product', factors: ProductFactor[], grouped?: boolean}} Product
|
|
58
59
|
* @typedef {Num | Dim | Ident | Call | OpaqueCall | Sum | Product} Node
|
|
59
60
|
*/
|
|
60
61
|
/**
|
|
@@ -99,10 +100,24 @@ declare function mkSum(rawTerms: SumTerm[]): Node;
|
|
|
99
100
|
* @return {Node}
|
|
100
101
|
*/
|
|
101
102
|
declare function mkProduct(rawFactors: ProductFactor[]): Node;
|
|
103
|
+
/**
|
|
104
|
+
* Mark a node as a source parenthesized group. A Sum or Product keeps its
|
|
105
|
+
* structure; any other node becomes a single-factor grouped Product.
|
|
106
|
+
* @param {Node} node
|
|
107
|
+
* @return {Node}
|
|
108
|
+
*/
|
|
109
|
+
declare function mkGroup(node: Node): Node;
|
|
110
|
+
/**
|
|
111
|
+
* Group a parenthesized expression when its tokens must stay bound: a sum, or
|
|
112
|
+
* a product around a substitution function.
|
|
113
|
+
* @param {Node} node
|
|
114
|
+
* @return {Node}
|
|
115
|
+
*/
|
|
116
|
+
declare function groupSubstitution(node: Node): Node;
|
|
102
117
|
/**
|
|
103
118
|
* Negate any node, preserving canonical form.
|
|
104
119
|
* @param {Node} node
|
|
105
120
|
* @return {Node}
|
|
106
121
|
*/
|
|
107
122
|
declare function negate(node: Node): Node;
|
|
108
|
-
export { num, dim, ident, call, opaqueCall, mkSum, mkProduct, negate };
|
|
123
|
+
export { num, dim, ident, call, opaqueCall, mkSum, mkProduct, mkGroup, groupSubstitution, negate, };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type CSSToken = import('@csstools/css-tokenizer').CSSToken;
|
|
2
|
+
export type Node = import('../node.js').Node;
|
|
3
|
+
export type OpaqueComponent = import('../node.js').OpaqueComponent;
|
|
4
|
+
export type ParseInput = import('./tokens.js').ParseInput;
|
|
5
|
+
export type Cursor = import('./tokens.js').Cursor;
|
|
6
|
+
export type FunctionToken = import('./tokens.js').FunctionToken;
|
|
7
|
+
export type ParseRange = (input: ParseInput, start: number, end: number) => Node;
|
|
8
|
+
/** @param {ParseInput} input @param {Cursor} cursor @param {FunctionToken} token @param {string} name @param {string} rawName @param {ParseRange} parseRange @return {Node} */
|
|
9
|
+
declare function parseOpaqueCall(input: ParseInput, cursor: Cursor, token: FunctionToken, name: string, rawName: string, parseRange: ParseRange): Node;
|
|
10
|
+
/** @param {ParseInput} input @param {Cursor} cursor @param {FunctionToken} token @param {string} name @param {string} rawName @param {ParseRange} parseRange @return {Node} */
|
|
11
|
+
declare function parseVar(input: ParseInput, cursor: Cursor, token: FunctionToken, name: string, rawName: string, parseRange: ParseRange): Node;
|
|
12
|
+
export { parseOpaqueCall, parseVar };
|