postcss-calc 11.0.3 → 11.1.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 +33 -1
- package/package.json +10 -6
- package/src/index.js +21 -159
- package/src/lib/node.js +5 -5
- package/src/lib/parser.js +40 -33
- package/src/lib/serialize.js +37 -22
- package/src/lib/simplify/bucket.js +1 -1
- package/src/lib/simplify/call.js +30 -2
- package/src/lib/simplify/clamp.js +25 -3
- package/src/lib/simplify/fold.js +14 -1
- package/src/lib/simplify/hypot.js +3 -3
- package/src/lib/simplify/min-max.js +3 -3
- package/src/lib/simplify/mod-rem.js +3 -3
- package/src/lib/simplify/round.js +4 -4
- package/src/lib/tokenizer.js +42 -30
- package/src/reduce.js +165 -0
- package/types/index.d.ts +0 -15
- package/types/lib/parser.d.ts +16 -0
- package/types/lib/simplify/call.d.ts +11 -1
- package/types/lib/simplify/fold.d.ts +14 -1
- package/types/lib/tokenizer.d.ts +3 -1
- package/types/reduce.d.ts +38 -0
- package/types/lib/type.d.ts +0 -23
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ statement is left as is, to fallback to the [W3C calc() implementation].
|
|
|
13
13
|
npm install postcss-calc
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## PostCSS usage
|
|
17
17
|
|
|
18
18
|
```js
|
|
19
19
|
// dependencies
|
|
@@ -52,6 +52,38 @@ h1 {
|
|
|
52
52
|
|
|
53
53
|
Checkout [tests] for more examples.
|
|
54
54
|
|
|
55
|
+
## Use the reducer without PostCSS
|
|
56
|
+
|
|
57
|
+
For a single CSS component-value string, import the dedicated reducer entry
|
|
58
|
+
point. It reduces `calc()` and the supported CSS math functions it finds while
|
|
59
|
+
leaving all other text untouched.
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
import reduceCalc from 'postcss-calc/reduce';
|
|
63
|
+
|
|
64
|
+
reduceCalc('calc(1in + 10px)');
|
|
65
|
+
// => '1.10417in'
|
|
66
|
+
|
|
67
|
+
reduceCalc('min(50px, calc(2 * 40px))');
|
|
68
|
+
// => '50px'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
It accepts `precision`, `warnWhenCannotResolve`, `onParseError`, and `onWarn`:
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
const result = reduceCalc('calc(100% + var(--gap))', {
|
|
75
|
+
precision: false,
|
|
76
|
+
warnWhenCannotResolve: true,
|
|
77
|
+
onWarn: console.warn,
|
|
78
|
+
onParseError(error, input) {
|
|
79
|
+
console.error(`Invalid calculation: ${input}`, error);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Unlike the PostCSS plugin, the standalone reducer does not show warnings
|
|
85
|
+
by default; provide `onParseError` and/or `onWarn` if you want diagnostics.
|
|
86
|
+
|
|
55
87
|
### Options
|
|
56
88
|
|
|
57
89
|
#### `precision` (default: `5`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postcss-calc",
|
|
3
|
-
"version": "11.0
|
|
3
|
+
"version": "11.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "PostCSS plugin to reduce calc()",
|
|
6
6
|
"keywords": [
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
".": {
|
|
20
20
|
"types": "./types/index.d.ts",
|
|
21
21
|
"default": "./src/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./reduce": {
|
|
24
|
+
"types": "./types/reduce.d.ts",
|
|
25
|
+
"default": "./src/reduce.js"
|
|
22
26
|
}
|
|
23
27
|
},
|
|
24
28
|
"files": [
|
|
@@ -40,18 +44,18 @@
|
|
|
40
44
|
"devDependencies": {
|
|
41
45
|
"@csstools/css-calc": "^3.3.0",
|
|
42
46
|
"@rmenke/css-tokenizer-tests": "^1.2.0",
|
|
43
|
-
"@types/node": "^26.4.
|
|
47
|
+
"@types/node": "^26.4.1",
|
|
44
48
|
"fast-check": "^4.9.0",
|
|
45
|
-
"oxfmt": "^0.
|
|
46
|
-
"oxlint": "^1.
|
|
47
|
-
"postcss": "^8.5.
|
|
49
|
+
"oxfmt": "^0.66.0",
|
|
50
|
+
"oxlint": "^1.81.0",
|
|
51
|
+
"postcss": "^8.5.28",
|
|
48
52
|
"typescript": "~7.0.2"
|
|
49
53
|
},
|
|
50
54
|
"dependencies": {
|
|
51
55
|
"@csstools/css-tokenizer": "^4.0.0"
|
|
52
56
|
},
|
|
53
57
|
"peerDependencies": {
|
|
54
|
-
"postcss": "^8.5.
|
|
58
|
+
"postcss": "^8.5.28"
|
|
55
59
|
},
|
|
56
60
|
"scripts": {
|
|
57
61
|
"lint": "oxlint . && tsc && oxfmt --check",
|
package/src/index.js
CHANGED
|
@@ -1,23 +1,5 @@
|
|
|
1
|
-
// PostCSS adapter
|
|
2
|
-
|
|
3
|
-
// → serialize, and writes the result back.
|
|
4
|
-
import {
|
|
5
|
-
tokenize as cssTokenize,
|
|
6
|
-
TokenType as CssType,
|
|
7
|
-
} from '@csstools/css-tokenizer';
|
|
8
|
-
import { tokenizeTokens } from './lib/tokenizer.js';
|
|
9
|
-
import { parse } from './lib/parser.js';
|
|
10
|
-
import { simplify } from './lib/simplify.js';
|
|
11
|
-
import { isSupportedMathFunction } from './lib/simplify/call.js';
|
|
12
|
-
import { serialize } from './lib/serialize.js';
|
|
13
|
-
|
|
14
|
-
const MATCH_CALC = /^(?:-(?:moz|webkit)-)?calc$/i;
|
|
15
|
-
|
|
16
|
-
const BLOCK_CLOSE = new Map([
|
|
17
|
-
[CssType.OpenParen, CssType.CloseParen],
|
|
18
|
-
[CssType.OpenSquare, CssType.CloseSquare],
|
|
19
|
-
[CssType.OpenCurly, CssType.CloseCurly],
|
|
20
|
-
]);
|
|
1
|
+
// PostCSS adapter over the standalone component-value reducer.
|
|
2
|
+
import reduceCalc, { hasPotentialMathFunction } from './reduce.js';
|
|
21
3
|
|
|
22
4
|
/**
|
|
23
5
|
* @typedef {object} PostCssCalcOptions
|
|
@@ -31,144 +13,7 @@ const BLOCK_CLOSE = new Map([
|
|
|
31
13
|
/** @typedef {Required<Omit<PostCssCalcOptions, 'onParseError'>> & Pick<PostCssCalcOptions, 'onParseError'>} ResolvedOptions */
|
|
32
14
|
|
|
33
15
|
/**
|
|
34
|
-
*
|
|
35
|
-
* `value` is the original full property text, used only for the
|
|
36
|
-
* warnWhenCannotResolve message.
|
|
37
|
-
*
|
|
38
|
-
* @typedef {object} TransformContext
|
|
39
|
-
* @property {ResolvedOptions} options
|
|
40
|
-
* @property {import('postcss').Result} result
|
|
41
|
-
* @property {import('postcss').ChildNode} item
|
|
42
|
-
* @property {string} value
|
|
43
|
-
* @property {import('@csstools/css-tokenizer').CSSToken[]} tokens
|
|
44
|
-
* @property {Replacement[]} replacements
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @typedef {object} Replacement
|
|
49
|
-
* @property {number} start
|
|
50
|
-
* @property {number} end
|
|
51
|
-
* @property {import('./lib/node.js').Node} node
|
|
52
|
-
* @property {string} calcName
|
|
53
|
-
* @property {string} matchedName
|
|
54
|
-
*/
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Walk one component-value level. Unsupported functions and simple blocks are
|
|
58
|
-
* traversed, while a supported function is treated as one opaque calculation
|
|
59
|
-
* even when parsing it fails. A missing closer consumes through EOF, matching
|
|
60
|
-
* CSS component-value parsing's error recovery.
|
|
61
|
-
*
|
|
62
|
-
* @param {number} start
|
|
63
|
-
* @param {import('@csstools/css-tokenizer').TokenType | undefined} expectedClose
|
|
64
|
-
* @param {TransformContext} ctx
|
|
65
|
-
* @param {boolean} transform
|
|
66
|
-
* @return {number} Index of the matching closer, or the EOF token.
|
|
67
|
-
*/
|
|
68
|
-
function walkTokens(start, expectedClose, ctx, transform) {
|
|
69
|
-
for (let i = start; i < ctx.tokens.length; i++) {
|
|
70
|
-
const token = ctx.tokens[i];
|
|
71
|
-
if (token[0] === CssType.EOF || token[0] === expectedClose) {
|
|
72
|
-
return i;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const blockClose = BLOCK_CLOSE.get(token[0]);
|
|
76
|
-
if (blockClose) {
|
|
77
|
-
i = walkTokens(i + 1, blockClose, ctx, transform);
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (token[0] !== CssType.Function) {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const name = token[4].value;
|
|
86
|
-
const isCalc = MATCH_CALC.test(name);
|
|
87
|
-
const isMath = !isCalc && isSupportedMathFunction(name);
|
|
88
|
-
if (!transform || (!isCalc && !isMath)) {
|
|
89
|
-
i = walkTokens(i + 1, CssType.CloseParen, ctx, transform);
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Locate the complete outer function without transforming its children.
|
|
94
|
-
const close = walkTokens(i + 1, CssType.CloseParen, ctx, false);
|
|
95
|
-
const closed = ctx.tokens[close][0] === CssType.CloseParen;
|
|
96
|
-
const end = closed ? ctx.tokens[close][3] + 1 : ctx.value.length;
|
|
97
|
-
const sliceStart = isCalc ? i + 1 : i;
|
|
98
|
-
const sliceEnd = closed ? close + (isCalc ? 0 : 1) : close;
|
|
99
|
-
const inputStart = isCalc ? token[3] + 1 : token[2];
|
|
100
|
-
const inputEnd = closed && isCalc ? ctx.tokens[close][2] : end;
|
|
101
|
-
const contents = ctx.value.slice(inputStart, inputEnd);
|
|
102
|
-
try {
|
|
103
|
-
const node = simplify(
|
|
104
|
-
parse(tokenizeTokens(ctx.tokens.slice(sliceStart, sliceEnd), end))
|
|
105
|
-
);
|
|
106
|
-
ctx.replacements.push({
|
|
107
|
-
start: token[2],
|
|
108
|
-
end,
|
|
109
|
-
node,
|
|
110
|
-
calcName: isCalc ? name : 'calc',
|
|
111
|
-
matchedName: name,
|
|
112
|
-
});
|
|
113
|
-
} catch (error) {
|
|
114
|
-
const err = error instanceof Error ? error : new Error('Error');
|
|
115
|
-
if (ctx.options.onParseError) {
|
|
116
|
-
ctx.options.onParseError(err, contents);
|
|
117
|
-
} else {
|
|
118
|
-
ctx.result.warn(err.message, { node: ctx.item });
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
i = close;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return ctx.tokens.length - 1;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* @param {string} value
|
|
129
|
-
* @param {ResolvedOptions} options
|
|
130
|
-
* @param {import('postcss').Result} result
|
|
131
|
-
* @param {import('postcss').ChildNode} item
|
|
132
|
-
* @return {string}
|
|
133
|
-
*/
|
|
134
|
-
function transformValue(value, options, result, item) {
|
|
135
|
-
const tokens = cssTokenize({ css: value });
|
|
136
|
-
/** @type {Replacement[]} */
|
|
137
|
-
const replacements = [];
|
|
138
|
-
const ctx = { options, result, item, value, tokens, replacements };
|
|
139
|
-
walkTokens(0, undefined, ctx, true);
|
|
140
|
-
|
|
141
|
-
/** @type {(Replacement & {text: string})[]} */
|
|
142
|
-
const serialized = replacements.map((replacement) => {
|
|
143
|
-
const text = serialize(replacement.node, {
|
|
144
|
-
precision: options.precision,
|
|
145
|
-
calcName: replacement.calcName,
|
|
146
|
-
});
|
|
147
|
-
if (
|
|
148
|
-
options.warnWhenCannotResolve &&
|
|
149
|
-
text.startsWith(`${replacement.matchedName}(`)
|
|
150
|
-
) {
|
|
151
|
-
result.warn('Could not reduce expression: ' + value, {
|
|
152
|
-
plugin: 'postcss-calc',
|
|
153
|
-
node: item,
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
return { ...replacement, text };
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
let output = value;
|
|
160
|
-
for (let i = serialized.length - 1; i >= 0; i--) {
|
|
161
|
-
const replacement = serialized[i];
|
|
162
|
-
output =
|
|
163
|
-
output.slice(0, replacement.start) +
|
|
164
|
-
replacement.text +
|
|
165
|
-
output.slice(replacement.end);
|
|
166
|
-
}
|
|
167
|
-
return output;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Runs `transformValue` over one text property of a decl/atrule/rule node
|
|
16
|
+
* Runs `reduceCalc` over one text property of a decl/atrule/rule node
|
|
172
17
|
* and updates it in place.
|
|
173
18
|
* `setProp` closes over the property name and the concrete node type at
|
|
174
19
|
* each call site, since `Declaration`/`AtRule`/`Rule` don't share a typed
|
|
@@ -182,7 +27,24 @@ function transformValue(value, options, result, item) {
|
|
|
182
27
|
* @return {void}
|
|
183
28
|
*/
|
|
184
29
|
function applyTransform(node, current, setProp, options, result) {
|
|
185
|
-
|
|
30
|
+
if (!hasPotentialMathFunction(current)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const transformed = reduceCalc(current, {
|
|
34
|
+
precision: options.precision,
|
|
35
|
+
warnWhenCannotResolve: options.warnWhenCannotResolve,
|
|
36
|
+
onParseError:
|
|
37
|
+
options.onParseError ??
|
|
38
|
+
((error) => {
|
|
39
|
+
result.warn(error.message, { node });
|
|
40
|
+
}),
|
|
41
|
+
onWarn: (message) => {
|
|
42
|
+
result.warn(message, { plugin: 'postcss-calc', node });
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
if (transformed !== current) {
|
|
46
|
+
setProp(node, transformed);
|
|
47
|
+
}
|
|
186
48
|
}
|
|
187
49
|
|
|
188
50
|
/**
|
package/src/lib/node.js
CHANGED
|
@@ -71,7 +71,7 @@ function mkSum(rawTerms) {
|
|
|
71
71
|
pushSumTerm(flat, t);
|
|
72
72
|
}
|
|
73
73
|
if (flat.length === 0) {
|
|
74
|
-
return
|
|
74
|
+
return num(0);
|
|
75
75
|
}
|
|
76
76
|
if (flat.length === 1 && flat[0].sign === 1) {
|
|
77
77
|
return flat[0].node;
|
|
@@ -101,10 +101,10 @@ function pushSumTerm(out, term) {
|
|
|
101
101
|
// canonical-form rule downstream code relies on.
|
|
102
102
|
if (sign === -1) {
|
|
103
103
|
if (node.type === 'Num') {
|
|
104
|
-
node =
|
|
104
|
+
node = num(-node.value);
|
|
105
105
|
sign = 1;
|
|
106
106
|
} else if (node.type === 'Dim') {
|
|
107
|
-
node =
|
|
107
|
+
node = dim(-node.value, node.unit);
|
|
108
108
|
sign = 1;
|
|
109
109
|
}
|
|
110
110
|
}
|
|
@@ -128,7 +128,7 @@ function mkProduct(rawFactors) {
|
|
|
128
128
|
pushProductFactor(flat, f);
|
|
129
129
|
}
|
|
130
130
|
if (flat.length === 0) {
|
|
131
|
-
return
|
|
131
|
+
return num(1);
|
|
132
132
|
}
|
|
133
133
|
if (flat.length === 1 && flat[0].exponent === 1) {
|
|
134
134
|
return flat[0].node;
|
|
@@ -184,7 +184,7 @@ function negate(node) {
|
|
|
184
184
|
}
|
|
185
185
|
// Opaque (Ident, Call, Product): wrap as a single negative-sign term —
|
|
186
186
|
// the only case where sign=-1 remains on a SumTerm.
|
|
187
|
-
return
|
|
187
|
+
return mkSum([{ sign: -1, node }]);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
export { num, dim, ident, call, mkSum, mkProduct, negate };
|
package/src/lib/parser.js
CHANGED
|
@@ -10,15 +10,6 @@ import { mkSum, mkProduct, negate, num, dim, ident, call } from './node.js';
|
|
|
10
10
|
* @typedef {(p: Parser, token: Token) => Node} PrefixParselet
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* @param {Token} t
|
|
15
|
-
* @param {string} value
|
|
16
|
-
* @return {boolean}
|
|
17
|
-
*/
|
|
18
|
-
function isPunct(t, value) {
|
|
19
|
-
return t.type === 'punct' && t.value === value;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
13
|
/**
|
|
23
14
|
* §10.9 — case-insensitive except for NaN.
|
|
24
15
|
* @param {string} name
|
|
@@ -81,6 +72,39 @@ class Parser {
|
|
|
81
72
|
return t;
|
|
82
73
|
}
|
|
83
74
|
|
|
75
|
+
/**
|
|
76
|
+
* @param {string} value
|
|
77
|
+
* @param {string} [value2]
|
|
78
|
+
* @return {boolean}
|
|
79
|
+
*/
|
|
80
|
+
isPunct(value, value2) {
|
|
81
|
+
const t = this.peek();
|
|
82
|
+
return (
|
|
83
|
+
t.type === 'punct' &&
|
|
84
|
+
(t.value === value || (value2 !== undefined && t.value === value2))
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {string} value
|
|
90
|
+
* @return {boolean}
|
|
91
|
+
*/
|
|
92
|
+
matchPunct(value) {
|
|
93
|
+
if (this.isPunct(value)) {
|
|
94
|
+
this.next();
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {string} value
|
|
102
|
+
* @return {Token}
|
|
103
|
+
*/
|
|
104
|
+
expectPunct(value) {
|
|
105
|
+
return this.expect('punct', value);
|
|
106
|
+
}
|
|
107
|
+
|
|
84
108
|
/**
|
|
85
109
|
* @param {number} [minBp]
|
|
86
110
|
* @return {Node}
|
|
@@ -111,14 +135,7 @@ class Parser {
|
|
|
111
135
|
sign: /** @type {1 | -1} */ (token.value === '+' ? 1 : -1),
|
|
112
136
|
node: this.parseExpr(ADD_BP + 1),
|
|
113
137
|
});
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
next.type !== 'punct' ||
|
|
117
|
-
(next.value !== '+' && next.value !== '-')
|
|
118
|
-
) {
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
} while (ADD_BP >= minBp);
|
|
138
|
+
} while (this.isPunct('+', '-'));
|
|
122
139
|
left = mkSum(terms);
|
|
123
140
|
continue;
|
|
124
141
|
}
|
|
@@ -132,14 +149,7 @@ class Parser {
|
|
|
132
149
|
exponent: /** @type {1 | -1} */ (token.value === '*' ? 1 : -1),
|
|
133
150
|
node: this.parseExpr(MUL_BP + 1),
|
|
134
151
|
});
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
next.type !== 'punct' ||
|
|
138
|
-
(next.value !== '*' && next.value !== '/')
|
|
139
|
-
) {
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
} while (MUL_BP >= minBp);
|
|
152
|
+
} while (this.isPunct('*', '/'));
|
|
143
153
|
left = mkProduct(factors);
|
|
144
154
|
continue;
|
|
145
155
|
}
|
|
@@ -255,22 +265,19 @@ const PREFIX = {
|
|
|
255
265
|
),
|
|
256
266
|
|
|
257
267
|
ident: (p, t) => {
|
|
258
|
-
|
|
259
|
-
if (nxt.type === 'punct' && nxt.value === '(') {
|
|
260
|
-
p.next();
|
|
268
|
+
if (p.matchPunct('(')) {
|
|
261
269
|
if (OPAQUE_ARG_FUNCTIONS.has(t.value.toLowerCase())) {
|
|
262
270
|
return parseOpaqueCall(p, t.value);
|
|
263
271
|
}
|
|
264
272
|
/** @type {Node[]} */
|
|
265
273
|
const args = [];
|
|
266
|
-
if (!
|
|
274
|
+
if (!p.isPunct(')')) {
|
|
267
275
|
args.push(p.parseExpr(0));
|
|
268
|
-
while (
|
|
269
|
-
p.next();
|
|
276
|
+
while (p.matchPunct(',')) {
|
|
270
277
|
args.push(p.parseExpr(0));
|
|
271
278
|
}
|
|
272
279
|
}
|
|
273
|
-
p.
|
|
280
|
+
p.expectPunct(')');
|
|
274
281
|
return call(t.value, args);
|
|
275
282
|
}
|
|
276
283
|
const kw = foldCalcKeyword(t.value);
|
|
@@ -282,7 +289,7 @@ const PREFIX = {
|
|
|
282
289
|
|
|
283
290
|
'(': (p) => {
|
|
284
291
|
const e = p.parseExpr(0);
|
|
285
|
-
p.
|
|
292
|
+
p.expectPunct(')');
|
|
286
293
|
return e.type === 'Sum' ? { ...e, grouped: true } : e;
|
|
287
294
|
},
|
|
288
295
|
|
package/src/lib/serialize.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// arithmetic operator. A Sum inside a Product is the only place parens
|
|
4
4
|
// are ever required on valid canonical input.
|
|
5
5
|
|
|
6
|
+
import { num, dim } from './node.js';
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* @typedef {import('./node.js').Node} Node
|
|
8
10
|
* @typedef {import('./node.js').Sum} Sum
|
|
@@ -105,14 +107,11 @@ function serialize(node, opts = {}) {
|
|
|
105
107
|
node.terms.length > 1 &&
|
|
106
108
|
displaySign(node.terms[0]).sign === -1
|
|
107
109
|
) {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
})),
|
|
114
|
-
});
|
|
115
|
-
return `${calcName}(-(${serializeExpr(body, prec)}))`;
|
|
110
|
+
const invertedTerms = node.terms.map((t) => ({
|
|
111
|
+
sign: /** @type {1 | -1} */ (-t.sign),
|
|
112
|
+
node: t.node,
|
|
113
|
+
}));
|
|
114
|
+
return `${calcName}(-(${serializeSumTerms(invertedTerms, prec)}))`;
|
|
116
115
|
}
|
|
117
116
|
|
|
118
117
|
if (
|
|
@@ -182,27 +181,27 @@ function displaySign(term) {
|
|
|
182
181
|
if (node.type === 'Num' && Number.isFinite(node.value) && node.value < 0) {
|
|
183
182
|
return {
|
|
184
183
|
sign: /** @type {1 | -1} */ (-sign),
|
|
185
|
-
magnitude:
|
|
184
|
+
magnitude: num(-node.value),
|
|
186
185
|
};
|
|
187
186
|
}
|
|
188
187
|
if (node.type === 'Dim' && Number.isFinite(node.value) && node.value < 0) {
|
|
189
188
|
return {
|
|
190
189
|
sign: /** @type {1 | -1} */ (-sign),
|
|
191
|
-
magnitude:
|
|
190
|
+
magnitude: dim(-node.value, node.unit),
|
|
192
191
|
};
|
|
193
192
|
}
|
|
194
193
|
return { sign, magnitude: node };
|
|
195
194
|
}
|
|
196
195
|
|
|
197
196
|
/**
|
|
198
|
-
* @param {
|
|
197
|
+
* @param {import('./node.js').SumTerm[]} terms
|
|
199
198
|
* @param {number | false} prec
|
|
200
199
|
* @return {string}
|
|
201
200
|
*/
|
|
202
|
-
function
|
|
201
|
+
function serializeSumTerms(terms, prec) {
|
|
203
202
|
let out = '';
|
|
204
|
-
for (
|
|
205
|
-
const { sign, magnitude } = displaySign(
|
|
203
|
+
for (let i = 0; i < terms.length; i++) {
|
|
204
|
+
const { sign, magnitude } = displaySign(terms[i]);
|
|
206
205
|
if (i === 0) {
|
|
207
206
|
if (magnitude.type === 'Sum' && magnitude.grouped) {
|
|
208
207
|
const body = `(${serializeExpr(magnitude, prec)})`;
|
|
@@ -225,6 +224,15 @@ function serializeSum(sum, prec) {
|
|
|
225
224
|
return out;
|
|
226
225
|
}
|
|
227
226
|
|
|
227
|
+
/**
|
|
228
|
+
* @param {Sum} sum
|
|
229
|
+
* @param {number | false} prec
|
|
230
|
+
* @return {string}
|
|
231
|
+
*/
|
|
232
|
+
function serializeSum(sum, prec) {
|
|
233
|
+
return serializeSumTerms(sum.terms, prec);
|
|
234
|
+
}
|
|
235
|
+
|
|
228
236
|
/**
|
|
229
237
|
* Fold a leading negation into a finite leading Num if there is one
|
|
230
238
|
* (`-(0.5 * x)` → `-0.5 * x`); else use `-(…)` for Sum/Product or `-x`.
|
|
@@ -249,11 +257,8 @@ function serializeLeadingNeg(node, prec) {
|
|
|
249
257
|
const negatedFactors =
|
|
250
258
|
negatedValue === 1
|
|
251
259
|
? rest
|
|
252
|
-
: [
|
|
253
|
-
|
|
254
|
-
...rest,
|
|
255
|
-
];
|
|
256
|
-
return serializeProduct({ type: 'Product', factors: negatedFactors }, prec);
|
|
260
|
+
: [{ exponent: 1, node: num(negatedValue) }, ...rest];
|
|
261
|
+
return serializeFactors(negatedFactors, prec);
|
|
257
262
|
}
|
|
258
263
|
const body = serializeExpr(node, prec);
|
|
259
264
|
return node.type === 'Sum' || node.type === 'Product'
|
|
@@ -262,13 +267,14 @@ function serializeLeadingNeg(node, prec) {
|
|
|
262
267
|
}
|
|
263
268
|
|
|
264
269
|
/**
|
|
265
|
-
* @param {
|
|
270
|
+
* @param {ProductFactor[]} factors
|
|
266
271
|
* @param {number | false} prec
|
|
267
272
|
* @return {string}
|
|
268
273
|
*/
|
|
269
|
-
function
|
|
274
|
+
function serializeFactors(factors, prec) {
|
|
270
275
|
let out = '';
|
|
271
|
-
for (
|
|
276
|
+
for (let i = 0; i < factors.length; i++) {
|
|
277
|
+
const f = factors[i];
|
|
272
278
|
let body = serializeExpr(f.node, prec);
|
|
273
279
|
// A Sum factor needs parens: `a * (b + c)`. Flat canonical form means
|
|
274
280
|
// this is the only place parens are required.
|
|
@@ -285,4 +291,13 @@ function serializeProduct(product, prec) {
|
|
|
285
291
|
return out;
|
|
286
292
|
}
|
|
287
293
|
|
|
294
|
+
/**
|
|
295
|
+
* @param {Product} product
|
|
296
|
+
* @param {number | false} prec
|
|
297
|
+
* @return {string}
|
|
298
|
+
*/
|
|
299
|
+
function serializeProduct(product, prec) {
|
|
300
|
+
return serializeFactors(product.factors, prec);
|
|
301
|
+
}
|
|
302
|
+
|
|
288
303
|
export { serialize };
|
|
@@ -21,7 +21,7 @@ import { convert } from '../convertUnits.js';
|
|
|
21
21
|
* @return {UnitBucket[]}
|
|
22
22
|
*/
|
|
23
23
|
function mergeConvertibleBuckets(buckets) {
|
|
24
|
-
const ordered =
|
|
24
|
+
const ordered = buckets.sort((a, b) => a.order - b.order);
|
|
25
25
|
/** @type {Set<string>} */ const merged = new Set();
|
|
26
26
|
/** @type {UnitBucket[]} */ const out = [];
|
|
27
27
|
for (const b of ordered) {
|
package/src/lib/simplify/call.js
CHANGED
|
@@ -26,7 +26,7 @@ import { call } from '../node.js';
|
|
|
26
26
|
// Bare CSS math functions with implemented simplification semantics, keyed
|
|
27
27
|
// by lowercase name. calc() and its vendor-prefixed forms are handled
|
|
28
28
|
// separately as wrappers in simplifyCall. This map is the single source of
|
|
29
|
-
// truth for
|
|
29
|
+
// truth for dispatch, `isSupportedMathFunction`, and `QUICK_MATH_TEST`.
|
|
30
30
|
/** @type {Map<string, MathSimplifier>} */
|
|
31
31
|
const MATH_SIMPLIFIERS = new Map([
|
|
32
32
|
['min', simplifyMinMax],
|
|
@@ -51,6 +51,29 @@ const MATH_SIMPLIFIERS = new Map([
|
|
|
51
51
|
['exp', (_name, args) => simplifyExp(args)],
|
|
52
52
|
]);
|
|
53
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
|
+
|
|
54
77
|
/**
|
|
55
78
|
* Whether a bare CSS math function has an implemented simplifier.
|
|
56
79
|
*
|
|
@@ -91,4 +114,9 @@ function simplifyCall(node, simplify) {
|
|
|
91
114
|
return call(node.name, args);
|
|
92
115
|
}
|
|
93
116
|
|
|
94
|
-
export {
|
|
117
|
+
export {
|
|
118
|
+
isSupportedMathFunction,
|
|
119
|
+
simplifyCall,
|
|
120
|
+
hasPotentialMathFunction,
|
|
121
|
+
QUICK_MATH_TEST,
|
|
122
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { foldConstArgs } from './fold.js';
|
|
1
|
+
import { call } from '../node.js';
|
|
2
|
+
import { foldConstArgs, foldResult } from './fold.js';
|
|
3
|
+
import { simplifyMinMax } from './min-max.js';
|
|
3
4
|
|
|
4
5
|
/** @typedef {import('../node.js').Node} Node */
|
|
5
6
|
|
|
@@ -9,16 +10,37 @@ import { foldConstArgs } from './fold.js';
|
|
|
9
10
|
*/
|
|
10
11
|
function simplifyClamp(args) {
|
|
11
12
|
if (args.length === 3) {
|
|
13
|
+
const minNone = isNone(args[0]);
|
|
14
|
+
const maxNone = isNone(args[2]);
|
|
15
|
+
|
|
16
|
+
if (minNone && maxNone) {
|
|
17
|
+
return args[1];
|
|
18
|
+
}
|
|
19
|
+
if (minNone) {
|
|
20
|
+
return simplifyMinMax('min', [args[1], args[2]]);
|
|
21
|
+
}
|
|
22
|
+
if (maxNone) {
|
|
23
|
+
return simplifyMinMax('max', [args[0], args[1]]);
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
const fold = foldConstArgs(args);
|
|
13
27
|
if (fold !== null) {
|
|
14
28
|
const [lo, v, hi] = /** @type {[number, number, number]} */ (fold.values);
|
|
15
29
|
// Spec §10.8: clamp(MIN, VAL, MAX) = max(MIN, min(VAL, MAX)). The
|
|
16
30
|
// outer max(MIN, …) means MIN wins when MIN > MAX — not MAX.
|
|
17
31
|
const clamped = Math.max(lo, Math.min(v, hi));
|
|
18
|
-
return fold
|
|
32
|
+
return foldResult(fold, clamped);
|
|
19
33
|
}
|
|
20
34
|
}
|
|
21
35
|
return call('clamp', args);
|
|
22
36
|
}
|
|
23
37
|
|
|
38
|
+
/**
|
|
39
|
+
* @param {Node} node
|
|
40
|
+
* @return {boolean}
|
|
41
|
+
*/
|
|
42
|
+
function isNone(node) {
|
|
43
|
+
return node.type === 'Ident' && node.name.toLowerCase() === 'none';
|
|
44
|
+
}
|
|
45
|
+
|
|
24
46
|
export { simplifyClamp };
|
package/src/lib/simplify/fold.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
|
+
import { num, dim } from '../node.js';
|
|
1
2
|
import { baseOf, convert } from '../convertUnits.js';
|
|
2
3
|
|
|
3
4
|
/** @typedef {import('../node.js').Node} Node */
|
|
5
|
+
/** @typedef {import('../node.js').Num} Num */
|
|
6
|
+
/** @typedef {import('../node.js').Dim} Dim */
|
|
4
7
|
/** @typedef {import('../convertUnits.js').BaseType} BaseType */
|
|
5
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Construct a Num or Dim node from a folded result.
|
|
11
|
+
* @param {{ unit: string }} fold
|
|
12
|
+
* @param {number} value
|
|
13
|
+
* @return {Num | Dim}
|
|
14
|
+
*/
|
|
15
|
+
function foldResult(fold, value) {
|
|
16
|
+
return fold.unit === '' ? num(value) : dim(value, fold.unit);
|
|
17
|
+
}
|
|
18
|
+
|
|
6
19
|
/**
|
|
7
20
|
* @param {Node[]} args
|
|
8
21
|
* @return {{ values: number[], unit: string } | null}
|
|
@@ -62,4 +75,4 @@ function foldDimArgs(args, unit, base) {
|
|
|
62
75
|
return { values, unit };
|
|
63
76
|
}
|
|
64
77
|
|
|
65
|
-
export { foldConstArgs };
|
|
78
|
+
export { foldConstArgs, foldResult };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// §10.5 — hypot. Empty args return null from foldConstArgs naturally.
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { foldConstArgs } from './fold.js';
|
|
3
|
+
import { call } from '../node.js';
|
|
4
|
+
import { foldConstArgs, foldResult } from './fold.js';
|
|
5
5
|
|
|
6
6
|
/** @typedef {import('../node.js').Node} Node */
|
|
7
7
|
|
|
@@ -16,7 +16,7 @@ function simplifyHypot(args) {
|
|
|
16
16
|
}
|
|
17
17
|
const sumSq = fold.values.reduce((acc, v) => acc + v * v, 0);
|
|
18
18
|
const result = Math.sqrt(sumSq);
|
|
19
|
-
return fold
|
|
19
|
+
return foldResult(fold, result);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export { simplifyHypot };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { foldConstArgs } from './fold.js';
|
|
1
|
+
import { call } from '../node.js';
|
|
2
|
+
import { foldConstArgs, foldResult } 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 fold
|
|
16
|
+
return foldResult(fold, value);
|
|
17
17
|
}
|
|
18
18
|
return call(name, args);
|
|
19
19
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { num,
|
|
2
|
-
import { foldConstArgs } from './fold.js';
|
|
1
|
+
import { num, call } from '../node.js';
|
|
2
|
+
import { foldConstArgs, foldResult } from './fold.js';
|
|
3
3
|
|
|
4
4
|
/** @typedef {import('../node.js').Node} Node */
|
|
5
5
|
|
|
@@ -23,7 +23,7 @@ function simplifyModRem(name, args) {
|
|
|
23
23
|
if (Number.isNaN(result)) {
|
|
24
24
|
return num(Number.NaN);
|
|
25
25
|
}
|
|
26
|
-
return fold
|
|
26
|
+
return foldResult(fold, result);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { num,
|
|
2
|
-
import { foldConstArgs } from './fold.js';
|
|
1
|
+
import { num, ident, call } from '../node.js';
|
|
2
|
+
import { foldConstArgs, foldResult } from './fold.js';
|
|
3
3
|
|
|
4
4
|
/** @typedef {import('../node.js').Node} Node */
|
|
5
5
|
|
|
@@ -58,14 +58,14 @@ function simplifyRound(args) {
|
|
|
58
58
|
} else {
|
|
59
59
|
result = a < 0 || Object.is(a, -0) ? -0 : 0;
|
|
60
60
|
}
|
|
61
|
-
return fold
|
|
61
|
+
return foldResult(fold, result);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const result = applyRound(strategy, a, b);
|
|
65
65
|
if (Number.isNaN(result)) {
|
|
66
66
|
return num(Number.NaN);
|
|
67
67
|
}
|
|
68
|
-
return fold
|
|
68
|
+
return foldResult(fold, result);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/**
|
package/src/lib/tokenizer.js
CHANGED
|
@@ -26,6 +26,32 @@ function tokenize(input) {
|
|
|
26
26
|
return tokenizeTokens(tokenizeCss({ css: input }), input.length);
|
|
27
27
|
}
|
|
28
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
|
+
|
|
29
55
|
/**
|
|
30
56
|
* Convert a slice of an existing CSS token stream into the token subset used
|
|
31
57
|
* by the calculation parser. Token positions remain relative to the original
|
|
@@ -33,52 +59,38 @@ function tokenize(input) {
|
|
|
33
59
|
*
|
|
34
60
|
* @param {import('@csstools/css-tokenizer').CSSToken[]} cssTokens
|
|
35
61
|
* @param {number} eofPosition
|
|
62
|
+
* @param {number} [start]
|
|
63
|
+
* @param {number} [end]
|
|
36
64
|
* @return {Token[]}
|
|
37
65
|
*/
|
|
38
|
-
function tokenizeTokens(
|
|
66
|
+
function tokenizeTokens(
|
|
67
|
+
cssTokens,
|
|
68
|
+
eofPosition,
|
|
69
|
+
start = 0,
|
|
70
|
+
end = cssTokens.length
|
|
71
|
+
) {
|
|
39
72
|
/** @type {Token[]} */
|
|
40
73
|
const tokens = [];
|
|
41
74
|
let ws = true;
|
|
42
75
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* @param {string} raw
|
|
47
|
-
* @param {string | undefined} unit
|
|
48
|
-
* @param {number} pos
|
|
49
|
-
* @return {void}
|
|
50
|
-
*/
|
|
51
|
-
function pushNumeric(raw, unit, pos) {
|
|
52
|
-
let value = /** @type {RegExpExecArray} */ (NUMERIC_RAW.exec(raw))[0];
|
|
53
|
-
const sign = value[0];
|
|
54
|
-
if (sign === '+' || sign === '-') {
|
|
55
|
-
tokens.push({ type: 'punct', value: sign, pos, ws });
|
|
56
|
-
value = value.slice(1);
|
|
57
|
-
pos += 1;
|
|
58
|
-
ws = false;
|
|
59
|
-
}
|
|
60
|
-
if (unit === undefined) {
|
|
61
|
-
tokens.push({ type: 'number', value, pos, ws });
|
|
62
|
-
} else {
|
|
63
|
-
tokens.push({ type: 'dimension', value, unit, pos, ws });
|
|
64
|
-
}
|
|
65
|
-
ws = false;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
for (const t of cssTokens) {
|
|
76
|
+
for (let i = start; i < end; i++) {
|
|
77
|
+
const t = cssTokens[i];
|
|
69
78
|
switch (t[0]) {
|
|
70
79
|
case CssType.Whitespace:
|
|
71
80
|
case CssType.Comment:
|
|
72
81
|
ws = true;
|
|
73
82
|
continue;
|
|
74
83
|
case CssType.Number:
|
|
75
|
-
pushNumeric(t[1], undefined, t[2]);
|
|
84
|
+
pushNumeric(tokens, t[1], undefined, t[2], ws);
|
|
85
|
+
ws = false;
|
|
76
86
|
continue;
|
|
77
87
|
case CssType.Dimension:
|
|
78
|
-
pushNumeric(t[1], t[4].unit, t[2]);
|
|
88
|
+
pushNumeric(tokens, t[1], t[4].unit, t[2], ws);
|
|
89
|
+
ws = false;
|
|
79
90
|
continue;
|
|
80
91
|
case CssType.Percentage:
|
|
81
|
-
pushNumeric(t[1], '%', t[2]);
|
|
92
|
+
pushNumeric(tokens, t[1], '%', t[2], ws);
|
|
93
|
+
ws = false;
|
|
82
94
|
continue;
|
|
83
95
|
case CssType.Ident:
|
|
84
96
|
tokens.push({ type: 'ident', value: t[4].value, pos: t[2], ws });
|
package/src/reduce.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// CSS component-value reducer. This module deliberately has no PostCSS
|
|
2
|
+
// dependency so it can also be used for individual declaration values,
|
|
3
|
+
// at-rule parameters, or selector text.
|
|
4
|
+
import {
|
|
5
|
+
tokenize as cssTokenize,
|
|
6
|
+
TokenType as CssType,
|
|
7
|
+
} from '@csstools/css-tokenizer';
|
|
8
|
+
import { tokenizeTokens } from './lib/tokenizer.js';
|
|
9
|
+
import { parse } from './lib/parser.js';
|
|
10
|
+
import { simplify } from './lib/simplify.js';
|
|
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
|
+
]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {object} ReduceCalcOptions
|
|
28
|
+
* @property {number | false} [precision]
|
|
29
|
+
* @property {boolean} [warnWhenCannotResolve]
|
|
30
|
+
* @property {(error: Error, input: string) => void} [onParseError] Invoked when parse/simplify throws.
|
|
31
|
+
* @property {(message: string) => void} [onWarn] Invoked when `warnWhenCannotResolve` is set and an expression cannot be reduced to a single value.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** @typedef {Required<Omit<ReduceCalcOptions, 'onParseError' | 'onWarn'>> & Pick<ReduceCalcOptions, 'onParseError' | 'onWarn'>} ResolvedReduceCalcOptions */
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Fields threaded unchanged through the token-range walk.
|
|
38
|
+
* `value` is the original full property text, used only for the
|
|
39
|
+
* warnWhenCannotResolve message.
|
|
40
|
+
*
|
|
41
|
+
* @typedef {object} TransformContext
|
|
42
|
+
* @property {ResolvedReduceCalcOptions} options
|
|
43
|
+
* @property {string} value
|
|
44
|
+
* @property {import('@csstools/css-tokenizer').CSSToken[]} tokens
|
|
45
|
+
* @property {Replacement[]} replacements
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {object} Replacement
|
|
50
|
+
* @property {number} start
|
|
51
|
+
* @property {number} end
|
|
52
|
+
* @property {import('./lib/node.js').Node} node
|
|
53
|
+
* @property {string} calcName
|
|
54
|
+
* @property {string} matchedName
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Walk one component-value level. Unsupported functions and simple blocks are
|
|
59
|
+
* traversed, while a supported function is treated as one opaque calculation
|
|
60
|
+
* even when parsing it fails. A missing closer consumes through EOF, matching
|
|
61
|
+
* CSS component-value parsing's error recovery.
|
|
62
|
+
*
|
|
63
|
+
* @param {number} start
|
|
64
|
+
* @param {import('@csstools/css-tokenizer').TokenType | undefined} expectedClose
|
|
65
|
+
* @param {TransformContext} ctx
|
|
66
|
+
* @param {boolean} transform
|
|
67
|
+
* @return {number} Index of the matching closer, or the EOF token.
|
|
68
|
+
*/
|
|
69
|
+
function walkTokens(start, expectedClose, ctx, transform) {
|
|
70
|
+
for (let i = start; i < ctx.tokens.length; i++) {
|
|
71
|
+
const token = ctx.tokens[i];
|
|
72
|
+
if (token[0] === CssType.EOF || token[0] === expectedClose) return i;
|
|
73
|
+
|
|
74
|
+
const blockClose = BLOCK_CLOSE.get(token[0]);
|
|
75
|
+
if (blockClose) {
|
|
76
|
+
i = walkTokens(i + 1, blockClose, ctx, transform);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (token[0] !== CssType.Function) continue;
|
|
81
|
+
|
|
82
|
+
const name = token[4].value;
|
|
83
|
+
const isCalc = MATCH_CALC.test(name);
|
|
84
|
+
const isMath = !isCalc && isSupportedMathFunction(name);
|
|
85
|
+
if (!transform || (!isCalc && !isMath)) {
|
|
86
|
+
i = walkTokens(i + 1, CssType.CloseParen, ctx, transform);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Locate the complete outer function without transforming its children.
|
|
91
|
+
const close = walkTokens(i + 1, CssType.CloseParen, ctx, false);
|
|
92
|
+
const closed = ctx.tokens[close][0] === CssType.CloseParen;
|
|
93
|
+
const end = closed ? ctx.tokens[close][3] + 1 : ctx.value.length;
|
|
94
|
+
const sliceStart = isCalc ? i + 1 : i;
|
|
95
|
+
const sliceEnd = closed ? close + (isCalc ? 0 : 1) : close;
|
|
96
|
+
const inputStart = isCalc ? token[3] + 1 : token[2];
|
|
97
|
+
const inputEnd = closed && isCalc ? ctx.tokens[close][2] : end;
|
|
98
|
+
const contents = ctx.value.slice(inputStart, inputEnd);
|
|
99
|
+
try {
|
|
100
|
+
const node = simplify(
|
|
101
|
+
parse(tokenizeTokens(ctx.tokens, end, sliceStart, sliceEnd))
|
|
102
|
+
);
|
|
103
|
+
ctx.replacements.push({
|
|
104
|
+
start: token[2],
|
|
105
|
+
end,
|
|
106
|
+
node,
|
|
107
|
+
calcName: isCalc ? name : 'calc',
|
|
108
|
+
matchedName: name,
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
const err = error instanceof Error ? error : new Error('Error');
|
|
112
|
+
ctx.options.onParseError?.(err, contents);
|
|
113
|
+
}
|
|
114
|
+
i = close;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return ctx.tokens.length - 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Simplify every supported CSS math function in a component-value string.
|
|
122
|
+
* Text outside those functions is preserved byte-for-byte.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} value
|
|
125
|
+
* @param {ReduceCalcOptions} [opts]
|
|
126
|
+
* @return {string}
|
|
127
|
+
*/
|
|
128
|
+
function reduceCalc(value, opts) {
|
|
129
|
+
if (!hasPotentialMathFunction(value)) {
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** @type {ResolvedReduceCalcOptions} */
|
|
134
|
+
const options = { precision: 5, warnWhenCannotResolve: false, ...opts };
|
|
135
|
+
const tokens = cssTokenize({ css: value });
|
|
136
|
+
/** @type {Replacement[]} */
|
|
137
|
+
const replacements = [];
|
|
138
|
+
walkTokens(0, undefined, { options, value, tokens, replacements }, true);
|
|
139
|
+
|
|
140
|
+
if (replacements.length === 0) {
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let output = '';
|
|
145
|
+
let lastIndex = 0;
|
|
146
|
+
for (const replacement of replacements) {
|
|
147
|
+
const text = serialize(replacement.node, {
|
|
148
|
+
precision: options.precision,
|
|
149
|
+
calcName: replacement.calcName,
|
|
150
|
+
});
|
|
151
|
+
if (
|
|
152
|
+
options.warnWhenCannotResolve &&
|
|
153
|
+
text.startsWith(`${replacement.matchedName}(`)
|
|
154
|
+
) {
|
|
155
|
+
options.onWarn?.('Could not reduce expression: ' + value);
|
|
156
|
+
}
|
|
157
|
+
output += value.slice(lastIndex, replacement.start) + text;
|
|
158
|
+
lastIndex = replacement.end;
|
|
159
|
+
}
|
|
160
|
+
output += value.slice(lastIndex);
|
|
161
|
+
return output;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export { QUICK_MATH_TEST, hasPotentialMathFunction };
|
|
165
|
+
export default reduceCalc;
|
package/types/index.d.ts
CHANGED
|
@@ -9,21 +9,6 @@ export type PostCssCalcOptions = {
|
|
|
9
9
|
onParseError?: (error: Error, input: string) => void;
|
|
10
10
|
};
|
|
11
11
|
export type ResolvedOptions = Required<Omit<PostCssCalcOptions, 'onParseError'>> & Pick<PostCssCalcOptions, 'onParseError'>;
|
|
12
|
-
export type TransformContext = {
|
|
13
|
-
options: ResolvedOptions;
|
|
14
|
-
result: import('postcss').Result;
|
|
15
|
-
item: import('postcss').ChildNode;
|
|
16
|
-
value: string;
|
|
17
|
-
tokens: import('@csstools/css-tokenizer').CSSToken[];
|
|
18
|
-
replacements: Replacement[];
|
|
19
|
-
};
|
|
20
|
-
export type Replacement = {
|
|
21
|
-
start: number;
|
|
22
|
-
end: number;
|
|
23
|
-
node: import('./lib/node.js').Node;
|
|
24
|
-
calcName: string;
|
|
25
|
-
matchedName: string;
|
|
26
|
-
};
|
|
27
12
|
/**
|
|
28
13
|
* @param {PostCssCalcOptions} [opts]
|
|
29
14
|
* @return {import('postcss').Plugin}
|
package/types/lib/parser.d.ts
CHANGED
|
@@ -21,6 +21,22 @@ declare class Parser {
|
|
|
21
21
|
* @return {Token}
|
|
22
22
|
*/
|
|
23
23
|
expect(type: TokenType, value?: string): Token;
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} value
|
|
26
|
+
* @param {string} [value2]
|
|
27
|
+
* @return {boolean}
|
|
28
|
+
*/
|
|
29
|
+
isPunct(value: string, value2?: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} value
|
|
32
|
+
* @return {boolean}
|
|
33
|
+
*/
|
|
34
|
+
matchPunct(value: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} value
|
|
37
|
+
* @return {Token}
|
|
38
|
+
*/
|
|
39
|
+
expectPunct(value: string): Token;
|
|
24
40
|
/**
|
|
25
41
|
* @param {number} [minBp]
|
|
26
42
|
* @return {Node}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
export type Node = import('../node.js').Node;
|
|
2
2
|
export type SimplifyFn = import('../simplify.js').SimplifyFn;
|
|
3
3
|
export type MathSimplifier = (name: string, args: Node[]) => Node;
|
|
4
|
+
declare const QUICK_MATH_TEST: RegExp;
|
|
5
|
+
/**
|
|
6
|
+
* Fast check to determine whether a CSS component value could contain
|
|
7
|
+
* a supported calculation or math function call (or an escape sequence
|
|
8
|
+
* that could decode to one).
|
|
9
|
+
*
|
|
10
|
+
* @param {string} value
|
|
11
|
+
* @return {boolean}
|
|
12
|
+
*/
|
|
13
|
+
declare function hasPotentialMathFunction(value: string): boolean;
|
|
4
14
|
/**
|
|
5
15
|
* Whether a bare CSS math function has an implemented simplifier.
|
|
6
16
|
*
|
|
@@ -16,4 +26,4 @@ declare function isSupportedMathFunction(name: string): boolean;
|
|
|
16
26
|
declare function simplifyCall(node: Extract<Node, {
|
|
17
27
|
type: 'Call';
|
|
18
28
|
}>, simplify: SimplifyFn): Node;
|
|
19
|
-
export { isSupportedMathFunction, simplifyCall };
|
|
29
|
+
export { isSupportedMathFunction, simplifyCall, hasPotentialMathFunction, QUICK_MATH_TEST, };
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
export type Node = import('../node.js').Node;
|
|
2
|
+
export type Num = import('../node.js').Num;
|
|
3
|
+
export type Dim = import('../node.js').Dim;
|
|
2
4
|
export type BaseType = import('../convertUnits.js').BaseType;
|
|
3
5
|
/** @typedef {import('../node.js').Node} Node */
|
|
6
|
+
/** @typedef {import('../node.js').Num} Num */
|
|
7
|
+
/** @typedef {import('../node.js').Dim} Dim */
|
|
4
8
|
/** @typedef {import('../convertUnits.js').BaseType} BaseType */
|
|
9
|
+
/**
|
|
10
|
+
* Construct a Num or Dim node from a folded result.
|
|
11
|
+
* @param {{ unit: string }} fold
|
|
12
|
+
* @param {number} value
|
|
13
|
+
* @return {Num | Dim}
|
|
14
|
+
*/
|
|
15
|
+
declare function foldResult(fold: {
|
|
16
|
+
unit: string;
|
|
17
|
+
}, value: number): Num | Dim;
|
|
5
18
|
/**
|
|
6
19
|
* @param {Node[]} args
|
|
7
20
|
* @return {{ values: number[], unit: string } | null}
|
|
@@ -10,4 +23,4 @@ declare function foldConstArgs(args: Node[]): {
|
|
|
10
23
|
values: number[];
|
|
11
24
|
unit: string;
|
|
12
25
|
} | null;
|
|
13
|
-
export { foldConstArgs };
|
|
26
|
+
export { foldConstArgs, foldResult };
|
package/types/lib/tokenizer.d.ts
CHANGED
|
@@ -24,7 +24,9 @@ declare function tokenize(input: string): Token[];
|
|
|
24
24
|
*
|
|
25
25
|
* @param {import('@csstools/css-tokenizer').CSSToken[]} cssTokens
|
|
26
26
|
* @param {number} eofPosition
|
|
27
|
+
* @param {number} [start]
|
|
28
|
+
* @param {number} [end]
|
|
27
29
|
* @return {Token[]}
|
|
28
30
|
*/
|
|
29
|
-
declare function tokenizeTokens(cssTokens: import('@csstools/css-tokenizer').CSSToken[], eofPosition: number): Token[];
|
|
31
|
+
declare function tokenizeTokens(cssTokens: import('@csstools/css-tokenizer').CSSToken[], eofPosition: number, start?: number, end?: number): Token[];
|
|
30
32
|
export { tokenize, tokenizeTokens };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { hasPotentialMathFunction, QUICK_MATH_TEST } from './lib/simplify/call.js';
|
|
2
|
+
export type ReduceCalcOptions = {
|
|
3
|
+
precision?: number | false;
|
|
4
|
+
warnWhenCannotResolve?: boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Invoked when parse/simplify throws.
|
|
7
|
+
*/
|
|
8
|
+
onParseError?: (error: Error, input: string) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Invoked when `warnWhenCannotResolve` is set and an expression cannot be reduced to a single value.
|
|
11
|
+
*/
|
|
12
|
+
onWarn?: (message: string) => void;
|
|
13
|
+
};
|
|
14
|
+
export type ResolvedReduceCalcOptions = Required<Omit<ReduceCalcOptions, 'onParseError' | 'onWarn'>> & Pick<ReduceCalcOptions, 'onParseError' | 'onWarn'>;
|
|
15
|
+
export type TransformContext = {
|
|
16
|
+
options: ResolvedReduceCalcOptions;
|
|
17
|
+
value: string;
|
|
18
|
+
tokens: import('@csstools/css-tokenizer').CSSToken[];
|
|
19
|
+
replacements: Replacement[];
|
|
20
|
+
};
|
|
21
|
+
export type Replacement = {
|
|
22
|
+
start: number;
|
|
23
|
+
end: number;
|
|
24
|
+
node: import('./lib/node.js').Node;
|
|
25
|
+
calcName: string;
|
|
26
|
+
matchedName: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Simplify every supported CSS math function in a component-value string.
|
|
30
|
+
* Text outside those functions is preserved byte-for-byte.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} value
|
|
33
|
+
* @param {ReduceCalcOptions} [opts]
|
|
34
|
+
* @return {string}
|
|
35
|
+
*/
|
|
36
|
+
declare function reduceCalc(value: string, opts?: ReduceCalcOptions): string;
|
|
37
|
+
export { QUICK_MATH_TEST, hasPotentialMathFunction };
|
|
38
|
+
export default reduceCalc;
|
package/types/lib/type.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export type BaseType =
|
|
2
|
-
| 'length'
|
|
3
|
-
| 'angle'
|
|
4
|
-
| 'time'
|
|
5
|
-
| 'frequency'
|
|
6
|
-
| 'resolution'
|
|
7
|
-
| 'flex'
|
|
8
|
-
| 'percentage';
|
|
9
|
-
/**
|
|
10
|
-
* @param {string} unit
|
|
11
|
-
* @return {BaseType | null}
|
|
12
|
-
*/
|
|
13
|
-
export function baseOf(unit: string): BaseType | null;
|
|
14
|
-
/**
|
|
15
|
-
* Convert a value within a single conversion family. Returns null when
|
|
16
|
-
* either unit is missing from the table (em/rem/vw need runtime context)
|
|
17
|
-
* or when the units belong to different base types.
|
|
18
|
-
* @param {number} value
|
|
19
|
-
* @param {string} from
|
|
20
|
-
* @param {string} to
|
|
21
|
-
* @return {number | null}
|
|
22
|
-
*/
|
|
23
|
-
export function convert(value: number, from: string, to: string): number | null;
|