postcss-calc 11.1.0 → 11.1.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/README.md +38 -8
- package/package.json +6 -5
- package/src/index.js +16 -4
- package/src/lib/convertUnits.js +15 -1
- package/src/lib/node.js +22 -18
- package/src/lib/opaque.js +40 -0
- package/src/lib/parser.js +381 -200
- package/src/lib/serialize.js +49 -16
- package/src/lib/simplify/abs.js +1 -1
- package/src/lib/simplify/bucket.js +18 -23
- package/src/lib/simplify/call.js +8 -1
- package/src/lib/simplify/product.js +7 -4
- package/src/lib/simplify/sum.js +8 -5
- package/src/lib/tokenizer.js +6 -135
- package/src/reduce.js +22 -14
- package/types/lib/convertUnits.d.ts +8 -1
- package/types/lib/node.d.ts +12 -6
- package/types/lib/opaque.d.ts +15 -0
- package/types/lib/parser.d.ts +40 -36
- package/types/lib/serialize.d.ts +4 -0
- package/types/lib/simplify/bucket.d.ts +2 -2
- package/types/lib/tokenizer.d.ts +3 -32
- package/types/reduce.d.ts +4 -1
package/src/lib/serialize.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Spec: https://www.w3.org/TR/css-values-4/#serialize-a-calculation-tree
|
|
2
|
-
// Outer calc() is added
|
|
3
|
-
//
|
|
4
|
-
// are ever required on valid canonical input.
|
|
2
|
+
// Outer calc() is added when the top-level result contains an arithmetic
|
|
3
|
+
// operator, or when a finite scalar is negative.
|
|
5
4
|
|
|
6
5
|
import { num, dim } from './node.js';
|
|
6
|
+
import { getComponents, serializeComponents } from './opaque.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @typedef {import('./node.js').Node} Node
|
|
@@ -13,6 +13,7 @@ import { num, dim } from './node.js';
|
|
|
13
13
|
* @typedef {object} SerializeOptions
|
|
14
14
|
* @property {number | false} [precision] Decimal places for numbers. `false` disables rounding. Default 5.
|
|
15
15
|
* @property {string} [calcName] Wrapper name to use when `calc()` is needed. Default `'calc'`.
|
|
16
|
+
* @property {boolean} [unwrapSingleNegativeNumber] Serialize finite negative scalars without a wrapper. Internal selector-only mode.
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
// Below this is float noise, not a value: `0.1 + 0.2 - 0.3` is 5.5e-17.
|
|
@@ -80,6 +81,20 @@ function serializeNumber(v) {
|
|
|
80
81
|
return text;
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Round and serialize a finite scalar once so callers can use the same value
|
|
86
|
+
* to decide its syntactic context and render its text.
|
|
87
|
+
*
|
|
88
|
+
* @param {import('./node.js').Num | import('./node.js').Dim} node
|
|
89
|
+
* @param {number | false} prec
|
|
90
|
+
* @return {{value: number, text: string}}
|
|
91
|
+
*/
|
|
92
|
+
function serializeScalar(node, prec) {
|
|
93
|
+
const value = round(node.value, prec);
|
|
94
|
+
const text = `${serializeNumber(value)}${node.type === 'Dim' ? (node.rawUnit ?? node.unit) : ''}`;
|
|
95
|
+
return { value, text };
|
|
96
|
+
}
|
|
97
|
+
|
|
83
98
|
/**
|
|
84
99
|
* @param {Node} node
|
|
85
100
|
* @param {SerializeOptions} [opts]
|
|
@@ -95,7 +110,23 @@ function serialize(node, opts = {}) {
|
|
|
95
110
|
return `${calcName}(${degenerateKeyword(node.value)})`;
|
|
96
111
|
}
|
|
97
112
|
if (node.type === 'Dim' && isDegenerate(node.value)) {
|
|
98
|
-
return `${calcName}(${degenerateKeyword(node.value)} * 1${node.unit})`;
|
|
113
|
+
return `${calcName}(${degenerateKeyword(node.value)} * 1${node.rawUnit ?? node.unit})`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (node.type === 'Num' || node.type === 'Dim') {
|
|
117
|
+
const scalar = serializeScalar(node, prec);
|
|
118
|
+
|
|
119
|
+
// A finite negative scalar must stay inside calc() so CSS parses it as a
|
|
120
|
+
// calculation result (and can apply range clamping) rather than as an
|
|
121
|
+
// invalid bare value. Base this on the serialized value so tiny negative
|
|
122
|
+
// floating-point noise that rounds to zero does not get wrapped.
|
|
123
|
+
if (scalar.value < 0) {
|
|
124
|
+
return opts.unwrapSingleNegativeNumber
|
|
125
|
+
? scalar.text
|
|
126
|
+
: `${calcName}(${scalar.text})`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return scalar.text;
|
|
99
130
|
}
|
|
100
131
|
|
|
101
132
|
// A grouped sum with a leading negative term is the canonical result of
|
|
@@ -114,12 +145,7 @@ function serialize(node, opts = {}) {
|
|
|
114
145
|
return `${calcName}(-(${serializeSumTerms(invertedTerms, prec)}))`;
|
|
115
146
|
}
|
|
116
147
|
|
|
117
|
-
if (
|
|
118
|
-
node.type === 'Num' ||
|
|
119
|
-
node.type === 'Dim' ||
|
|
120
|
-
node.type === 'Ident' ||
|
|
121
|
-
node.type === 'Call'
|
|
122
|
-
) {
|
|
148
|
+
if (node.type === 'Ident' || node.type === 'Call') {
|
|
123
149
|
return serializeExpr(node, prec);
|
|
124
150
|
}
|
|
125
151
|
|
|
@@ -145,20 +171,27 @@ function serializeExpr(node, prec) {
|
|
|
145
171
|
if (isDegenerate(node.value)) {
|
|
146
172
|
return degenerateKeyword(node.value);
|
|
147
173
|
}
|
|
148
|
-
return
|
|
174
|
+
return serializeScalar(node, prec).text;
|
|
149
175
|
case 'Dim':
|
|
150
176
|
if (isDegenerate(node.value)) {
|
|
151
177
|
// Nested degenerate Dim wraps in calc() so the `<kw> * 1<unit>` form
|
|
152
178
|
// parses back as one Dim factor. The bare form round-trips wrong
|
|
153
179
|
// inside a Product — `0 * Dim(Infinity, px)` would re-fold as NaN.
|
|
154
|
-
return `calc(${degenerateKeyword(node.value)} * 1${node.unit})`;
|
|
180
|
+
return `calc(${degenerateKeyword(node.value)} * 1${node.rawUnit ?? node.unit})`;
|
|
155
181
|
}
|
|
156
|
-
return
|
|
182
|
+
return serializeScalar(node, prec).text;
|
|
157
183
|
case 'Ident':
|
|
158
|
-
return node.name;
|
|
184
|
+
return node.rawName ?? node.name;
|
|
159
185
|
case 'Call': {
|
|
186
|
+
const components = getComponents(node);
|
|
187
|
+
if (components) {
|
|
188
|
+
const args = node.args
|
|
189
|
+
.map((arg) => serializeExpr(arg, prec))
|
|
190
|
+
.join(', ');
|
|
191
|
+
return `${node.rawName ?? node.name}(${args}${serializeComponents(components, (child) => serialize(child, { precision: prec }))})`;
|
|
192
|
+
}
|
|
160
193
|
const args = node.args.map((a) => serializeExpr(a, prec)).join(', ');
|
|
161
|
-
return `${node.name}(${args})`;
|
|
194
|
+
return `${node.rawName ?? node.name}(${args})`;
|
|
162
195
|
}
|
|
163
196
|
case 'Sum':
|
|
164
197
|
return serializeSum(node, prec);
|
|
@@ -187,7 +220,7 @@ function displaySign(term) {
|
|
|
187
220
|
if (node.type === 'Dim' && Number.isFinite(node.value) && node.value < 0) {
|
|
188
221
|
return {
|
|
189
222
|
sign: /** @type {1 | -1} */ (-sign),
|
|
190
|
-
magnitude: dim(-node.value, node.unit),
|
|
223
|
+
magnitude: dim(-node.value, node.unit, node.rawUnit),
|
|
191
224
|
};
|
|
192
225
|
}
|
|
193
226
|
return { sign, magnitude: node };
|
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
|
@@ -17,6 +17,7 @@ import { simplifyLog } from './log.js';
|
|
|
17
17
|
import { simplifyHypot } from './hypot.js';
|
|
18
18
|
|
|
19
19
|
import { call } from '../node.js';
|
|
20
|
+
import { getComponents, setComponents, simplifyComponents } from '../opaque.js';
|
|
20
21
|
|
|
21
22
|
/** @typedef {import('../node.js').Node} Node */
|
|
22
23
|
/** @typedef {import('../simplify.js').SimplifyFn} SimplifyFn */
|
|
@@ -101,6 +102,12 @@ function simplifyCall(node, simplify) {
|
|
|
101
102
|
|
|
102
103
|
const args = node.args.map((a) => simplify(a));
|
|
103
104
|
|
|
105
|
+
const components = getComponents(node);
|
|
106
|
+
if (components) {
|
|
107
|
+
const result = call(node.name, args, node.rawName);
|
|
108
|
+
return setComponents(result, simplifyComponents(components, simplify));
|
|
109
|
+
}
|
|
110
|
+
|
|
104
111
|
const simplifier = MATH_SIMPLIFIERS.get(name);
|
|
105
112
|
if (simplifier) {
|
|
106
113
|
// min/max preserve the call's original casing in their opaque-args
|
|
@@ -111,7 +118,7 @@ function simplifyCall(node, simplify) {
|
|
|
111
118
|
);
|
|
112
119
|
}
|
|
113
120
|
|
|
114
|
-
return call(node.name, args);
|
|
121
|
+
return call(node.name, args, node.rawName);
|
|
115
122
|
}
|
|
116
123
|
|
|
117
124
|
export {
|
|
@@ -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
|
|
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
|
/**
|
|
@@ -39,7 +39,6 @@ function simplifySum(sum, simplify) {
|
|
|
39
39
|
const byUnit = new Map();
|
|
40
40
|
/** @type {SumTerm[]} */
|
|
41
41
|
const opaque = [];
|
|
42
|
-
let bucketOrder = 0;
|
|
43
42
|
|
|
44
43
|
/**
|
|
45
44
|
* @param {1 | -1} sign
|
|
@@ -78,10 +77,10 @@ function simplifySum(sum, simplify) {
|
|
|
78
77
|
} else {
|
|
79
78
|
byUnit.set(key, {
|
|
80
79
|
unit: n.unit,
|
|
80
|
+
rawUnit: n.rawUnit,
|
|
81
81
|
total: sign * n.value,
|
|
82
82
|
scale: Math.abs(n.value),
|
|
83
|
-
base:
|
|
84
|
-
order: bucketOrder++,
|
|
83
|
+
base: staticBaseOf(n.unit),
|
|
85
84
|
});
|
|
86
85
|
}
|
|
87
86
|
return;
|
|
@@ -101,7 +100,11 @@ function simplifySum(sum, simplify) {
|
|
|
101
100
|
for (const bucket of mergeConvertibleBuckets([...byUnit.values()])) {
|
|
102
101
|
terms.push({
|
|
103
102
|
sign: 1,
|
|
104
|
-
node: dim(
|
|
103
|
+
node: dim(
|
|
104
|
+
denoise(bucket.total, bucket.scale),
|
|
105
|
+
bucket.unit,
|
|
106
|
+
bucket.rawUnit
|
|
107
|
+
),
|
|
105
108
|
});
|
|
106
109
|
}
|
|
107
110
|
terms.push(...opaque);
|
package/src/lib/tokenizer.js
CHANGED
|
@@ -1,139 +1,10 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Thin project-local entry point for the CSS Syntax tokenizer. The parser
|
|
2
|
+
// consumes native tokens directly so decoded values and source spelling remain available.
|
|
3
|
+
import { tokenize as tokenizeCss } from '@csstools/css-tokenizer';
|
|
2
4
|
|
|
3
|
-
import
|
|
4
|
-
tokenize as tokenizeCss,
|
|
5
|
-
TokenType as CssType,
|
|
6
|
-
} from '@csstools/css-tokenizer';
|
|
7
|
-
/**
|
|
8
|
-
* @typedef {'number' | 'dimension' | 'ident' | 'punct' | 'eof'} TokenType
|
|
9
|
-
* @typedef {object} Token
|
|
10
|
-
* @property {TokenType} type
|
|
11
|
-
* @property {string} value
|
|
12
|
-
* @property {string} [unit] Present on `dimension` tokens; `%` for percentages.
|
|
13
|
-
* @property {number} pos
|
|
14
|
-
* @property {boolean} ws Whitespace immediately before — drives the §10.1 `+`/`-` rule.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const PUNCT_DELIMS = new Set(['+', '-', '*', '/']);
|
|
18
|
-
|
|
19
|
-
const NUMERIC_RAW = /^[+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?/;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @param {string} input
|
|
23
|
-
* @return {Token[]}
|
|
24
|
-
*/
|
|
5
|
+
/** @param {string} input @return {import('@csstools/css-tokenizer').CSSToken[]} */
|
|
25
6
|
function tokenize(input) {
|
|
26
|
-
return
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* CSS absorbs leading signs (`-5px` is one token); the parser expects
|
|
31
|
-
* punct sign + unsigned numeric, so split them back out.
|
|
32
|
-
* @param {Token[]} tokens
|
|
33
|
-
* @param {string} raw
|
|
34
|
-
* @param {string | undefined} unit
|
|
35
|
-
* @param {number} pos
|
|
36
|
-
* @param {boolean} ws
|
|
37
|
-
* @return {void}
|
|
38
|
-
*/
|
|
39
|
-
function pushNumeric(tokens, raw, unit, pos, ws) {
|
|
40
|
-
let value = /** @type {RegExpExecArray} */ (NUMERIC_RAW.exec(raw))[0];
|
|
41
|
-
const sign = value[0];
|
|
42
|
-
if (sign === '+' || sign === '-') {
|
|
43
|
-
tokens.push({ type: 'punct', value: sign, pos, ws });
|
|
44
|
-
value = value.slice(1);
|
|
45
|
-
pos += 1;
|
|
46
|
-
ws = false;
|
|
47
|
-
}
|
|
48
|
-
if (unit === undefined) {
|
|
49
|
-
tokens.push({ type: 'number', value, pos, ws });
|
|
50
|
-
} else {
|
|
51
|
-
tokens.push({ type: 'dimension', value, unit, pos, ws });
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Convert a slice of an existing CSS token stream into the token subset used
|
|
57
|
-
* by the calculation parser. Token positions remain relative to the original
|
|
58
|
-
* source text, which keeps parse errors useful to adapter callers.
|
|
59
|
-
*
|
|
60
|
-
* @param {import('@csstools/css-tokenizer').CSSToken[]} cssTokens
|
|
61
|
-
* @param {number} eofPosition
|
|
62
|
-
* @param {number} [start]
|
|
63
|
-
* @param {number} [end]
|
|
64
|
-
* @return {Token[]}
|
|
65
|
-
*/
|
|
66
|
-
function tokenizeTokens(
|
|
67
|
-
cssTokens,
|
|
68
|
-
eofPosition,
|
|
69
|
-
start = 0,
|
|
70
|
-
end = cssTokens.length
|
|
71
|
-
) {
|
|
72
|
-
/** @type {Token[]} */
|
|
73
|
-
const tokens = [];
|
|
74
|
-
let ws = true;
|
|
75
|
-
|
|
76
|
-
for (let i = start; i < end; i++) {
|
|
77
|
-
const t = cssTokens[i];
|
|
78
|
-
switch (t[0]) {
|
|
79
|
-
case CssType.Whitespace:
|
|
80
|
-
case CssType.Comment:
|
|
81
|
-
ws = true;
|
|
82
|
-
continue;
|
|
83
|
-
case CssType.Number:
|
|
84
|
-
pushNumeric(tokens, t[1], undefined, t[2], ws);
|
|
85
|
-
ws = false;
|
|
86
|
-
continue;
|
|
87
|
-
case CssType.Dimension:
|
|
88
|
-
pushNumeric(tokens, t[1], t[4].unit, t[2], ws);
|
|
89
|
-
ws = false;
|
|
90
|
-
continue;
|
|
91
|
-
case CssType.Percentage:
|
|
92
|
-
pushNumeric(tokens, t[1], '%', t[2], ws);
|
|
93
|
-
ws = false;
|
|
94
|
-
continue;
|
|
95
|
-
case CssType.Ident:
|
|
96
|
-
tokens.push({ type: 'ident', value: t[4].value, pos: t[2], ws });
|
|
97
|
-
break;
|
|
98
|
-
case CssType.Function:
|
|
99
|
-
tokens.push({ type: 'ident', value: t[4].value, pos: t[2], ws });
|
|
100
|
-
tokens.push({
|
|
101
|
-
type: 'punct',
|
|
102
|
-
value: '(',
|
|
103
|
-
pos: t[2] + t[1].length - 1,
|
|
104
|
-
ws: false,
|
|
105
|
-
});
|
|
106
|
-
break;
|
|
107
|
-
case CssType.OpenParen:
|
|
108
|
-
tokens.push({ type: 'punct', value: '(', pos: t[2], ws });
|
|
109
|
-
break;
|
|
110
|
-
case CssType.CloseParen:
|
|
111
|
-
tokens.push({ type: 'punct', value: ')', pos: t[2], ws });
|
|
112
|
-
break;
|
|
113
|
-
case CssType.Comma:
|
|
114
|
-
tokens.push({ type: 'punct', value: ',', pos: t[2], ws });
|
|
115
|
-
break;
|
|
116
|
-
case CssType.Delim:
|
|
117
|
-
if (!PUNCT_DELIMS.has(t[4].value)) {
|
|
118
|
-
throw new Error(
|
|
119
|
-
`Unexpected character "${t[4].value}" at position ${t[2]}`
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
tokens.push({ type: 'punct', value: t[4].value, pos: t[2], ws });
|
|
123
|
-
break;
|
|
124
|
-
case CssType.EOF:
|
|
125
|
-
continue;
|
|
126
|
-
default:
|
|
127
|
-
throw new Error(
|
|
128
|
-
`Unexpected character "${t[1][0] ?? ''}" at position ${t[2]}`
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
ws = false;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
tokens.push({ type: 'eof', value: '', pos: eofPosition, ws });
|
|
135
|
-
|
|
136
|
-
return tokens;
|
|
7
|
+
return tokenizeCss({ css: input });
|
|
137
8
|
}
|
|
138
9
|
|
|
139
|
-
export { tokenize
|
|
10
|
+
export { tokenize };
|
package/src/reduce.js
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
tokenize as cssTokenize,
|
|
6
6
|
TokenType as CssType,
|
|
7
7
|
} from '@csstools/css-tokenizer';
|
|
8
|
-
import { tokenizeTokens } from './lib/tokenizer.js';
|
|
9
8
|
import { parse } from './lib/parser.js';
|
|
10
9
|
import { simplify } from './lib/simplify.js';
|
|
11
10
|
import {
|
|
@@ -27,6 +26,7 @@ const BLOCK_CLOSE = new Map([
|
|
|
27
26
|
* @typedef {object} ReduceCalcOptions
|
|
28
27
|
* @property {number | false} [precision]
|
|
29
28
|
* @property {boolean} [warnWhenCannotResolve]
|
|
29
|
+
* @property {boolean} [unwrapSingleNegativeNumber] Serialize finite negative results without a `calc()` wrapper. Defaults to `false`.
|
|
30
30
|
* @property {(error: Error, input: string) => void} [onParseError] Invoked when parse/simplify throws.
|
|
31
31
|
* @property {(message: string) => void} [onWarn] Invoked when `warnWhenCannotResolve` is set and an expression cannot be reduced to a single value.
|
|
32
32
|
*/
|
|
@@ -34,9 +34,7 @@ const BLOCK_CLOSE = new Map([
|
|
|
34
34
|
/** @typedef {Required<Omit<ReduceCalcOptions, 'onParseError' | 'onWarn'>> & Pick<ReduceCalcOptions, 'onParseError' | 'onWarn'>} ResolvedReduceCalcOptions */
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Fields threaded
|
|
38
|
-
* `value` is the original full property text, used only for the
|
|
39
|
-
* warnWhenCannotResolve message.
|
|
37
|
+
* Fields threaded through the internal token-range walk.
|
|
40
38
|
*
|
|
41
39
|
* @typedef {object} TransformContext
|
|
42
40
|
* @property {ResolvedReduceCalcOptions} options
|
|
@@ -51,7 +49,6 @@ const BLOCK_CLOSE = new Map([
|
|
|
51
49
|
* @property {number} end
|
|
52
50
|
* @property {import('./lib/node.js').Node} node
|
|
53
51
|
* @property {string} calcName
|
|
54
|
-
* @property {string} matchedName
|
|
55
52
|
*/
|
|
56
53
|
|
|
57
54
|
/**
|
|
@@ -97,15 +94,12 @@ function walkTokens(start, expectedClose, ctx, transform) {
|
|
|
97
94
|
const inputEnd = closed && isCalc ? ctx.tokens[close][2] : end;
|
|
98
95
|
const contents = ctx.value.slice(inputStart, inputEnd);
|
|
99
96
|
try {
|
|
100
|
-
const node = simplify(
|
|
101
|
-
parse(tokenizeTokens(ctx.tokens, end, sliceStart, sliceEnd))
|
|
102
|
-
);
|
|
97
|
+
const node = simplify(parse(ctx.tokens, sliceStart, sliceEnd));
|
|
103
98
|
ctx.replacements.push({
|
|
104
99
|
start: token[2],
|
|
105
100
|
end,
|
|
106
101
|
node,
|
|
107
102
|
calcName: isCalc ? name : 'calc',
|
|
108
|
-
matchedName: name,
|
|
109
103
|
});
|
|
110
104
|
} catch (error) {
|
|
111
105
|
const err = error instanceof Error ? error : new Error('Error');
|
|
@@ -117,6 +111,17 @@ function walkTokens(start, expectedClose, ctx, transform) {
|
|
|
117
111
|
return ctx.tokens.length - 1;
|
|
118
112
|
}
|
|
119
113
|
|
|
114
|
+
/**
|
|
115
|
+
* @param {import('./lib/node.js').Node} node
|
|
116
|
+
* @return {boolean}
|
|
117
|
+
*/
|
|
118
|
+
function isUnresolvedResult(node) {
|
|
119
|
+
if (node.type === 'Sum' || node.type === 'Product') {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
return node.type === 'Call' && isSupportedMathFunction(node.name);
|
|
123
|
+
}
|
|
124
|
+
|
|
120
125
|
/**
|
|
121
126
|
* Simplify every supported CSS math function in a component-value string.
|
|
122
127
|
* Text outside those functions is preserved byte-for-byte.
|
|
@@ -131,7 +136,12 @@ function reduceCalc(value, opts) {
|
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
/** @type {ResolvedReduceCalcOptions} */
|
|
134
|
-
const options = {
|
|
139
|
+
const options = {
|
|
140
|
+
precision: 5,
|
|
141
|
+
warnWhenCannotResolve: false,
|
|
142
|
+
unwrapSingleNegativeNumber: false,
|
|
143
|
+
...opts,
|
|
144
|
+
};
|
|
135
145
|
const tokens = cssTokenize({ css: value });
|
|
136
146
|
/** @type {Replacement[]} */
|
|
137
147
|
const replacements = [];
|
|
@@ -147,11 +157,9 @@ function reduceCalc(value, opts) {
|
|
|
147
157
|
const text = serialize(replacement.node, {
|
|
148
158
|
precision: options.precision,
|
|
149
159
|
calcName: replacement.calcName,
|
|
160
|
+
unwrapSingleNegativeNumber: options.unwrapSingleNegativeNumber,
|
|
150
161
|
});
|
|
151
|
-
if (
|
|
152
|
-
options.warnWhenCannotResolve &&
|
|
153
|
-
text.startsWith(`${replacement.matchedName}(`)
|
|
154
|
-
) {
|
|
162
|
+
if (options.warnWhenCannotResolve && isUnresolvedResult(replacement.node)) {
|
|
155
163
|
options.onWarn?.('Could not reduce expression: ' + value);
|
|
156
164
|
}
|
|
157
165
|
output += value.slice(lastIndex, replacement.start) + text;
|
|
@@ -14,4 +14,11 @@ declare function baseOf(unit: string): BaseType | null;
|
|
|
14
14
|
* @return {number | null}
|
|
15
15
|
*/
|
|
16
16
|
declare function convert(value: number, from: string, to: string): number | null;
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Return a base type only for units present in the static conversion table.
|
|
19
|
+
* Units with context-dependent values (em, rem, vw, etc.) return null.
|
|
20
|
+
* @param {string} unit
|
|
21
|
+
* @return {BaseType | null}
|
|
22
|
+
*/
|
|
23
|
+
declare function staticBaseOf(unit: string): BaseType | null;
|
|
24
|
+
export { baseOf, staticBaseOf, convert };
|
package/types/lib/node.d.ts
CHANGED
|
@@ -6,15 +6,18 @@ export type Dim = {
|
|
|
6
6
|
type: 'Dim';
|
|
7
7
|
value: number;
|
|
8
8
|
unit: string;
|
|
9
|
+
rawUnit?: string;
|
|
9
10
|
};
|
|
10
11
|
export type Ident = {
|
|
11
12
|
type: 'Ident';
|
|
12
13
|
name: string;
|
|
14
|
+
rawName?: string;
|
|
13
15
|
};
|
|
14
16
|
export type Call = {
|
|
15
17
|
type: 'Call';
|
|
16
18
|
name: string;
|
|
17
19
|
args: Node[];
|
|
20
|
+
rawName?: string;
|
|
18
21
|
};
|
|
19
22
|
export type SumTerm = {
|
|
20
23
|
sign: 1 | -1;
|
|
@@ -36,9 +39,9 @@ export type Product = {
|
|
|
36
39
|
export type Node = Num | Dim | Ident | Call | Sum | Product;
|
|
37
40
|
/**
|
|
38
41
|
* @typedef {{type: 'Num', value: number}} Num
|
|
39
|
-
* @typedef {{type: 'Dim', value: number, unit: string}} Dim
|
|
40
|
-
* @typedef {{type: 'Ident', name: string}} Ident
|
|
41
|
-
* @typedef {{type: 'Call', name: string, args: Node[]}} Call
|
|
42
|
+
* @typedef {{type: 'Dim', value: number, unit: string, rawUnit?: string}} Dim
|
|
43
|
+
* @typedef {{type: 'Ident', name: string, rawName?: string}} Ident
|
|
44
|
+
* @typedef {{type: 'Call', name: string, args: Node[], rawName?: string}} Call
|
|
42
45
|
* @typedef {{sign: 1 | -1, node: Node}} SumTerm Sign is always +1 when node is Num or Dim.
|
|
43
46
|
* @typedef {{type: 'Sum', terms: SumTerm[], grouped?: boolean}} Sum
|
|
44
47
|
* @typedef {{exponent: 1 | -1, node: Node}} ProductFactor exponent +1 = numerator, -1 = denominator.
|
|
@@ -53,20 +56,23 @@ declare function num(value: number): Num;
|
|
|
53
56
|
/**
|
|
54
57
|
* @param {number} value
|
|
55
58
|
* @param {string} unit
|
|
59
|
+
* @param {string} [rawUnit]
|
|
56
60
|
* @return {Dim}
|
|
57
61
|
*/
|
|
58
|
-
declare function dim(value: number, unit: string): Dim;
|
|
62
|
+
declare function dim(value: number, unit: string, rawUnit?: string): Dim;
|
|
59
63
|
/**
|
|
60
64
|
* @param {string} name
|
|
65
|
+
* @param {string} [rawName]
|
|
61
66
|
* @return {Ident}
|
|
62
67
|
*/
|
|
63
|
-
declare function ident(name: string): Ident;
|
|
68
|
+
declare function ident(name: string, rawName?: string): Ident;
|
|
64
69
|
/**
|
|
65
70
|
* @param {string} name
|
|
66
71
|
* @param {Node[]} args
|
|
72
|
+
* @param {string} [rawName]
|
|
67
73
|
* @return {Call}
|
|
68
74
|
*/
|
|
69
|
-
declare function call(name: string, args: Node[]): Call;
|
|
75
|
+
declare function call(name: string, args: Node[], rawName?: string): Call;
|
|
70
76
|
/**
|
|
71
77
|
* @param {SumTerm[]} rawTerms
|
|
72
78
|
* @return {Node}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type Node = import('./node.js').Node;
|
|
2
|
+
export type Component = string | Node | Component[];
|
|
3
|
+
/** @param {Extract<Node, {type: 'Call'}>} node @param {Component[]} tree */
|
|
4
|
+
declare function setComponents(node: Extract<Node, {
|
|
5
|
+
type: 'Call';
|
|
6
|
+
}>, tree: Component[]): import("./node.js").Call;
|
|
7
|
+
/** @param {Extract<Node, {type: 'Call'}>} node */
|
|
8
|
+
declare function getComponents(node: Extract<Node, {
|
|
9
|
+
type: 'Call';
|
|
10
|
+
}>): Component[] | undefined;
|
|
11
|
+
/** @param {Component[]} tree @param {(node: Node) => Node} simplify @return {Component[]} */
|
|
12
|
+
declare function simplifyComponents(tree: Component[], simplify: (node: Node) => Node): Component[];
|
|
13
|
+
/** @param {Component[]} tree @param {(node: Node) => string} serialize @return {string} */
|
|
14
|
+
declare function serializeComponents(tree: Component[], serialize: (node: Node) => string): string;
|
|
15
|
+
export { getComponents, setComponents, simplifyComponents, serializeComponents, };
|