postcss-calc 11.1.1 → 11.2.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 +40 -18
- package/package.json +16 -11
- package/src/index.js +8 -5
- package/src/lib/analyze.js +228 -0
- package/src/lib/block-index.js +118 -0
- package/src/lib/calculation-type.js +25 -0
- package/src/lib/compile.js +80 -0
- package/src/lib/convertUnits.js +15 -1
- package/src/lib/functions.js +364 -0
- package/src/lib/limits.js +49 -0
- package/src/lib/node.js +85 -39
- package/src/lib/opaque.js +20 -0
- package/src/lib/parser.js +493 -257
- package/src/lib/print.js +75 -0
- package/src/lib/regex.js +4 -0
- package/src/lib/scan.js +61 -0
- package/src/lib/serialize.js +574 -207
- package/src/lib/simplify/abs.js +1 -1
- package/src/lib/simplify/bucket.js +18 -23
- package/src/lib/simplify/call.js +6 -92
- package/src/lib/simplify/product.js +7 -4
- package/src/lib/simplify/round.js +4 -4
- package/src/lib/simplify/sum.js +22 -10
- package/src/lib/simplify.js +20 -5
- package/src/reduce.js +46 -118
- package/types/index.d.ts +4 -0
- package/types/lib/analyze.d.ts +18 -0
- package/types/lib/block-index.d.ts +35 -0
- package/types/lib/calculation-type.d.ts +18 -0
- package/types/lib/compile.d.ts +37 -0
- package/types/lib/convertUnits.d.ts +8 -1
- package/types/lib/functions.d.ts +40 -0
- package/types/lib/limits.d.ts +8 -0
- package/types/lib/node.d.ts +31 -9
- package/types/lib/opaque.d.ts +9 -0
- package/types/lib/parser.d.ts +47 -49
- package/types/lib/print.d.ts +16 -0
- package/types/lib/regex.d.ts +2 -0
- package/types/lib/scan.d.ts +35 -0
- package/types/lib/serialize.d.ts +19 -2
- package/types/lib/simplify/bucket.d.ts +2 -2
- package/types/lib/simplify/call.d.ts +4 -18
- package/types/lib/simplify/round.d.ts +3 -1
- package/types/lib/simplify.d.ts +5 -2
- package/types/reduce.d.ts +47 -6
- package/src/lib/tokenizer.js +0 -139
- package/types/lib/tokenizer.d.ts +0 -32
package/src/lib/simplify/abs.js
CHANGED
|
@@ -9,10 +9,10 @@ import { convert } from '../convertUnits.js';
|
|
|
9
9
|
/**
|
|
10
10
|
* @typedef {object} UnitBucket
|
|
11
11
|
* @property {string} unit
|
|
12
|
+
* @property {string} [rawUnit]
|
|
12
13
|
* @property {number} total
|
|
13
14
|
* @property {number} scale largest |term| accumulated into `total`, for noise detection
|
|
14
15
|
* @property {import('../convertUnits.js').BaseType | null} base
|
|
15
|
-
* @property {number} order
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
/** Mutates `buckets` in place — totals of survivor buckets accumulate the
|
|
@@ -21,32 +21,27 @@ import { convert } from '../convertUnits.js';
|
|
|
21
21
|
* @return {UnitBucket[]}
|
|
22
22
|
*/
|
|
23
23
|
function mergeConvertibleBuckets(buckets) {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
/** @type {Map<import('../convertUnits.js').BaseType, UnitBucket>} */ const representative =
|
|
25
|
+
new Map();
|
|
26
26
|
/** @type {UnitBucket[]} */ const out = [];
|
|
27
|
-
for (const b of
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
for (const b of buckets) {
|
|
28
|
+
if (b.base === null) {
|
|
29
|
+
out.push(b);
|
|
30
30
|
continue;
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const converted = convert(other.total, other.unit, b.unit);
|
|
43
|
-
if (converted !== null) {
|
|
44
|
-
b.total += converted;
|
|
45
|
-
merged.add(keyO);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
32
|
+
const first = representative.get(b.base);
|
|
33
|
+
if (!first) {
|
|
34
|
+
representative.set(b.base, b);
|
|
35
|
+
out.push(b);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const converted = convert(b.total, b.unit, first.unit);
|
|
39
|
+
if (converted === null) {
|
|
40
|
+
out.push(b);
|
|
41
|
+
continue;
|
|
48
42
|
}
|
|
49
|
-
|
|
43
|
+
first.total += converted;
|
|
44
|
+
first.scale = Math.max(first.scale, Math.abs(converted));
|
|
50
45
|
}
|
|
51
46
|
return out;
|
|
52
47
|
}
|
package/src/lib/simplify/call.js
CHANGED
|
@@ -1,89 +1,13 @@
|
|
|
1
1
|
// Pre-simplify args once, route by name. Leaf folds receive simplified
|
|
2
2
|
// args so they don't need to recurse into `simplify` themselves.
|
|
3
3
|
|
|
4
|
-
import { simplifyMinMax } from './min-max.js';
|
|
5
|
-
import { simplifyClamp } from './clamp.js';
|
|
6
|
-
import { simplifyAbs } from './abs.js';
|
|
7
|
-
import { simplifySign } from './sign.js';
|
|
8
|
-
import { simplifyModRem } from './mod-rem.js';
|
|
9
|
-
import { simplifyRound } from './round.js';
|
|
10
|
-
import { simplifyTrig } from './trig.js';
|
|
11
|
-
import { simplifyInverseTrig } from './inverse-trig.js';
|
|
12
|
-
import { simplifyAtan2 } from './atan2.js';
|
|
13
|
-
import { simplifyPow } from './pow.js';
|
|
14
|
-
import { simplifySqrt } from './sqrt.js';
|
|
15
|
-
import { simplifyExp } from './exp.js';
|
|
16
|
-
import { simplifyLog } from './log.js';
|
|
17
|
-
import { simplifyHypot } from './hypot.js';
|
|
18
|
-
|
|
19
4
|
import { call } from '../node.js';
|
|
5
|
+
import { mathFunctions, isCalculationFunction } from '../functions.js';
|
|
20
6
|
|
|
21
7
|
/** @typedef {import('../node.js').Node} Node */
|
|
22
8
|
/** @typedef {import('../simplify.js').SimplifyFn} SimplifyFn */
|
|
23
|
-
|
|
24
9
|
/** @typedef {(name: string, args: Node[]) => Node} MathSimplifier */
|
|
25
10
|
|
|
26
|
-
// Bare CSS math functions with implemented simplification semantics, keyed
|
|
27
|
-
// by lowercase name. calc() and its vendor-prefixed forms are handled
|
|
28
|
-
// separately as wrappers in simplifyCall. This map is the single source of
|
|
29
|
-
// truth for dispatch, `isSupportedMathFunction`, and `QUICK_MATH_TEST`.
|
|
30
|
-
/** @type {Map<string, MathSimplifier>} */
|
|
31
|
-
const MATH_SIMPLIFIERS = new Map([
|
|
32
|
-
['min', simplifyMinMax],
|
|
33
|
-
['max', simplifyMinMax],
|
|
34
|
-
['clamp', (_name, args) => simplifyClamp(args)],
|
|
35
|
-
['abs', (_name, args) => simplifyAbs(args)],
|
|
36
|
-
['sign', (_name, args) => simplifySign(args)],
|
|
37
|
-
['mod', (_name, args) => simplifyModRem('mod', args)],
|
|
38
|
-
['rem', (_name, args) => simplifyModRem('rem', args)],
|
|
39
|
-
['round', (_name, args) => simplifyRound(args)],
|
|
40
|
-
['sin', (_name, args) => simplifyTrig('sin', args)],
|
|
41
|
-
['cos', (_name, args) => simplifyTrig('cos', args)],
|
|
42
|
-
['tan', (_name, args) => simplifyTrig('tan', args)],
|
|
43
|
-
['asin', (_name, args) => simplifyInverseTrig('asin', args)],
|
|
44
|
-
['acos', (_name, args) => simplifyInverseTrig('acos', args)],
|
|
45
|
-
['atan', (_name, args) => simplifyInverseTrig('atan', args)],
|
|
46
|
-
['atan2', (_name, args) => simplifyAtan2(args)],
|
|
47
|
-
['pow', (_name, args) => simplifyPow(args)],
|
|
48
|
-
['sqrt', (_name, args) => simplifySqrt(args)],
|
|
49
|
-
['hypot', (_name, args) => simplifyHypot(args)],
|
|
50
|
-
['log', (_name, args) => simplifyLog(args)],
|
|
51
|
-
['exp', (_name, args) => simplifyExp(args)],
|
|
52
|
-
]);
|
|
53
|
-
|
|
54
|
-
const mathFnNames = [...MATH_SIMPLIFIERS.keys()].sort(
|
|
55
|
-
(a, b) => b.length - a.length
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
const QUICK_MATH_TEST = new RegExp(
|
|
59
|
-
`(?:-(?:webkit|moz)-)?(?:calc|${mathFnNames.join('|')})\\(`,
|
|
60
|
-
'i'
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Fast check to determine whether a CSS component value could contain
|
|
65
|
-
* a supported calculation or math function call (or an escape sequence
|
|
66
|
-
* that could decode to one).
|
|
67
|
-
*
|
|
68
|
-
* @param {string} value
|
|
69
|
-
* @return {boolean}
|
|
70
|
-
*/
|
|
71
|
-
function hasPotentialMathFunction(value) {
|
|
72
|
-
return (
|
|
73
|
-
value.includes('(') && (QUICK_MATH_TEST.test(value) || value.includes('\\'))
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Whether a bare CSS math function has an implemented simplifier.
|
|
79
|
-
*
|
|
80
|
-
* @param {string} name
|
|
81
|
-
* @return {boolean}
|
|
82
|
-
*/
|
|
83
|
-
function isSupportedMathFunction(name) {
|
|
84
|
-
return MATH_SIMPLIFIERS.has(name.toLowerCase());
|
|
85
|
-
}
|
|
86
|
-
|
|
87
11
|
/**
|
|
88
12
|
* @param {Extract<Node, { type: 'Call' }>} node
|
|
89
13
|
* @param {SimplifyFn} simplify
|
|
@@ -92,7 +16,7 @@ function isSupportedMathFunction(name) {
|
|
|
92
16
|
function simplifyCall(node, simplify) {
|
|
93
17
|
const name = node.name.toLowerCase();
|
|
94
18
|
|
|
95
|
-
if (name
|
|
19
|
+
if (isCalculationFunction(name)) {
|
|
96
20
|
if (node.args.length !== 1) {
|
|
97
21
|
throw new Error(`${node.name}() takes exactly one argument`);
|
|
98
22
|
}
|
|
@@ -101,22 +25,12 @@ function simplifyCall(node, simplify) {
|
|
|
101
25
|
|
|
102
26
|
const args = node.args.map((a) => simplify(a));
|
|
103
27
|
|
|
104
|
-
const simplifier =
|
|
28
|
+
const simplifier = mathFunctions.get(name)?.simplify;
|
|
105
29
|
if (simplifier) {
|
|
106
|
-
|
|
107
|
-
// fallback; the rest normalize to lowercase internally.
|
|
108
|
-
return simplifier(
|
|
109
|
-
name === 'min' || name === 'max' ? node.name : name,
|
|
110
|
-
args
|
|
111
|
-
);
|
|
30
|
+
return simplifier(name, args);
|
|
112
31
|
}
|
|
113
32
|
|
|
114
|
-
return call(node.name, args);
|
|
33
|
+
return call(node.name, args, node.rawName);
|
|
115
34
|
}
|
|
116
35
|
|
|
117
|
-
export {
|
|
118
|
-
isSupportedMathFunction,
|
|
119
|
-
simplifyCall,
|
|
120
|
-
hasPotentialMathFunction,
|
|
121
|
-
QUICK_MATH_TEST,
|
|
122
|
-
};
|
|
36
|
+
export { simplifyCall };
|
|
@@ -15,7 +15,7 @@ import { tryCancelPair } from './cancel.js';
|
|
|
15
15
|
*/
|
|
16
16
|
function simplifyProduct(product, simplify) {
|
|
17
17
|
let coeff = 1;
|
|
18
|
-
/** @type {{exponent: 1 | -1, value: number, unit: string}[]} */
|
|
18
|
+
/** @type {{exponent: 1 | -1, value: number, unit: string, rawUnit?: string}[]} */
|
|
19
19
|
const dims = [];
|
|
20
20
|
/** @type {ProductFactor[]} */
|
|
21
21
|
const opaque = [];
|
|
@@ -54,7 +54,7 @@ function simplifyProduct(product, simplify) {
|
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
if (n.type === 'Dim') {
|
|
57
|
-
dims.push({ exponent, value: n.value, unit: n.unit });
|
|
57
|
+
dims.push({ exponent, value: n.value, unit: n.unit, rawUnit: n.rawUnit });
|
|
58
58
|
scalarChain.push({ exponent, value: n.value });
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
@@ -112,7 +112,7 @@ function simplifyProduct(product, simplify) {
|
|
|
112
112
|
value = value / f.value;
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
-
return dim(value, d.unit);
|
|
115
|
+
return dim(value, d.unit, d.rawUnit);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
if (remainingDims.length === 0 && opaque.length === 0) {
|
|
@@ -125,7 +125,10 @@ function simplifyProduct(product, simplify) {
|
|
|
125
125
|
factors.push({ exponent: 1, node: num(coeff) });
|
|
126
126
|
}
|
|
127
127
|
for (const d of remainingDims) {
|
|
128
|
-
factors.push({
|
|
128
|
+
factors.push({
|
|
129
|
+
exponent: d.exponent,
|
|
130
|
+
node: dim(d.value, d.unit, d.rawUnit),
|
|
131
|
+
});
|
|
129
132
|
}
|
|
130
133
|
factors.push(...opaque);
|
|
131
134
|
|
|
@@ -44,11 +44,11 @@ function simplifyRound(args) {
|
|
|
44
44
|
// to applyRound, where floor*b===ceil*b===±∞ collapses back to A
|
|
45
45
|
// (§10.3.1 "result is the same infinity").
|
|
46
46
|
if (Number.isNaN(b)) {
|
|
47
|
-
return
|
|
47
|
+
return foldResult(fold, Number.NaN);
|
|
48
48
|
}
|
|
49
49
|
if (!Number.isFinite(b)) {
|
|
50
50
|
if (!Number.isFinite(a)) {
|
|
51
|
-
return
|
|
51
|
+
return foldResult(fold, Number.NaN);
|
|
52
52
|
}
|
|
53
53
|
let result;
|
|
54
54
|
if (strategy === 'up' && a > 0) {
|
|
@@ -63,7 +63,7 @@ function simplifyRound(args) {
|
|
|
63
63
|
|
|
64
64
|
const result = applyRound(strategy, a, b);
|
|
65
65
|
if (Number.isNaN(result)) {
|
|
66
|
-
return
|
|
66
|
+
return foldResult(fold, Number.NaN);
|
|
67
67
|
}
|
|
68
68
|
return foldResult(fold, result);
|
|
69
69
|
}
|
|
@@ -116,4 +116,4 @@ function applyRound(strategy, a, b) {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
export { simplifyRound };
|
|
119
|
+
export { ROUND_STRATEGIES, simplifyRound };
|
package/src/lib/simplify/sum.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mkSum, num, dim } from '../node.js';
|
|
2
|
-
import {
|
|
2
|
+
import { staticBaseOf } from '../convertUnits.js';
|
|
3
3
|
import { mergeConvertibleBuckets } from './bucket.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -34,12 +34,12 @@ function simplifySum(sum, simplify) {
|
|
|
34
34
|
// encountered unit. `100vh - 5rem - 10rem - 100px` → `-15rem` in phase 1,
|
|
35
35
|
// then vh/rem/px stay separate in phase 2 (none convert to each other).
|
|
36
36
|
let numTotal = 0;
|
|
37
|
+
let hasNum = false;
|
|
37
38
|
let numScale = 0;
|
|
38
39
|
/** @type {Map<string, UnitBucket>} */
|
|
39
40
|
const byUnit = new Map();
|
|
40
41
|
/** @type {SumTerm[]} */
|
|
41
42
|
const opaque = [];
|
|
42
|
-
let bucketOrder = 0;
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* @param {1 | -1} sign
|
|
@@ -65,7 +65,11 @@ function simplifySum(sum, simplify) {
|
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
67
|
if (n.type === 'Num') {
|
|
68
|
-
|
|
68
|
+
// Do not add an artificial +0 before the first value: `+0 + -0`
|
|
69
|
+
// becomes +0 in IEEE-754 and would discard a parsed/simplified -0.
|
|
70
|
+
const value = sign * n.value;
|
|
71
|
+
numTotal = hasNum ? numTotal + value : value;
|
|
72
|
+
hasNum = true;
|
|
69
73
|
numScale = Math.max(numScale, Math.abs(n.value));
|
|
70
74
|
return;
|
|
71
75
|
}
|
|
@@ -78,10 +82,10 @@ function simplifySum(sum, simplify) {
|
|
|
78
82
|
} else {
|
|
79
83
|
byUnit.set(key, {
|
|
80
84
|
unit: n.unit,
|
|
85
|
+
rawUnit: n.rawUnit,
|
|
81
86
|
total: sign * n.value,
|
|
82
87
|
scale: Math.abs(n.value),
|
|
83
|
-
base:
|
|
84
|
-
order: bucketOrder++,
|
|
88
|
+
base: staticBaseOf(n.unit),
|
|
85
89
|
});
|
|
86
90
|
}
|
|
87
91
|
return;
|
|
@@ -93,15 +97,23 @@ function simplifySum(sum, simplify) {
|
|
|
93
97
|
processTerm(t.sign, simplify(t.node));
|
|
94
98
|
}
|
|
95
99
|
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// type
|
|
100
|
+
// Keep a numeric total, including zero, when the source sum contained a
|
|
101
|
+
// number. In a mixed or unresolved sum that value constrains the other
|
|
102
|
+
// terms to <number>; mkSum retains it as a type anchor. A negative zero is
|
|
103
|
+
// also deliberately retained. Zero-valued unit buckets are kept for type
|
|
104
|
+
// info (WPT calc-serialization-002).
|
|
99
105
|
/** @type {SumTerm[]} */
|
|
100
|
-
const terms =
|
|
106
|
+
const terms = hasNum
|
|
107
|
+
? [{ sign: /** @type {1} */ (1), node: num(denoise(numTotal, numScale)) }]
|
|
108
|
+
: [];
|
|
101
109
|
for (const bucket of mergeConvertibleBuckets([...byUnit.values()])) {
|
|
102
110
|
terms.push({
|
|
103
111
|
sign: 1,
|
|
104
|
-
node: dim(
|
|
112
|
+
node: dim(
|
|
113
|
+
denoise(bucket.total, bucket.scale),
|
|
114
|
+
bucket.unit,
|
|
115
|
+
bucket.rawUnit
|
|
116
|
+
),
|
|
105
117
|
});
|
|
106
118
|
}
|
|
107
119
|
terms.push(...opaque);
|
package/src/lib/simplify.js
CHANGED
|
@@ -5,31 +5,46 @@
|
|
|
5
5
|
import { simplifySum } from './simplify/sum.js';
|
|
6
6
|
import { simplifyProduct } from './simplify/product.js';
|
|
7
7
|
import { simplifyCall } from './simplify/call.js';
|
|
8
|
+
import { simplifyComponents } from './opaque.js';
|
|
9
|
+
import { opaqueCall } from './node.js';
|
|
10
|
+
import { assertDepth } from './limits.js';
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* @typedef {import('./node.js').Node} Node
|
|
11
14
|
*
|
|
12
|
-
* Recursive simplifier reference, threaded into Sum/Product/Call. Lets
|
|
15
|
+
* Recursive simplifier reference, threaded into Sum/Product/Call/OpaqueCall. Lets
|
|
13
16
|
* leaf fold modules avoid circular imports of the entry function.
|
|
14
17
|
* @typedef {(node: Node) => Node} SimplifyFn
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
20
|
/**
|
|
21
|
+
* Simplify is an independent, composable AST transformation. It may
|
|
22
|
+
* synthesize canonical nodes while preserving the Node -> Node contract.
|
|
18
23
|
* @param {Node} node
|
|
24
|
+
* @param {number} [depth]
|
|
19
25
|
* @return {Node}
|
|
20
26
|
*/
|
|
21
|
-
function simplify(node) {
|
|
27
|
+
function simplify(node, depth = 0) {
|
|
28
|
+
assertDepth(depth);
|
|
29
|
+
/** @param {Node} value */
|
|
30
|
+
const child = (value) => simplify(value, depth + 1);
|
|
22
31
|
switch (node.type) {
|
|
23
32
|
case 'Num':
|
|
24
33
|
case 'Dim':
|
|
25
34
|
case 'Ident':
|
|
26
35
|
return node;
|
|
27
36
|
case 'Call':
|
|
28
|
-
return simplifyCall(node,
|
|
37
|
+
return simplifyCall(node, child);
|
|
38
|
+
case 'OpaqueCall':
|
|
39
|
+
return opaqueCall(
|
|
40
|
+
node.name,
|
|
41
|
+
simplifyComponents(node.components, child),
|
|
42
|
+
node.rawName
|
|
43
|
+
);
|
|
29
44
|
case 'Sum':
|
|
30
|
-
return simplifySum(node,
|
|
45
|
+
return simplifySum(node, child);
|
|
31
46
|
case 'Product':
|
|
32
|
-
return simplifyProduct(node,
|
|
47
|
+
return simplifyProduct(node, child);
|
|
33
48
|
}
|
|
34
49
|
}
|
|
35
50
|
|
package/src/reduce.js
CHANGED
|
@@ -1,33 +1,20 @@
|
|
|
1
1
|
// CSS component-value reducer. This module deliberately has no PostCSS
|
|
2
2
|
// dependency so it can also be used for individual declaration values,
|
|
3
3
|
// at-rule parameters, or selector text.
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from '
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
isSupportedMathFunction,
|
|
13
|
-
hasPotentialMathFunction,
|
|
14
|
-
QUICK_MATH_TEST,
|
|
15
|
-
} from './lib/simplify/call.js';
|
|
16
|
-
import { serialize } from './lib/serialize.js';
|
|
17
|
-
|
|
18
|
-
const MATCH_CALC = /^(?:-(?:moz|webkit)-)?calc$/i;
|
|
19
|
-
|
|
20
|
-
const BLOCK_CLOSE = new Map([
|
|
21
|
-
[CssType.OpenParen, CssType.CloseParen],
|
|
22
|
-
[CssType.OpenSquare, CssType.CloseSquare],
|
|
23
|
-
[CssType.OpenCurly, CssType.CloseCurly],
|
|
24
|
-
]);
|
|
4
|
+
import { tokenize as cssTokenize } from '@csstools/css-tokenizer';
|
|
5
|
+
import { indexBlocks } from './lib/block-index.js';
|
|
6
|
+
import { hasPotentialMathFunction } from './lib/functions.js';
|
|
7
|
+
import { assertDepth } from './lib/limits.js';
|
|
8
|
+
import { findCalculations } from './lib/scan.js';
|
|
9
|
+
import { compileCandidates } from './lib/compile.js';
|
|
10
|
+
import { applyReplacements } from './lib/print.js';
|
|
25
11
|
|
|
26
12
|
/**
|
|
27
13
|
* @typedef {object} ReduceCalcOptions
|
|
28
14
|
* @property {number | false} [precision]
|
|
29
15
|
* @property {boolean} [warnWhenCannotResolve]
|
|
30
|
-
* @property {boolean} [unwrapSingleNegativeNumber]
|
|
16
|
+
* @property {boolean} [unwrapSingleNegativeNumber] Deprecated alias for `unwrapSingleValue`.
|
|
17
|
+
* @property {boolean} [unwrapSingleValue] Serialize fully resolved finite scalar results without calculation syntax. Defaults to `false`.
|
|
31
18
|
* @property {(error: Error, input: string) => void} [onParseError] Invoked when parse/simplify throws.
|
|
32
19
|
* @property {(message: string) => void} [onWarn] Invoked when `warnWhenCannotResolve` is set and an expression cannot be reduced to a single value.
|
|
33
20
|
*/
|
|
@@ -41,89 +28,23 @@ const BLOCK_CLOSE = new Map([
|
|
|
41
28
|
* @property {ResolvedReduceCalcOptions} options
|
|
42
29
|
* @property {string} value
|
|
43
30
|
* @property {import('@csstools/css-tokenizer').CSSToken[]} tokens
|
|
44
|
-
* @property {Replacement[]} replacements
|
|
45
31
|
*/
|
|
46
32
|
|
|
47
33
|
/**
|
|
48
34
|
* @typedef {object} Replacement
|
|
49
35
|
* @property {number} start
|
|
50
36
|
* @property {number} end
|
|
51
|
-
* @property {
|
|
52
|
-
* @property {string} calcName
|
|
37
|
+
* @property {CalculationResult} result
|
|
53
38
|
*/
|
|
54
|
-
|
|
55
39
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* @
|
|
62
|
-
* @
|
|
63
|
-
* @param {TransformContext} ctx
|
|
64
|
-
* @param {boolean} transform
|
|
65
|
-
* @return {number} Index of the matching closer, or the EOF token.
|
|
40
|
+
* @typedef {object} CalculationResult
|
|
41
|
+
* @property {import('./lib/node.js').Node} tree
|
|
42
|
+
* @property {'resolved' | 'unresolved'} status
|
|
43
|
+
* @property {string} rootName
|
|
44
|
+
* @property {string} rootSpelling
|
|
45
|
+
* @property {boolean} calculation
|
|
46
|
+
* @property {string | undefined} original
|
|
66
47
|
*/
|
|
67
|
-
function walkTokens(start, expectedClose, ctx, transform) {
|
|
68
|
-
for (let i = start; i < ctx.tokens.length; i++) {
|
|
69
|
-
const token = ctx.tokens[i];
|
|
70
|
-
if (token[0] === CssType.EOF || token[0] === expectedClose) return i;
|
|
71
|
-
|
|
72
|
-
const blockClose = BLOCK_CLOSE.get(token[0]);
|
|
73
|
-
if (blockClose) {
|
|
74
|
-
i = walkTokens(i + 1, blockClose, ctx, transform);
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (token[0] !== CssType.Function) continue;
|
|
79
|
-
|
|
80
|
-
const name = token[4].value;
|
|
81
|
-
const isCalc = MATCH_CALC.test(name);
|
|
82
|
-
const isMath = !isCalc && isSupportedMathFunction(name);
|
|
83
|
-
if (!transform || (!isCalc && !isMath)) {
|
|
84
|
-
i = walkTokens(i + 1, CssType.CloseParen, ctx, transform);
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Locate the complete outer function without transforming its children.
|
|
89
|
-
const close = walkTokens(i + 1, CssType.CloseParen, ctx, false);
|
|
90
|
-
const closed = ctx.tokens[close][0] === CssType.CloseParen;
|
|
91
|
-
const end = closed ? ctx.tokens[close][3] + 1 : ctx.value.length;
|
|
92
|
-
const sliceStart = isCalc ? i + 1 : i;
|
|
93
|
-
const sliceEnd = closed ? close + (isCalc ? 0 : 1) : close;
|
|
94
|
-
const inputStart = isCalc ? token[3] + 1 : token[2];
|
|
95
|
-
const inputEnd = closed && isCalc ? ctx.tokens[close][2] : end;
|
|
96
|
-
const contents = ctx.value.slice(inputStart, inputEnd);
|
|
97
|
-
try {
|
|
98
|
-
const node = simplify(
|
|
99
|
-
parse(tokenizeTokens(ctx.tokens, end, sliceStart, sliceEnd))
|
|
100
|
-
);
|
|
101
|
-
ctx.replacements.push({
|
|
102
|
-
start: token[2],
|
|
103
|
-
end,
|
|
104
|
-
node,
|
|
105
|
-
calcName: isCalc ? name : 'calc',
|
|
106
|
-
});
|
|
107
|
-
} catch (error) {
|
|
108
|
-
const err = error instanceof Error ? error : new Error('Error');
|
|
109
|
-
ctx.options.onParseError?.(err, contents);
|
|
110
|
-
}
|
|
111
|
-
i = close;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return ctx.tokens.length - 1;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* @param {import('./lib/node.js').Node} node
|
|
119
|
-
* @return {boolean}
|
|
120
|
-
*/
|
|
121
|
-
function isUnresolvedResult(node) {
|
|
122
|
-
if (node.type === 'Sum' || node.type === 'Product') {
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
return node.type === 'Call' && isSupportedMathFunction(node.name);
|
|
126
|
-
}
|
|
127
48
|
|
|
128
49
|
/**
|
|
129
50
|
* Simplify every supported CSS math function in a component-value string.
|
|
@@ -143,34 +64,41 @@ function reduceCalc(value, opts) {
|
|
|
143
64
|
precision: 5,
|
|
144
65
|
warnWhenCannotResolve: false,
|
|
145
66
|
unwrapSingleNegativeNumber: false,
|
|
67
|
+
unwrapSingleValue: false,
|
|
146
68
|
...opts,
|
|
147
69
|
};
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
70
|
+
/** @type {import('@csstools/css-tokenizer').CSSToken[]} */
|
|
71
|
+
let tokens;
|
|
72
|
+
/** @type {ReturnType<typeof indexBlocks>} */
|
|
73
|
+
let index;
|
|
74
|
+
try {
|
|
75
|
+
tokens = cssTokenize({ css: value });
|
|
76
|
+
index = indexBlocks(tokens);
|
|
77
|
+
assertDepth(index.maxDepth);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
options.onParseError?.(
|
|
80
|
+
error instanceof Error ? error : new Error('Error', { cause: error }),
|
|
81
|
+
value
|
|
82
|
+
);
|
|
154
83
|
return value;
|
|
155
84
|
}
|
|
156
85
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
options.onWarn?.('Could not reduce expression: ' + value);
|
|
167
|
-
}
|
|
168
|
-
output += value.slice(lastIndex, replacement.start) + text;
|
|
169
|
-
lastIndex = replacement.end;
|
|
86
|
+
const candidates = findCalculations(value, tokens, index);
|
|
87
|
+
const replacements = compileCandidates(candidates, {
|
|
88
|
+
options,
|
|
89
|
+
value,
|
|
90
|
+
tokens,
|
|
91
|
+
index,
|
|
92
|
+
});
|
|
93
|
+
if (replacements.length === 0) {
|
|
94
|
+
return value;
|
|
170
95
|
}
|
|
171
|
-
|
|
172
|
-
|
|
96
|
+
const serializationOptions = {
|
|
97
|
+
precision: options.precision,
|
|
98
|
+
unwrapSingleNegativeNumber: options.unwrapSingleNegativeNumber,
|
|
99
|
+
unwrapSingleValue: options.unwrapSingleValue,
|
|
100
|
+
};
|
|
101
|
+
return applyReplacements(value, replacements, options, serializationOptions);
|
|
173
102
|
}
|
|
174
103
|
|
|
175
|
-
export { QUICK_MATH_TEST, hasPotentialMathFunction };
|
|
176
104
|
export default reduceCalc;
|
package/types/index.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ export type PostCssCalcOptions = {
|
|
|
3
3
|
warnWhenCannotResolve?: boolean;
|
|
4
4
|
mediaQueries?: boolean;
|
|
5
5
|
selectors?: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Serialize fully resolved finite scalar results without calculation syntax. Defaults to `false`.
|
|
8
|
+
*/
|
|
9
|
+
unwrapSingleValue?: boolean;
|
|
6
10
|
/**
|
|
7
11
|
* Invoked when parse/simplify throws. Replaces the default `result.warn`.
|
|
8
12
|
*/
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type Node = import('./node.js').Node;
|
|
2
|
+
export type CalculationType = import('./functions.js').CalculationType;
|
|
3
|
+
export type AnalysisType = 'number' | 'unknown' | {
|
|
4
|
+
dimension: string | null;
|
|
5
|
+
};
|
|
6
|
+
export type Analysis = {
|
|
7
|
+
type: AnalysisType;
|
|
8
|
+
valid: boolean;
|
|
9
|
+
unresolved: boolean;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Analyze the original complete tree and return its root summary. Analysis
|
|
13
|
+
* validates and classifies the tree; it is not a rewrite plan.
|
|
14
|
+
* @param {Node} node
|
|
15
|
+
* @return {Analysis}
|
|
16
|
+
*/
|
|
17
|
+
declare function analyze(node: Node): Analysis;
|
|
18
|
+
export { analyze };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type CSSToken = import('@csstools/css-tokenizer').CSSToken;
|
|
2
|
+
/**
|
|
3
|
+
* Read-only delimiter navigation for one native token stream range.
|
|
4
|
+
*/
|
|
5
|
+
declare class BlockIndex {
|
|
6
|
+
#private;
|
|
7
|
+
/**
|
|
8
|
+
* Build the delimiter index once for a token stream range. Matching remains
|
|
9
|
+
* LIFO: a mismatched closer is ignored and does not disturb the open stack.
|
|
10
|
+
*
|
|
11
|
+
* @param {CSSToken[]} tokens
|
|
12
|
+
* @param {number} start
|
|
13
|
+
* @param {number} end
|
|
14
|
+
*/
|
|
15
|
+
constructor(tokens: CSSToken[], start: number, end: number);
|
|
16
|
+
/** @return {number} */
|
|
17
|
+
get maxDepth(): number;
|
|
18
|
+
/** @param {number} openPosition @param {number} [endPosition] @return {number} */
|
|
19
|
+
closeOf(openPosition: number, endPosition?: number): number;
|
|
20
|
+
/** @param {number} position @param {number} endPosition @return {number} */
|
|
21
|
+
nextComponent(position: number, endPosition: number): number;
|
|
22
|
+
/** @param {number} startPosition @param {number} endPosition @return {number} */
|
|
23
|
+
firstTopLevelComma(startPosition: number, endPosition: number): number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build the delimiter index once for a token stream range. Matching remains
|
|
27
|
+
* LIFO: a mismatched closer is ignored and does not disturb the open stack.
|
|
28
|
+
*
|
|
29
|
+
* @param {CSSToken[]} tokens
|
|
30
|
+
* @param {number} [start]
|
|
31
|
+
* @param {number} [end]
|
|
32
|
+
* @return {BlockIndex}
|
|
33
|
+
*/
|
|
34
|
+
declare function indexBlocks(tokens: CSSToken[], start?: number, end?: number): BlockIndex;
|
|
35
|
+
export { indexBlocks };
|