postcss-calc 11.0.0-rc.2 → 11.0.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 +34 -14
- package/package.json +17 -9
- package/src/index.js +143 -96
- package/src/lib/node.js +9 -4
- package/src/lib/parser.js +11 -11
- package/src/lib/serialize.js +33 -6
- package/src/lib/simplify/abs.js +3 -3
- package/src/lib/simplify/atan2.js +5 -5
- package/src/lib/simplify/call.js +52 -43
- package/src/lib/simplify/clamp.js +2 -2
- package/src/lib/simplify/exp.js +2 -2
- package/src/lib/simplify/hypot.js +2 -2
- package/src/lib/simplify/inverse-trig.js +5 -5
- package/src/lib/simplify/log.js +2 -2
- package/src/lib/simplify/min-max.js +2 -2
- package/src/lib/simplify/mod-rem.js +10 -10
- package/src/lib/simplify/pow.js +2 -2
- package/src/lib/simplify/round.js +12 -18
- package/src/lib/simplify/sign.js +3 -3
- package/src/lib/simplify/sqrt.js +2 -2
- package/src/lib/simplify/sum.js +18 -1
- package/src/lib/simplify/trig.js +3 -3
- package/types/index.d.ts +12 -7
- package/types/lib/node.d.ts +2 -1
- package/types/lib/simplify/call.d.ts +9 -3
- package/types/lib/type.d.ts +8 -1
package/README.md
CHANGED
|
@@ -56,7 +56,8 @@ Checkout [tests] for more examples.
|
|
|
56
56
|
|
|
57
57
|
#### `precision` (default: `5`)
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
Allows you to define the precision for decimal numbers. Set it to `false` to
|
|
60
|
+
disable rounding.
|
|
60
61
|
|
|
61
62
|
```js
|
|
62
63
|
var out = postcss()
|
|
@@ -64,17 +65,6 @@ var out = postcss()
|
|
|
64
65
|
.process(css).css;
|
|
65
66
|
```
|
|
66
67
|
|
|
67
|
-
#### `preserve` (default: `false`)
|
|
68
|
-
|
|
69
|
-
Allow you to preserve calc() usage in output so browsers will handle decimal
|
|
70
|
-
precision themselves.
|
|
71
|
-
|
|
72
|
-
```js
|
|
73
|
-
var out = postcss()
|
|
74
|
-
.use(calc({ preserve: true }))
|
|
75
|
-
.process(css).css;
|
|
76
|
-
```
|
|
77
|
-
|
|
78
68
|
#### `warnWhenCannotResolve` (default: `false`)
|
|
79
69
|
|
|
80
70
|
Adds warnings when calc() are not reduced to a single value.
|
|
@@ -87,7 +77,7 @@ var out = postcss()
|
|
|
87
77
|
|
|
88
78
|
#### `mediaQueries` (default: `false`)
|
|
89
79
|
|
|
90
|
-
Allows calc() usage
|
|
80
|
+
Allows calc() usage in media query parameters.
|
|
91
81
|
|
|
92
82
|
```js
|
|
93
83
|
var out = postcss()
|
|
@@ -95,6 +85,26 @@ var out = postcss()
|
|
|
95
85
|
.process(css).css;
|
|
96
86
|
```
|
|
97
87
|
|
|
88
|
+
Example:
|
|
89
|
+
|
|
90
|
+
```css
|
|
91
|
+
@media (min-width: calc(100px + 100px)) {
|
|
92
|
+
div {
|
|
93
|
+
width: 100px;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
With `mediaQueries: true`, this becomes:
|
|
99
|
+
|
|
100
|
+
```css
|
|
101
|
+
@media (min-width: 200px) {
|
|
102
|
+
div {
|
|
103
|
+
width: 100px;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
98
108
|
#### `selectors` (default: `false`)
|
|
99
109
|
|
|
100
110
|
Allows calc() usage as part of selectors.
|
|
@@ -108,11 +118,13 @@ var out = postcss()
|
|
|
108
118
|
Example:
|
|
109
119
|
|
|
110
120
|
```css
|
|
111
|
-
div
|
|
121
|
+
div:nth-child(calc(1 + 2)) {
|
|
112
122
|
width: 100px;
|
|
113
123
|
}
|
|
114
124
|
```
|
|
115
125
|
|
|
126
|
+
With `selectors: true`, this becomes `div:nth-child(3)`.
|
|
127
|
+
|
|
116
128
|
#### `onParseError`
|
|
117
129
|
|
|
118
130
|
Callback invoked when a `calc()` body fails to parse or simplify. Matches
|
|
@@ -186,6 +198,14 @@ npm install
|
|
|
186
198
|
npm test
|
|
187
199
|
```
|
|
188
200
|
|
|
201
|
+
The normal test run uses a deterministic structural sample of the harvested
|
|
202
|
+
real-world corpus. Run the complete differential corpus before releases or
|
|
203
|
+
when changing parsing/simplification behavior:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
pnpm test:corpus:full
|
|
207
|
+
```
|
|
208
|
+
|
|
189
209
|
## [Changelog](CHANGELOG.md)
|
|
190
210
|
|
|
191
211
|
## [License](LICENSE)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postcss-calc",
|
|
3
|
-
"version": "11.0.0
|
|
3
|
+
"version": "11.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "PostCSS plugin to reduce calc()",
|
|
6
6
|
"keywords": [
|
|
@@ -29,21 +29,27 @@
|
|
|
29
29
|
"author": "Andy Jansson",
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"engines": {
|
|
32
|
-
"node": "^22.
|
|
32
|
+
"node": "^22.22.3 || ^24.15 || >=26.0"
|
|
33
|
+
},
|
|
34
|
+
"devEngines": {
|
|
35
|
+
"packageManager": {
|
|
36
|
+
"name": "pnpm",
|
|
37
|
+
"version": "11.22.0"
|
|
38
|
+
}
|
|
33
39
|
},
|
|
34
40
|
"devDependencies": {
|
|
35
41
|
"@csstools/css-calc": "^3.3.0",
|
|
36
42
|
"@rmenke/css-tokenizer-tests": "^1.2.0",
|
|
37
|
-
"@types/node": "^26.
|
|
43
|
+
"@types/node": "^26.2.0",
|
|
38
44
|
"fast-check": "^4.9.0",
|
|
39
|
-
"oxfmt": "^0.
|
|
40
|
-
"oxlint": "^1.
|
|
41
|
-
"postcss": "^8.5.
|
|
45
|
+
"oxfmt": "^0.64.0",
|
|
46
|
+
"oxlint": "^1.79.0",
|
|
47
|
+
"postcss": "^8.5.26",
|
|
42
48
|
"typescript": "~7.0.2"
|
|
43
49
|
},
|
|
44
50
|
"dependencies": {
|
|
45
|
-
"@csstools/css-
|
|
46
|
-
"
|
|
51
|
+
"@csstools/css-parser-algorithms": "^4.0.0",
|
|
52
|
+
"@csstools/css-tokenizer": "^4.0.0"
|
|
47
53
|
},
|
|
48
54
|
"peerDependencies": {
|
|
49
55
|
"postcss": "^8.5.25"
|
|
@@ -51,6 +57,8 @@
|
|
|
51
57
|
"scripts": {
|
|
52
58
|
"lint": "oxlint . && tsc && oxfmt --check",
|
|
53
59
|
"fmt": "oxfmt",
|
|
54
|
-
"test": "node --test"
|
|
60
|
+
"test": "node --test --test-reporter=dot 'test/**/*.test.mjs' test/index.cjs test/convertUnit.cjs",
|
|
61
|
+
"test:mutation:corpus": "node test/mutation/corpus-selection.mjs",
|
|
62
|
+
"test:corpus:full": "POSTCSS_CALC_FULL_CORPUS=1 node --test test/conformance/corpus.test.mjs"
|
|
55
63
|
}
|
|
56
64
|
}
|
package/src/index.js
CHANGED
|
@@ -1,50 +1,111 @@
|
|
|
1
1
|
// PostCSS adapter. Walks declaration values (and optionally @rule params
|
|
2
2
|
// and selectors), feeds calc() bodies through tokenize → parse → simplify
|
|
3
3
|
// → serialize, and writes the result back.
|
|
4
|
-
import
|
|
4
|
+
import { tokenize as cssTokenize } from '@csstools/css-tokenizer';
|
|
5
|
+
import {
|
|
6
|
+
isFunctionNode,
|
|
7
|
+
isSimpleBlockNode,
|
|
8
|
+
parseListOfComponentValues,
|
|
9
|
+
} from '@csstools/css-parser-algorithms';
|
|
5
10
|
import { tokenize } from './lib/tokenizer.js';
|
|
6
11
|
import { parse } from './lib/parser.js';
|
|
7
12
|
import { simplify } from './lib/simplify.js';
|
|
13
|
+
import { isSupportedMathFunction } from './lib/simplify/call.js';
|
|
8
14
|
import { serialize } from './lib/serialize.js';
|
|
9
15
|
|
|
10
|
-
|
|
16
|
+
// The outer walk is deliberately forgiving: it only needs to locate calc()/
|
|
17
|
+
// math-function boundaries in otherwise arbitrary (and possibly non-standard)
|
|
18
|
+
// CSS values, so parse errors from the outer tokenizer/parser are swallowed.
|
|
19
|
+
// Genuine syntax problems inside a matched call are
|
|
20
|
+
// caught below via our own tokenize/parse/simplify pipeline.
|
|
21
|
+
const NOOP_PARSE_ERROR = { onParseError: () => {} };
|
|
11
22
|
|
|
12
|
-
|
|
13
|
-
// Mirrors the dispatch in lib/simplify/call.js.
|
|
14
|
-
const MATH_FUNCTIONS = new Set([
|
|
15
|
-
'min',
|
|
16
|
-
'max',
|
|
17
|
-
'clamp',
|
|
18
|
-
'abs',
|
|
19
|
-
'sign',
|
|
20
|
-
'mod',
|
|
21
|
-
'rem',
|
|
22
|
-
'round',
|
|
23
|
-
'sin',
|
|
24
|
-
'cos',
|
|
25
|
-
'tan',
|
|
26
|
-
'asin',
|
|
27
|
-
'acos',
|
|
28
|
-
'atan',
|
|
29
|
-
'atan2',
|
|
30
|
-
'pow',
|
|
31
|
-
'sqrt',
|
|
32
|
-
'hypot',
|
|
33
|
-
'log',
|
|
34
|
-
'exp',
|
|
35
|
-
]);
|
|
23
|
+
const MATCH_CALC = /^(?:-(?:moz|webkit)-)?calc$/i;
|
|
36
24
|
|
|
37
25
|
/**
|
|
38
|
-
* @typedef {object}
|
|
26
|
+
* @typedef {object} PostCssCalcOptions
|
|
39
27
|
* @property {number | false} [precision]
|
|
40
|
-
* @property {boolean} [preserve]
|
|
41
28
|
* @property {boolean} [warnWhenCannotResolve]
|
|
42
29
|
* @property {boolean} [mediaQueries]
|
|
43
30
|
* @property {boolean} [selectors]
|
|
44
31
|
* @property {(error: Error, input: string) => void} [onParseError] Invoked when parse/simplify throws. Replaces the default `result.warn`.
|
|
45
32
|
*/
|
|
46
33
|
|
|
47
|
-
/** @typedef {Required<Omit<
|
|
34
|
+
/** @typedef {Required<Omit<PostCssCalcOptions, 'onParseError'>> & Pick<PostCssCalcOptions, 'onParseError'>} ResolvedOptions */
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Fields threaded unchanged through the recursive `transformList` walk.
|
|
38
|
+
* `value` is the original full property text, used only for the
|
|
39
|
+
* warnWhenCannotResolve message.
|
|
40
|
+
*
|
|
41
|
+
* @typedef {object} TransformContext
|
|
42
|
+
* @property {ResolvedOptions} options
|
|
43
|
+
* @property {import('postcss').Result} result
|
|
44
|
+
* @property {import('postcss').ChildNode} item
|
|
45
|
+
* @property {string} value
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Walks a list of component values in place, replacing matched calc()/math
|
|
50
|
+
* function nodes with their simplified form. Unlike the library's generic
|
|
51
|
+
* `walk` helper, this recurses manually so a matched node's own (stale,
|
|
52
|
+
* pre-simplification) children are never independently re-visited.
|
|
53
|
+
*
|
|
54
|
+
* @param {import('@csstools/css-parser-algorithms').ComponentValue[]} list
|
|
55
|
+
* @param {TransformContext} ctx
|
|
56
|
+
* @return {void}
|
|
57
|
+
*/
|
|
58
|
+
function transformList(list, ctx) {
|
|
59
|
+
for (let i = 0; i < list.length; i++) {
|
|
60
|
+
const node = list[i];
|
|
61
|
+
if (!isFunctionNode(node)) {
|
|
62
|
+
if (isSimpleBlockNode(node)) {
|
|
63
|
+
transformList(node.value, ctx);
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const name = node.getName();
|
|
69
|
+
const isCalc = MATCH_CALC.test(name);
|
|
70
|
+
const isMath = !isCalc && isSupportedMathFunction(name);
|
|
71
|
+
if (!isCalc && !isMath) {
|
|
72
|
+
transformList(node.value, ctx);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// calc(): feed the body. Bare math: feed the whole call.
|
|
77
|
+
const inner = node.value.map((child) => child.toString()).join('');
|
|
78
|
+
const contents = isCalc ? inner : `${name}(${inner})`;
|
|
79
|
+
try {
|
|
80
|
+
const simplified = simplify(parse(tokenize(contents)));
|
|
81
|
+
const str = serialize(simplified, {
|
|
82
|
+
precision: ctx.options.precision,
|
|
83
|
+
calcName: isCalc ? name : 'calc', // preserve vendor prefix on calc()
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (ctx.options.warnWhenCannotResolve && str.startsWith(`${name}(`)) {
|
|
87
|
+
ctx.result.warn('Could not reduce expression: ' + ctx.value, {
|
|
88
|
+
plugin: 'postcss-calc',
|
|
89
|
+
node: ctx.item,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const replacement = parseListOfComponentValues(
|
|
94
|
+
cssTokenize({ css: str }),
|
|
95
|
+
NOOP_PARSE_ERROR
|
|
96
|
+
);
|
|
97
|
+
list.splice(i, 1, ...replacement);
|
|
98
|
+
i += replacement.length - 1;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
const err = error instanceof Error ? error : new Error('Error');
|
|
101
|
+
if (ctx.options.onParseError) {
|
|
102
|
+
ctx.options.onParseError(err, contents);
|
|
103
|
+
} else {
|
|
104
|
+
ctx.result.warn(err.message, { node: ctx.item });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
48
109
|
|
|
49
110
|
/**
|
|
50
111
|
* @param {string} value
|
|
@@ -54,60 +115,42 @@ const MATH_FUNCTIONS = new Set([
|
|
|
54
115
|
* @return {string}
|
|
55
116
|
*/
|
|
56
117
|
function transformValue(value, options, result, item) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
const isCalc = MATCH_CALC.test(node.value);
|
|
63
|
-
const isMath = !isCalc && MATH_FUNCTIONS.has(node.value.toLowerCase());
|
|
64
|
-
if (!isCalc && !isMath) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
118
|
+
const componentValues = parseListOfComponentValues(
|
|
119
|
+
cssTokenize({ css: value }),
|
|
120
|
+
NOOP_PARSE_ERROR
|
|
121
|
+
);
|
|
67
122
|
|
|
68
|
-
|
|
69
|
-
const inner = valueParser.stringify(node.nodes);
|
|
70
|
-
const contents = isCalc ? inner : `${node.value}(${inner})`;
|
|
71
|
-
try {
|
|
72
|
-
const simplified = simplify(parse(tokenize(contents)));
|
|
73
|
-
const str = serialize(simplified, {
|
|
74
|
-
precision: options.precision,
|
|
75
|
-
calcName: isCalc ? node.value : 'calc', // preserve vendor prefix on calc()
|
|
76
|
-
});
|
|
123
|
+
transformList(componentValues, { options, result, item, value });
|
|
77
124
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
plugin: 'postcss-calc',
|
|
81
|
-
node: item,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
125
|
+
return componentValues.map((node) => node.toString()).join('');
|
|
126
|
+
}
|
|
84
127
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Runs `transformValue` over one text property of a decl/atrule/rule node
|
|
130
|
+
* and updates it in place.
|
|
131
|
+
* `setProp` closes over the property name and the concrete node type at
|
|
132
|
+
* each call site, since `Declaration`/`AtRule`/`Rule` don't share a typed
|
|
133
|
+
* "text property" name to index generically.
|
|
134
|
+
*
|
|
135
|
+
* @param {import('postcss').ChildNode} node
|
|
136
|
+
* @param {string} current
|
|
137
|
+
* @param {(target: import('postcss').ChildNode, value: string) => void} setProp
|
|
138
|
+
* @param {ResolvedOptions} options
|
|
139
|
+
* @param {import('postcss').Result} result
|
|
140
|
+
* @return {void}
|
|
141
|
+
*/
|
|
142
|
+
function applyTransform(node, current, setProp, options, result) {
|
|
143
|
+
setProp(node, transformValue(current, options, result, node));
|
|
100
144
|
}
|
|
101
145
|
|
|
102
146
|
/**
|
|
103
|
-
* @param {
|
|
147
|
+
* @param {PostCssCalcOptions} [opts]
|
|
104
148
|
* @return {import('postcss').Plugin}
|
|
105
149
|
*/
|
|
106
150
|
function pluginCreator(opts) {
|
|
107
151
|
/** @type {ResolvedOptions} */
|
|
108
152
|
const options = {
|
|
109
153
|
precision: 5,
|
|
110
|
-
preserve: false,
|
|
111
154
|
warnWhenCannotResolve: false,
|
|
112
155
|
mediaQueries: false,
|
|
113
156
|
selectors: false,
|
|
@@ -123,45 +166,49 @@ function pluginCreator(opts) {
|
|
|
123
166
|
OnceExit(css, { result }) {
|
|
124
167
|
css.walk((node) => {
|
|
125
168
|
if (node.type === 'decl') {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
169
|
+
applyTransform(
|
|
170
|
+
node,
|
|
171
|
+
node.value,
|
|
172
|
+
(n, v) => {
|
|
173
|
+
/** @type {import('postcss').Declaration} */ (n).value = v;
|
|
174
|
+
},
|
|
175
|
+
options,
|
|
176
|
+
result
|
|
177
|
+
);
|
|
134
178
|
}
|
|
135
179
|
if (node.type === 'atrule' && options.mediaQueries) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
180
|
+
applyTransform(
|
|
181
|
+
node,
|
|
182
|
+
node.params,
|
|
183
|
+
(n, v) => {
|
|
184
|
+
/** @type {import('postcss').AtRule} */ (n).params = v;
|
|
185
|
+
},
|
|
186
|
+
options,
|
|
187
|
+
result
|
|
188
|
+
);
|
|
144
189
|
}
|
|
145
190
|
if (node.type === 'rule' && options.selectors) {
|
|
146
191
|
// Reduces `:nth-child(calc(...))` via the function walk. calc() in a
|
|
147
192
|
// quoted attribute value is a literal match, so it's left untouched.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
193
|
+
applyTransform(
|
|
194
|
+
node,
|
|
195
|
+
node.selector,
|
|
196
|
+
(n, v) => {
|
|
197
|
+
/** @type {import('postcss').Rule} */ (n).selector = v;
|
|
198
|
+
},
|
|
199
|
+
options,
|
|
200
|
+
result
|
|
201
|
+
);
|
|
156
202
|
}
|
|
157
203
|
});
|
|
158
204
|
},
|
|
159
205
|
};
|
|
160
206
|
}
|
|
161
207
|
|
|
208
|
+
/** @type {true} */
|
|
162
209
|
pluginCreator.postcss = true;
|
|
163
210
|
|
|
164
|
-
export default /** @type import('postcss').PluginCreator<
|
|
211
|
+
export default /** @type import('postcss').PluginCreator<PostCssCalcOptions>*/ (
|
|
165
212
|
pluginCreator
|
|
166
213
|
);
|
|
167
214
|
export { pluginCreator as 'module.exports' };
|
package/src/lib/node.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// is `Num(-5)`, never `Sum([{sign:-1, Num(5)}])`. One form per value.
|
|
6
6
|
// - In a SumTerm with Num/Dim node, sign is always +1; the sign slot is
|
|
7
7
|
// reserved for opaque nodes (Ident, Call, Product, multi-term Sum).
|
|
8
|
-
// - No Sum directly contains another Sum (flattened on
|
|
8
|
+
// - No ungrouped Sum directly contains another Sum (flattened on
|
|
9
|
+
// construction). A grouped Sum is retained so a later negative sign
|
|
10
|
+
// cannot be distributed across opaque terms.
|
|
9
11
|
// - No Product directly contains another Product (flattened).
|
|
10
12
|
// - A Sum/Product with one positive element collapses to that element.
|
|
11
13
|
// - A Sum/Product with no elements collapses to Num(0) / Num(1).
|
|
@@ -18,7 +20,7 @@
|
|
|
18
20
|
* @typedef {{type: 'Ident', name: string}} Ident
|
|
19
21
|
* @typedef {{type: 'Call', name: string, args: Node[]}} Call
|
|
20
22
|
* @typedef {{sign: 1 | -1, node: Node}} SumTerm Sign is always +1 when node is Num or Dim.
|
|
21
|
-
* @typedef {{type: 'Sum', terms: SumTerm[]}} Sum
|
|
23
|
+
* @typedef {{type: 'Sum', terms: SumTerm[], grouped?: boolean}} Sum
|
|
22
24
|
* @typedef {{exponent: 1 | -1, node: Node}} ProductFactor exponent +1 = numerator, -1 = denominator.
|
|
23
25
|
* @typedef {{type: 'Product', factors: ProductFactor[]}} Product
|
|
24
26
|
* @typedef {Num | Dim | Ident | Call | Sum | Product} Node
|
|
@@ -85,7 +87,7 @@ function mkSum(rawTerms) {
|
|
|
85
87
|
function pushSumTerm(out, term) {
|
|
86
88
|
let { sign, node } = term;
|
|
87
89
|
|
|
88
|
-
if (node.type === 'Sum') {
|
|
90
|
+
if (node.type === 'Sum' && !node.grouped) {
|
|
89
91
|
for (const inner of node.terms) {
|
|
90
92
|
pushSumTerm(out, {
|
|
91
93
|
sign: /** @type {1 | -1} */ (sign * inner.sign),
|
|
@@ -170,12 +172,15 @@ function negate(node) {
|
|
|
170
172
|
return dim(-node.value, node.unit);
|
|
171
173
|
}
|
|
172
174
|
if (node.type === 'Sum') {
|
|
173
|
-
|
|
175
|
+
const result = mkSum(
|
|
174
176
|
node.terms.map((t) => ({
|
|
175
177
|
sign: /** @type {1 | -1} */ (-t.sign),
|
|
176
178
|
node: t.node,
|
|
177
179
|
}))
|
|
178
180
|
);
|
|
181
|
+
return node.grouped && result.type === 'Sum'
|
|
182
|
+
? { ...result, grouped: true }
|
|
183
|
+
: result;
|
|
179
184
|
}
|
|
180
185
|
// Opaque (Ident, Call, Product): wrap as a single negative-sign term —
|
|
181
186
|
// the only case where sign=-1 remains on a SumTerm.
|
package/src/lib/parser.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Pratt parser. +/- emit Sum nodes; */÷ emit Product nodes. node.js
|
|
2
|
-
// constructors flatten and normalize on construction,
|
|
3
|
-
//
|
|
4
|
-
import { mkSum, mkProduct, negate } from './node.js';
|
|
2
|
+
// constructors flatten and normalize on construction, while parenthesized
|
|
3
|
+
// sums retain a grouping marker for the opaque-subtraction invariant.
|
|
4
|
+
import { mkSum, mkProduct, negate, ident, call } from './node.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @typedef {import('./tokenizer.js').Token} Token
|
|
@@ -30,7 +30,7 @@ function foldCalcKeyword(name) {
|
|
|
30
30
|
// form arrives as a single ident because CSS Syntax tokenizes leading
|
|
31
31
|
// `-` + ident-start as one ident-token.
|
|
32
32
|
if (name === 'NaN' || name === '-NaN') {
|
|
33
|
-
return { type: 'Num', value: NaN };
|
|
33
|
+
return { type: 'Num', value: Number.NaN };
|
|
34
34
|
}
|
|
35
35
|
switch (name.toLowerCase()) {
|
|
36
36
|
case 'pi':
|
|
@@ -174,7 +174,7 @@ function parseOpaqueCall(p, name) {
|
|
|
174
174
|
const flush = () => {
|
|
175
175
|
const trimmed = buf.trim();
|
|
176
176
|
if (trimmed) {
|
|
177
|
-
args.push(
|
|
177
|
+
args.push(ident(trimmed));
|
|
178
178
|
}
|
|
179
179
|
buf = '';
|
|
180
180
|
};
|
|
@@ -191,7 +191,7 @@ function parseOpaqueCall(p, name) {
|
|
|
191
191
|
if (depth === 0) {
|
|
192
192
|
p.next();
|
|
193
193
|
flush();
|
|
194
|
-
return
|
|
194
|
+
return call(name, args);
|
|
195
195
|
}
|
|
196
196
|
} else if (tk.value === ',' && depth === 1) {
|
|
197
197
|
p.next();
|
|
@@ -228,12 +228,12 @@ function requireSurroundingWs(p, token) {
|
|
|
228
228
|
|
|
229
229
|
/** @type {Record<string, PrefixParselet>} */
|
|
230
230
|
const PREFIX = {
|
|
231
|
-
number: (_p, t) => ({ type: 'Num', value: parseFloat(t.value) }),
|
|
231
|
+
number: (_p, t) => ({ type: 'Num', value: Number.parseFloat(t.value) }),
|
|
232
232
|
|
|
233
233
|
// Unit case normalization per §10.12: `1PX` serializes as `1px`.
|
|
234
234
|
dimension: (_p, t) => ({
|
|
235
235
|
type: 'Dim',
|
|
236
|
-
value: parseFloat(t.value),
|
|
236
|
+
value: Number.parseFloat(t.value),
|
|
237
237
|
unit: t.unit === '%' ? '%' : /** @type {string} */ (t.unit).toLowerCase(),
|
|
238
238
|
}),
|
|
239
239
|
|
|
@@ -254,19 +254,19 @@ const PREFIX = {
|
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
p.expect('punct', ')');
|
|
257
|
-
return
|
|
257
|
+
return call(t.value, args);
|
|
258
258
|
}
|
|
259
259
|
const kw = foldCalcKeyword(t.value);
|
|
260
260
|
if (kw) {
|
|
261
261
|
return kw;
|
|
262
262
|
}
|
|
263
|
-
return
|
|
263
|
+
return ident(t.value);
|
|
264
264
|
},
|
|
265
265
|
|
|
266
266
|
'(': (p) => {
|
|
267
267
|
const e = p.parseExpr(0);
|
|
268
268
|
p.expect('punct', ')');
|
|
269
|
-
return e;
|
|
269
|
+
return e.type === 'Sum' ? { ...e, grouped: true } : e;
|
|
270
270
|
},
|
|
271
271
|
|
|
272
272
|
'-': (p) => negate(p.parseExpr(UNARY_BP)),
|
package/src/lib/serialize.js
CHANGED
|
@@ -45,7 +45,7 @@ function round(v, prec) {
|
|
|
45
45
|
* @return {boolean}
|
|
46
46
|
*/
|
|
47
47
|
function isDegenerate(v) {
|
|
48
|
-
return !isFinite(v) || isNaN(v);
|
|
48
|
+
return !Number.isFinite(v) || Number.isNaN(v);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
@@ -53,7 +53,7 @@ function isDegenerate(v) {
|
|
|
53
53
|
* @return {string}
|
|
54
54
|
*/
|
|
55
55
|
function degenerateKeyword(v) {
|
|
56
|
-
if (isNaN(v)) {
|
|
56
|
+
if (Number.isNaN(v)) {
|
|
57
57
|
return 'NaN';
|
|
58
58
|
}
|
|
59
59
|
return v > 0 ? 'infinity' : '-infinity';
|
|
@@ -77,6 +77,25 @@ function serialize(node, opts = {}) {
|
|
|
77
77
|
return `${calcName}(${degenerateKeyword(node.value)} * 1${node.unit})`;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// A grouped sum with a leading negative term is the canonical result of
|
|
81
|
+
// negating a parenthesized expression. Re-invert its terms for the body so
|
|
82
|
+
// the grouping survives as `-(...)` instead of becoming `-a - b`.
|
|
83
|
+
if (
|
|
84
|
+
node.type === 'Sum' &&
|
|
85
|
+
node.grouped &&
|
|
86
|
+
node.terms.length > 1 &&
|
|
87
|
+
displaySign(node.terms[0]).sign === -1
|
|
88
|
+
) {
|
|
89
|
+
const body = /** @type {Sum} */ ({
|
|
90
|
+
type: 'Sum',
|
|
91
|
+
terms: node.terms.map((t) => ({
|
|
92
|
+
sign: /** @type {1 | -1} */ (-t.sign),
|
|
93
|
+
node: t.node,
|
|
94
|
+
})),
|
|
95
|
+
});
|
|
96
|
+
return `${calcName}(-(${serializeExpr(body, prec)}))`;
|
|
97
|
+
}
|
|
98
|
+
|
|
80
99
|
if (
|
|
81
100
|
node.type === 'Num' ||
|
|
82
101
|
node.type === 'Dim' ||
|
|
@@ -141,13 +160,13 @@ function serializeExpr(node, prec) {
|
|
|
141
160
|
*/
|
|
142
161
|
function displaySign(term) {
|
|
143
162
|
const { sign, node } = term;
|
|
144
|
-
if (node.type === 'Num' && isFinite(node.value) && node.value < 0) {
|
|
163
|
+
if (node.type === 'Num' && Number.isFinite(node.value) && node.value < 0) {
|
|
145
164
|
return {
|
|
146
165
|
sign: /** @type {1 | -1} */ (-sign),
|
|
147
166
|
magnitude: { type: 'Num', value: -node.value },
|
|
148
167
|
};
|
|
149
168
|
}
|
|
150
|
-
if (node.type === 'Dim' && isFinite(node.value) && node.value < 0) {
|
|
169
|
+
if (node.type === 'Dim' && Number.isFinite(node.value) && node.value < 0) {
|
|
151
170
|
return {
|
|
152
171
|
sign: /** @type {1 | -1} */ (-sign),
|
|
153
172
|
magnitude: { type: 'Dim', value: -node.value, unit: node.unit },
|
|
@@ -166,13 +185,21 @@ function serializeSum(sum, prec) {
|
|
|
166
185
|
for (const [i, t] of sum.terms.entries()) {
|
|
167
186
|
const { sign, magnitude } = displaySign(t);
|
|
168
187
|
if (i === 0) {
|
|
188
|
+
if (magnitude.type === 'Sum' && magnitude.grouped) {
|
|
189
|
+
const body = `(${serializeExpr(magnitude, prec)})`;
|
|
190
|
+
out = sign === 1 ? body : `-${body}`;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
169
193
|
out =
|
|
170
194
|
sign === 1
|
|
171
195
|
? serializeExpr(magnitude, prec)
|
|
172
196
|
: serializeLeadingNeg(magnitude, prec);
|
|
173
197
|
} else {
|
|
174
198
|
// `-` binds looser than `*`/`/` so the right side never needs parens.
|
|
175
|
-
|
|
199
|
+
let body = serializeExpr(magnitude, prec);
|
|
200
|
+
if (magnitude.type === 'Sum' && magnitude.grouped) {
|
|
201
|
+
body = `(${body})`;
|
|
202
|
+
}
|
|
176
203
|
out += sign === 1 ? ` + ${body}` : ` - ${body}`;
|
|
177
204
|
}
|
|
178
205
|
}
|
|
@@ -192,7 +219,7 @@ function serializeLeadingNeg(node, prec) {
|
|
|
192
219
|
node.factors.length > 0 &&
|
|
193
220
|
node.factors[0].exponent === 1 &&
|
|
194
221
|
node.factors[0].node.type === 'Num' &&
|
|
195
|
-
isFinite(node.factors[0].node.value) &&
|
|
222
|
+
Number.isFinite(node.factors[0].node.value) &&
|
|
196
223
|
node.factors[0].node.value !== 0
|
|
197
224
|
) {
|
|
198
225
|
const head = node.factors[0].node;
|
package/src/lib/simplify/abs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { num, dim } from '../node.js';
|
|
1
|
+
import { num, dim, call } from '../node.js';
|
|
2
2
|
|
|
3
3
|
/** @typedef {import('../node.js').Node} Node */
|
|
4
4
|
|
|
@@ -8,7 +8,7 @@ import { num, dim } from '../node.js';
|
|
|
8
8
|
*/
|
|
9
9
|
function simplifyAbs(args) {
|
|
10
10
|
if (args.length !== 1) {
|
|
11
|
-
return
|
|
11
|
+
return call('abs', args);
|
|
12
12
|
}
|
|
13
13
|
const a = args[0];
|
|
14
14
|
if (a.type === 'Num') {
|
|
@@ -17,7 +17,7 @@ function simplifyAbs(args) {
|
|
|
17
17
|
if (a.type === 'Dim' && a.unit !== '%') {
|
|
18
18
|
return dim(Math.abs(a.value), a.unit);
|
|
19
19
|
}
|
|
20
|
-
return
|
|
20
|
+
return call('abs', [a]);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export { simplifyAbs };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* §10.4 — atan2. foldConstArgs already rejects percentages (property-
|
|
2
2
|
context-resolved) and enforces shared base + static convertibility. */
|
|
3
3
|
|
|
4
|
-
import { num, dim } from '../node.js';
|
|
4
|
+
import { num, dim, call } from '../node.js';
|
|
5
5
|
import { foldConstArgs } from './fold.js';
|
|
6
6
|
|
|
7
7
|
/** @typedef {import('../node.js').Node} Node */
|
|
@@ -12,16 +12,16 @@ import { foldConstArgs } from './fold.js';
|
|
|
12
12
|
*/
|
|
13
13
|
function simplifyAtan2(args) {
|
|
14
14
|
if (args.length !== 2) {
|
|
15
|
-
return
|
|
15
|
+
return call('atan2', args);
|
|
16
16
|
}
|
|
17
17
|
const fold = foldConstArgs(args);
|
|
18
18
|
if (fold === null) {
|
|
19
|
-
return
|
|
19
|
+
return call('atan2', args);
|
|
20
20
|
}
|
|
21
21
|
const [y, x] = /** @type {[number, number]} */ (fold.values);
|
|
22
22
|
const radians = Math.atan2(y, x);
|
|
23
|
-
if (isNaN(radians)) {
|
|
24
|
-
return num(NaN);
|
|
23
|
+
if (Number.isNaN(radians)) {
|
|
24
|
+
return num(Number.NaN);
|
|
25
25
|
}
|
|
26
26
|
return dim((radians * 180) / Math.PI, 'deg');
|
|
27
27
|
}
|
package/src/lib/simplify/call.js
CHANGED
|
@@ -16,9 +16,51 @@ import { simplifyExp } from './exp.js';
|
|
|
16
16
|
import { simplifyLog } from './log.js';
|
|
17
17
|
import { simplifyHypot } from './hypot.js';
|
|
18
18
|
|
|
19
|
+
import { call } from '../node.js';
|
|
20
|
+
|
|
19
21
|
/** @typedef {import('../node.js').Node} Node */
|
|
20
22
|
/** @typedef {import('../simplify.js').SimplifyFn} SimplifyFn */
|
|
21
23
|
|
|
24
|
+
/** @typedef {(name: string, args: Node[]) => Node} MathSimplifier */
|
|
25
|
+
|
|
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 both dispatch and `isSupportedMathFunction`.
|
|
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
|
+
/**
|
|
55
|
+
* Whether a bare CSS math function has an implemented simplifier.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} name
|
|
58
|
+
* @return {boolean}
|
|
59
|
+
*/
|
|
60
|
+
function isSupportedMathFunction(name) {
|
|
61
|
+
return MATH_SIMPLIFIERS.has(name.toLowerCase());
|
|
62
|
+
}
|
|
63
|
+
|
|
22
64
|
/**
|
|
23
65
|
* @param {Extract<Node, { type: 'Call' }>} node
|
|
24
66
|
* @param {SimplifyFn} simplify
|
|
@@ -36,50 +78,17 @@ function simplifyCall(node, simplify) {
|
|
|
36
78
|
|
|
37
79
|
const args = node.args.map((a) => simplify(a));
|
|
38
80
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
if (name === 'sign') {
|
|
49
|
-
return simplifySign(args);
|
|
50
|
-
}
|
|
51
|
-
if (name === 'mod' || name === 'rem') {
|
|
52
|
-
return simplifyModRem(name, args);
|
|
53
|
-
}
|
|
54
|
-
if (name === 'round') {
|
|
55
|
-
return simplifyRound(args);
|
|
56
|
-
}
|
|
57
|
-
if (name === 'sin' || name === 'cos' || name === 'tan') {
|
|
58
|
-
return simplifyTrig(name, args);
|
|
59
|
-
}
|
|
60
|
-
if (name === 'asin' || name === 'acos' || name === 'atan') {
|
|
61
|
-
return simplifyInverseTrig(name, args);
|
|
62
|
-
}
|
|
63
|
-
if (name === 'atan2') {
|
|
64
|
-
return simplifyAtan2(args);
|
|
65
|
-
}
|
|
66
|
-
if (name === 'pow') {
|
|
67
|
-
return simplifyPow(args);
|
|
68
|
-
}
|
|
69
|
-
if (name === 'sqrt') {
|
|
70
|
-
return simplifySqrt(args);
|
|
71
|
-
}
|
|
72
|
-
if (name === 'hypot') {
|
|
73
|
-
return simplifyHypot(args);
|
|
74
|
-
}
|
|
75
|
-
if (name === 'log') {
|
|
76
|
-
return simplifyLog(args);
|
|
77
|
-
}
|
|
78
|
-
if (name === 'exp') {
|
|
79
|
-
return simplifyExp(args);
|
|
81
|
+
const simplifier = MATH_SIMPLIFIERS.get(name);
|
|
82
|
+
if (simplifier) {
|
|
83
|
+
// min/max preserve the call's original casing in their opaque-args
|
|
84
|
+
// fallback; the rest normalize to lowercase internally.
|
|
85
|
+
return simplifier(
|
|
86
|
+
name === 'min' || name === 'max' ? node.name : name,
|
|
87
|
+
args
|
|
88
|
+
);
|
|
80
89
|
}
|
|
81
90
|
|
|
82
|
-
return
|
|
91
|
+
return call(node.name, args);
|
|
83
92
|
}
|
|
84
93
|
|
|
85
|
-
export { simplifyCall };
|
|
94
|
+
export { isSupportedMathFunction, simplifyCall };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { num, dim } from '../node.js';
|
|
1
|
+
import { num, dim, call } from '../node.js';
|
|
2
2
|
import { foldConstArgs } from './fold.js';
|
|
3
3
|
|
|
4
4
|
/** @typedef {import('../node.js').Node} Node */
|
|
@@ -18,7 +18,7 @@ function simplifyClamp(args) {
|
|
|
18
18
|
return fold.unit === '' ? num(clamped) : dim(clamped, fold.unit);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
return
|
|
21
|
+
return call('clamp', args);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export { simplifyClamp };
|
package/src/lib/simplify/exp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { num } from '../node.js';
|
|
1
|
+
import { num, call } from '../node.js';
|
|
2
2
|
|
|
3
3
|
/** @typedef {import('../node.js').Node} Node */
|
|
4
4
|
|
|
@@ -8,7 +8,7 @@ import { num } from '../node.js';
|
|
|
8
8
|
*/
|
|
9
9
|
function simplifyExp(args) {
|
|
10
10
|
if (args.length !== 1 || args[0].type !== 'Num') {
|
|
11
|
-
return
|
|
11
|
+
return call('exp', args);
|
|
12
12
|
}
|
|
13
13
|
return num(Math.exp(args[0].value));
|
|
14
14
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// §10.5 — hypot. Empty args return null from foldConstArgs naturally.
|
|
2
2
|
|
|
3
|
-
import { num, dim } from '../node.js';
|
|
3
|
+
import { num, dim, call } from '../node.js';
|
|
4
4
|
import { foldConstArgs } from './fold.js';
|
|
5
5
|
|
|
6
6
|
/** @typedef {import('../node.js').Node} Node */
|
|
@@ -12,7 +12,7 @@ import { foldConstArgs } from './fold.js';
|
|
|
12
12
|
function simplifyHypot(args) {
|
|
13
13
|
const fold = foldConstArgs(args);
|
|
14
14
|
if (fold === null) {
|
|
15
|
-
return
|
|
15
|
+
return call('hypot', args);
|
|
16
16
|
}
|
|
17
17
|
const sumSq = fold.values.reduce((acc, v) => acc + v * v, 0);
|
|
18
18
|
const result = Math.sqrt(sumSq);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// §10.4 — asin/acos/atan. Bare <number> in, <angle> in degrees out.
|
|
2
2
|
|
|
3
|
-
import { num, dim } from '../node.js';
|
|
3
|
+
import { num, dim, call } from '../node.js';
|
|
4
4
|
|
|
5
5
|
/** @typedef {import('../node.js').Node} Node */
|
|
6
6
|
|
|
@@ -17,15 +17,15 @@ const INVERSE_TRIG_OPS = /** @type {const} */ ({
|
|
|
17
17
|
*/
|
|
18
18
|
function simplifyInverseTrig(name, args) {
|
|
19
19
|
if (args.length !== 1) {
|
|
20
|
-
return
|
|
20
|
+
return call(name, args);
|
|
21
21
|
}
|
|
22
22
|
const a = args[0];
|
|
23
23
|
if (a.type !== 'Num') {
|
|
24
|
-
return
|
|
24
|
+
return call(name, args);
|
|
25
25
|
}
|
|
26
26
|
const radians = INVERSE_TRIG_OPS[name](a.value);
|
|
27
|
-
if (isNaN(radians)) {
|
|
28
|
-
return num(NaN);
|
|
27
|
+
if (Number.isNaN(radians)) {
|
|
28
|
+
return num(Number.NaN);
|
|
29
29
|
}
|
|
30
30
|
return dim((radians * 180) / Math.PI, 'deg');
|
|
31
31
|
}
|
package/src/lib/simplify/log.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { num } from '../node.js';
|
|
1
|
+
import { num, call } from '../node.js';
|
|
2
2
|
|
|
3
3
|
/** @typedef {import('../node.js').Node} Node */
|
|
4
4
|
|
|
@@ -13,7 +13,7 @@ function simplifyLog(args) {
|
|
|
13
13
|
if (args.length === 2 && args[0].type === 'Num' && args[1].type === 'Num') {
|
|
14
14
|
return num(Math.log(args[0].value) / Math.log(args[1].value));
|
|
15
15
|
}
|
|
16
|
-
return
|
|
16
|
+
return call('log', args);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export { simplifyLog };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { num, dim } from '../node.js';
|
|
1
|
+
import { num, dim, call } from '../node.js';
|
|
2
2
|
import { foldConstArgs } from './fold.js';
|
|
3
3
|
|
|
4
4
|
/** @typedef {import('../node.js').Node} Node */
|
|
@@ -15,7 +15,7 @@ function simplifyMinMax(name, args) {
|
|
|
15
15
|
const value = fn(...fold.values);
|
|
16
16
|
return fold.unit === '' ? num(value) : dim(value, fold.unit);
|
|
17
17
|
}
|
|
18
|
-
return
|
|
18
|
+
return call(name, args);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export { simplifyMinMax };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { num, dim } from '../node.js';
|
|
1
|
+
import { num, dim, call } from '../node.js';
|
|
2
2
|
import { foldConstArgs } from './fold.js';
|
|
3
3
|
|
|
4
4
|
/** @typedef {import('../node.js').Node} Node */
|
|
@@ -10,18 +10,18 @@ import { foldConstArgs } from './fold.js';
|
|
|
10
10
|
*/
|
|
11
11
|
function simplifyModRem(name, args) {
|
|
12
12
|
if (args.length !== 2) {
|
|
13
|
-
return
|
|
13
|
+
return call(name, args);
|
|
14
14
|
}
|
|
15
15
|
const fold = foldConstArgs(args);
|
|
16
16
|
if (fold === null) {
|
|
17
|
-
return
|
|
17
|
+
return call(name, args);
|
|
18
18
|
}
|
|
19
19
|
const [a, b] = /** @type {[number, number]} */ (fold.values);
|
|
20
20
|
const result = applyModRem(name, a, b);
|
|
21
21
|
// NaN results drop the unit (`mod(5px, 0px)` → `calc(NaN)`, not
|
|
22
22
|
// `calc(NaN * 1px)`). §10.12 unit-preserving form is a known divergence.
|
|
23
|
-
if (isNaN(result)) {
|
|
24
|
-
return num(NaN);
|
|
23
|
+
if (Number.isNaN(result)) {
|
|
24
|
+
return num(Number.NaN);
|
|
25
25
|
}
|
|
26
26
|
return fold.unit === '' ? num(result) : dim(result, fold.unit);
|
|
27
27
|
}
|
|
@@ -34,16 +34,16 @@ function simplifyModRem(name, args) {
|
|
|
34
34
|
*/
|
|
35
35
|
function applyModRem(name, a, b) {
|
|
36
36
|
if (b === 0) {
|
|
37
|
-
return NaN;
|
|
37
|
+
return Number.NaN;
|
|
38
38
|
}
|
|
39
|
-
if (!isFinite(a)) {
|
|
40
|
-
return NaN;
|
|
39
|
+
if (!Number.isFinite(a)) {
|
|
40
|
+
return Number.NaN;
|
|
41
41
|
}
|
|
42
|
-
if (!isFinite(b)) {
|
|
42
|
+
if (!Number.isFinite(b)) {
|
|
43
43
|
// mod: result is NaN when A has opposite sign to B; otherwise A.
|
|
44
44
|
// rem: result is A regardless of signs.
|
|
45
45
|
if (name === 'mod' && a !== 0 && Math.sign(a) !== Math.sign(b)) {
|
|
46
|
-
return NaN;
|
|
46
|
+
return Number.NaN;
|
|
47
47
|
}
|
|
48
48
|
return a;
|
|
49
49
|
}
|
package/src/lib/simplify/pow.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// §10.5 — pow is <number>-only.
|
|
2
2
|
|
|
3
|
-
import { num } from '../node.js';
|
|
3
|
+
import { num, call } from '../node.js';
|
|
4
4
|
|
|
5
5
|
/** @typedef {import('../node.js').Node} Node */
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ import { num } from '../node.js';
|
|
|
10
10
|
*/
|
|
11
11
|
function simplifyPow(args) {
|
|
12
12
|
if (args.length !== 2 || args[0].type !== 'Num' || args[1].type !== 'Num') {
|
|
13
|
-
return
|
|
13
|
+
return call('pow', args);
|
|
14
14
|
}
|
|
15
15
|
return num(Math.pow(args[0].value, args[1].value));
|
|
16
16
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { num, dim } from '../node.js';
|
|
1
|
+
import { num, dim, ident, call } from '../node.js';
|
|
2
2
|
import { foldConstArgs } from './fold.js';
|
|
3
3
|
|
|
4
4
|
/** @typedef {import('../node.js').Node} Node */
|
|
@@ -18,21 +18,15 @@ function simplifyRound(args) {
|
|
|
18
18
|
const n = first.name.toLowerCase();
|
|
19
19
|
if (!ROUND_STRATEGIES.has(n)) {
|
|
20
20
|
// Unrecognized strategy ident — opaque rather than guessing intent.
|
|
21
|
-
return
|
|
21
|
+
return call('round', args);
|
|
22
22
|
}
|
|
23
23
|
strategy = /** @type {RoundStrategy} */ (n);
|
|
24
24
|
rest = args.slice(1);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/** @type {() => Node} */
|
|
28
|
-
const passthrough = () =>
|
|
29
|
-
|
|
30
|
-
name: 'round',
|
|
31
|
-
args:
|
|
32
|
-
strategy === 'nearest'
|
|
33
|
-
? rest
|
|
34
|
-
: [{ type: 'Ident', name: strategy }, ...rest],
|
|
35
|
-
});
|
|
28
|
+
const passthrough = () =>
|
|
29
|
+
call('round', strategy === 'nearest' ? rest : [ident(strategy), ...rest]);
|
|
36
30
|
|
|
37
31
|
// B omitted: defaults to 1 when A is <number>; else opaque.
|
|
38
32
|
const argsForFold = argsForRoundFold(rest);
|
|
@@ -49,12 +43,12 @@ function simplifyRound(args) {
|
|
|
49
43
|
// case folds to ±0 carrying A's sign. Infinite-A / finite-B falls through
|
|
50
44
|
// to applyRound, where floor*b===ceil*b===±∞ collapses back to A
|
|
51
45
|
// (§10.3.1 "result is the same infinity").
|
|
52
|
-
if (isNaN(b)) {
|
|
53
|
-
return num(NaN);
|
|
46
|
+
if (Number.isNaN(b)) {
|
|
47
|
+
return num(Number.NaN);
|
|
54
48
|
}
|
|
55
|
-
if (!isFinite(b)) {
|
|
56
|
-
if (!isFinite(a)) {
|
|
57
|
-
return num(NaN);
|
|
49
|
+
if (!Number.isFinite(b)) {
|
|
50
|
+
if (!Number.isFinite(a)) {
|
|
51
|
+
return num(Number.NaN);
|
|
58
52
|
}
|
|
59
53
|
let result;
|
|
60
54
|
if (strategy === 'up' && a > 0) {
|
|
@@ -68,8 +62,8 @@ function simplifyRound(args) {
|
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
const result = applyRound(strategy, a, b);
|
|
71
|
-
if (isNaN(result)) {
|
|
72
|
-
return num(NaN);
|
|
65
|
+
if (Number.isNaN(result)) {
|
|
66
|
+
return num(Number.NaN);
|
|
73
67
|
}
|
|
74
68
|
return fold.unit === '' ? num(result) : dim(result, fold.unit);
|
|
75
69
|
}
|
|
@@ -96,7 +90,7 @@ function argsForRoundFold(args) {
|
|
|
96
90
|
*/
|
|
97
91
|
function applyRound(strategy, a, b) {
|
|
98
92
|
if (b === 0) {
|
|
99
|
-
return NaN;
|
|
93
|
+
return Number.NaN;
|
|
100
94
|
}
|
|
101
95
|
const q = a / b;
|
|
102
96
|
const c1 = Math.floor(q) * b;
|
package/src/lib/simplify/sign.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { num } from '../node.js';
|
|
1
|
+
import { num, call } from '../node.js';
|
|
2
2
|
|
|
3
3
|
/** @typedef {import('../node.js').Node} Node */
|
|
4
4
|
|
|
@@ -8,7 +8,7 @@ import { num } from '../node.js';
|
|
|
8
8
|
*/
|
|
9
9
|
function simplifySign(args) {
|
|
10
10
|
if (args.length !== 1) {
|
|
11
|
-
return
|
|
11
|
+
return call('sign', args);
|
|
12
12
|
}
|
|
13
13
|
const a = args[0];
|
|
14
14
|
if (a.type === 'Num') {
|
|
@@ -18,7 +18,7 @@ function simplifySign(args) {
|
|
|
18
18
|
if (a.type === 'Dim' && a.unit !== '%') {
|
|
19
19
|
return num(Math.sign(a.value));
|
|
20
20
|
}
|
|
21
|
-
return
|
|
21
|
+
return call('sign', [a]);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export { simplifySign };
|
package/src/lib/simplify/sqrt.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { num } from '../node.js';
|
|
1
|
+
import { num, call } from '../node.js';
|
|
2
2
|
|
|
3
3
|
/** @typedef {import('../node.js').Node} Node */
|
|
4
4
|
/**
|
|
@@ -7,7 +7,7 @@ import { num } from '../node.js';
|
|
|
7
7
|
*/
|
|
8
8
|
function simplifySqrt(args) {
|
|
9
9
|
if (args.length !== 1 || args[0].type !== 'Num') {
|
|
10
|
-
return
|
|
10
|
+
return call('sqrt', args);
|
|
11
11
|
}
|
|
12
12
|
return num(Math.sqrt(args[0].value));
|
|
13
13
|
}
|
package/src/lib/simplify/sum.js
CHANGED
|
@@ -48,6 +48,17 @@ function simplifySum(sum, simplify) {
|
|
|
48
48
|
*/
|
|
49
49
|
function processTerm(sign, n) {
|
|
50
50
|
if (n.type === 'Sum') {
|
|
51
|
+
// Parentheses around a sum are normally algebraically disposable. Keep
|
|
52
|
+
// them only for an opaque sum used negatively: distributing that sign
|
|
53
|
+
// changes the CSS value when a var() contains a sum of its own.
|
|
54
|
+
if (
|
|
55
|
+
n.grouped &&
|
|
56
|
+
sign === -1 &&
|
|
57
|
+
n.terms.some((t) => t.node.type !== 'Num' && t.node.type !== 'Dim')
|
|
58
|
+
) {
|
|
59
|
+
opaque.push({ sign, node: n });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
51
62
|
for (const inner of n.terms) {
|
|
52
63
|
processTerm(/** @type {1 | -1} */ (sign * inner.sign), inner.node);
|
|
53
64
|
}
|
|
@@ -95,7 +106,13 @@ function simplifySum(sum, simplify) {
|
|
|
95
106
|
}
|
|
96
107
|
terms.push(...opaque);
|
|
97
108
|
|
|
98
|
-
|
|
109
|
+
const result = mkSum(terms);
|
|
110
|
+
// Keep the source grouping available to an enclosing subtraction. Numeric
|
|
111
|
+
// groups have already folded and therefore do not need the marker.
|
|
112
|
+
if (sum.grouped && result.type === 'Sum' && opaque.length > 0) {
|
|
113
|
+
return { ...result, grouped: true };
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
99
116
|
}
|
|
100
117
|
|
|
101
118
|
export { simplifySum };
|
package/src/lib/simplify/trig.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// §10.4 — sin/cos/tan. <number> is radians; <angle> dim is converted.
|
|
2
2
|
|
|
3
|
-
import { num } from '../node.js';
|
|
3
|
+
import { num, call } from '../node.js';
|
|
4
4
|
import { baseOf, convert } from '../convertUnits.js';
|
|
5
5
|
|
|
6
6
|
/** @typedef {import('../node.js').Node} Node */
|
|
@@ -18,7 +18,7 @@ const TRIG_OPS = /** @type {const} */ ({
|
|
|
18
18
|
*/
|
|
19
19
|
function simplifyTrig(name, args) {
|
|
20
20
|
if (args.length !== 1) {
|
|
21
|
-
return
|
|
21
|
+
return call(name, args);
|
|
22
22
|
}
|
|
23
23
|
const a = args[0];
|
|
24
24
|
/** @type {number | null} */ let radians = null;
|
|
@@ -36,7 +36,7 @@ function simplifyTrig(name, args) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
if (radians === null) {
|
|
39
|
-
return
|
|
39
|
+
return call(name, args);
|
|
40
40
|
}
|
|
41
41
|
return num(TRIG_OPS[name](radians));
|
|
42
42
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
export type
|
|
1
|
+
export type PostCssCalcOptions = {
|
|
2
2
|
precision?: number | false;
|
|
3
|
-
preserve?: boolean;
|
|
4
3
|
warnWhenCannotResolve?: boolean;
|
|
5
4
|
mediaQueries?: boolean;
|
|
6
5
|
selectors?: boolean;
|
|
@@ -9,15 +8,21 @@ export type PluginOptions = {
|
|
|
9
8
|
*/
|
|
10
9
|
onParseError?: (error: Error, input: string) => void;
|
|
11
10
|
};
|
|
12
|
-
export type ResolvedOptions = Required<Omit<
|
|
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
|
+
};
|
|
13
18
|
/**
|
|
14
|
-
* @param {
|
|
19
|
+
* @param {PostCssCalcOptions} [opts]
|
|
15
20
|
* @return {import('postcss').Plugin}
|
|
16
21
|
*/
|
|
17
|
-
declare function pluginCreator(opts?:
|
|
22
|
+
declare function pluginCreator(opts?: PostCssCalcOptions): import('postcss').Plugin;
|
|
18
23
|
declare namespace pluginCreator {
|
|
19
|
-
var postcss:
|
|
24
|
+
var postcss: true;
|
|
20
25
|
}
|
|
21
|
-
declare const _default: import('postcss').PluginCreator<
|
|
26
|
+
declare const _default: import('postcss').PluginCreator<PostCssCalcOptions>;
|
|
22
27
|
export default _default;
|
|
23
28
|
export { pluginCreator as 'module.exports' };
|
package/types/lib/node.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type SumTerm = {
|
|
|
23
23
|
export type Sum = {
|
|
24
24
|
type: 'Sum';
|
|
25
25
|
terms: SumTerm[];
|
|
26
|
+
grouped?: boolean;
|
|
26
27
|
};
|
|
27
28
|
export type ProductFactor = {
|
|
28
29
|
exponent: 1 | -1;
|
|
@@ -39,7 +40,7 @@ export type Node = Num | Dim | Ident | Call | Sum | Product;
|
|
|
39
40
|
* @typedef {{type: 'Ident', name: string}} Ident
|
|
40
41
|
* @typedef {{type: 'Call', name: string, args: Node[]}} Call
|
|
41
42
|
* @typedef {{sign: 1 | -1, node: Node}} SumTerm Sign is always +1 when node is Num or Dim.
|
|
42
|
-
* @typedef {{type: 'Sum', terms: SumTerm[]}} Sum
|
|
43
|
+
* @typedef {{type: 'Sum', terms: SumTerm[], grouped?: boolean}} Sum
|
|
43
44
|
* @typedef {{exponent: 1 | -1, node: Node}} ProductFactor exponent +1 = numerator, -1 = denominator.
|
|
44
45
|
* @typedef {{type: 'Product', factors: ProductFactor[]}} Product
|
|
45
46
|
* @typedef {Num | Dim | Ident | Call | Sum | Product} Node
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
export type Node = import('../node.js').Node;
|
|
2
2
|
export type SimplifyFn = import('../simplify.js').SimplifyFn;
|
|
3
|
-
|
|
4
|
-
/**
|
|
3
|
+
export type MathSimplifier = (name: string, args: Node[]) => Node;
|
|
4
|
+
/**
|
|
5
|
+
* Whether a bare CSS math function has an implemented simplifier.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} name
|
|
8
|
+
* @return {boolean}
|
|
9
|
+
*/
|
|
10
|
+
declare function isSupportedMathFunction(name: string): boolean;
|
|
5
11
|
/**
|
|
6
12
|
* @param {Extract<Node, { type: 'Call' }>} node
|
|
7
13
|
* @param {SimplifyFn} simplify
|
|
@@ -10,4 +16,4 @@ export type SimplifyFn = import('../simplify.js').SimplifyFn;
|
|
|
10
16
|
declare function simplifyCall(node: Extract<Node, {
|
|
11
17
|
type: 'Call';
|
|
12
18
|
}>, simplify: SimplifyFn): Node;
|
|
13
|
-
export { simplifyCall };
|
|
19
|
+
export { isSupportedMathFunction, simplifyCall };
|
package/types/lib/type.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
export type BaseType =
|
|
1
|
+
export type BaseType =
|
|
2
|
+
| 'length'
|
|
3
|
+
| 'angle'
|
|
4
|
+
| 'time'
|
|
5
|
+
| 'frequency'
|
|
6
|
+
| 'resolution'
|
|
7
|
+
| 'flex'
|
|
8
|
+
| 'percentage';
|
|
2
9
|
/**
|
|
3
10
|
* @param {string} unit
|
|
4
11
|
* @return {BaseType | null}
|