postcss-calc 11.2.1 → 11.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -0
- package/README.md +47 -35
- package/package.json +15 -15
- package/src/index.js +2 -1
- package/src/lib/analyze.js +0 -6
- package/src/lib/compile.js +1 -1
- package/src/lib/function-analyzers.js +119 -0
- package/src/lib/functions.js +25 -175
- package/src/lib/node.js +61 -4
- package/src/lib/parser/opaque.js +152 -0
- package/src/lib/parser/tokens.js +242 -0
- package/src/lib/parser.js +32 -377
- package/src/lib/serialize/expression.js +329 -0
- package/src/lib/serialize/precedence.js +47 -0
- package/src/lib/serialize/precision.js +244 -0
- package/src/lib/serialize.js +24 -561
- package/src/lib/simplify/bucket.js +22 -5
- package/src/lib/simplify/cancel.js +35 -11
- package/src/lib/simplify/clamp.js +2 -2
- package/src/lib/simplify/exact.js +18 -0
- package/src/lib/simplify/fold.js +15 -1
- package/src/lib/simplify/min-max.js +2 -2
- package/src/lib/simplify/product-barrier.js +55 -0
- package/src/lib/simplify/product-quotient.js +141 -0
- package/src/lib/simplify/product.js +92 -48
- package/src/lib/simplify/sum.js +6 -2
- package/src/lib/simplify.js +22 -5
- package/src/lib/types.js +94 -0
- package/src/reduce.js +2 -1
- package/types/lib/function-analyzers.d.ts +27 -0
- package/types/lib/functions.d.ts +3 -28
- package/types/lib/node.d.ts +17 -2
- package/types/lib/parser/opaque.d.ts +12 -0
- package/types/lib/parser/tokens.d.ts +83 -0
- package/types/lib/parser.d.ts +4 -44
- package/types/lib/serialize/expression.d.ts +55 -0
- package/types/lib/serialize/precedence.d.ts +18 -0
- package/types/lib/serialize/precision.d.ts +102 -0
- package/types/lib/simplify/bucket.d.ts +4 -1
- package/types/lib/simplify/cancel.d.ts +11 -7
- package/types/lib/simplify/exact.d.ts +9 -0
- package/types/lib/simplify/fold.d.ts +14 -1
- package/types/lib/simplify/product-barrier.d.ts +26 -0
- package/types/lib/simplify/product-quotient.d.ts +48 -0
- package/types/lib/simplify/product.d.ts +3 -7
- package/types/lib/simplify/sum.d.ts +2 -1
- package/types/lib/simplify.d.ts +3 -8
- package/types/lib/types.d.ts +48 -0
- package/types/lib/calculation-type.d.ts +0 -18
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
[![Support Chat][git-img]][git-url]
|
|
5
5
|
|
|
6
6
|
[PostCSS Calc] lets you reduce `calc()` references whenever it's possible.
|
|
7
|
-
When
|
|
8
|
-
|
|
7
|
+
When an expression mixes units that cannot be combined exactly (such as `px`
|
|
8
|
+
and `em`), or contains values only known at runtime (such as `var()`), the
|
|
9
|
+
unresolved part is left for the browser's [CSS Values 4][css-values-4]
|
|
10
|
+
implementation.
|
|
9
11
|
|
|
10
12
|
## Installation
|
|
11
13
|
|
|
@@ -62,15 +64,14 @@ leaving all other text untouched.
|
|
|
62
64
|
import reduceCalc from 'postcss-calc/reduce';
|
|
63
65
|
|
|
64
66
|
reduceCalc('calc(1in + 10px)');
|
|
65
|
-
// => 'calc(
|
|
67
|
+
// => 'calc(106px)'
|
|
66
68
|
|
|
67
69
|
reduceCalc('min(50px, calc(2 * 40px))');
|
|
68
70
|
// => 'calc(50px)'
|
|
69
71
|
```
|
|
70
72
|
|
|
71
|
-
It accepts `precision`, `unwrapSingleValue`,
|
|
72
|
-
`
|
|
73
|
-
`warnWhenCannotResolve`, `onParseError`, and `onWarn`:
|
|
73
|
+
It accepts `precision`, `unwrapSingleValue`, `warnWhenCannotResolve`,
|
|
74
|
+
`onParseError`, and `onWarn`:
|
|
74
75
|
|
|
75
76
|
```js
|
|
76
77
|
const result = reduceCalc('calc(100% + var(--gap))', {
|
|
@@ -123,6 +124,28 @@ Allows you to define the precision for decimal numbers. Set it to `false` to
|
|
|
123
124
|
disable rounding and preserve full IEEE-754 floating-point precision (emitting
|
|
124
125
|
the shortest round-tripping decimal representation).
|
|
125
126
|
|
|
127
|
+
Values below 1 keep `precision` significant digits (`.0123456px` becomes
|
|
128
|
+
`.012346px`), and larger values keep `precision` decimals. Divisions and unit
|
|
129
|
+
conversions are folded only when the result is exact at the precision;
|
|
130
|
+
otherwise they stay symbolic and everything around them is still simplified:
|
|
131
|
+
|
|
132
|
+
```css
|
|
133
|
+
.a {
|
|
134
|
+
width: calc(100% / 4);
|
|
135
|
+
} /* calc(25%) */
|
|
136
|
+
.b {
|
|
137
|
+
width: calc(100% / 3);
|
|
138
|
+
} /* calc(100% / 3), not 33.33333% */
|
|
139
|
+
.c {
|
|
140
|
+
width: calc(1cm + 1px);
|
|
141
|
+
} /* calc(1cm + 1px) */
|
|
142
|
+
.d {
|
|
143
|
+
width: calc(1px + 1pt);
|
|
144
|
+
} /* calc(1.75pt) */
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
With `precision: false` nothing is rounded, so every division is folded.
|
|
148
|
+
|
|
126
149
|
```js
|
|
127
150
|
var out = postcss()
|
|
128
151
|
.use(calc({ precision: 10 }))
|
|
@@ -168,20 +191,20 @@ Example:
|
|
|
168
191
|
With `mediaQueries: true`, this becomes:
|
|
169
192
|
|
|
170
193
|
```css
|
|
171
|
-
@media (min-width: 200px) {
|
|
194
|
+
@media (min-width: calc(200px)) {
|
|
172
195
|
div {
|
|
173
196
|
width: 100px;
|
|
174
197
|
}
|
|
175
198
|
}
|
|
176
199
|
```
|
|
177
200
|
|
|
201
|
+
Add `unwrapSingleValue: true` to get the bare value, `(min-width: 200px)`.
|
|
202
|
+
|
|
178
203
|
#### `selectors` (default: `false`)
|
|
179
204
|
|
|
180
205
|
Reduces `calc()` functions found in selectors. Selectors do not accept
|
|
181
|
-
`calc()` functions, so the plugin replaces them with their reduced values
|
|
182
|
-
|
|
183
|
-
because a selector cannot contain a `calc()` function; the plugin enables the
|
|
184
|
-
`unwrapSingleValue` automatically for selectors.
|
|
206
|
+
`calc()` functions, so the plugin replaces them with their reduced values, as
|
|
207
|
+
if `unwrapSingleValue` were enabled.
|
|
185
208
|
|
|
186
209
|
```js
|
|
187
210
|
var out = postcss()
|
|
@@ -201,8 +224,8 @@ With `selectors: true`, this becomes `div:nth-child(3)`.
|
|
|
201
224
|
|
|
202
225
|
#### `onParseError`
|
|
203
226
|
|
|
204
|
-
Callback invoked when a `calc()` body fails
|
|
205
|
-
|
|
227
|
+
Callback invoked with the error and the input value when a `calc()` body fails
|
|
228
|
+
to parse or simplify:
|
|
206
229
|
|
|
207
230
|
```js
|
|
208
231
|
postcss().use(
|
|
@@ -227,23 +250,18 @@ canonical-form decisions:
|
|
|
227
250
|
- **Strict whitespace (§10.1).** `calc(2px+3px)` is invalid CSS (binary
|
|
228
251
|
`+` / `-` require surrounding whitespace) and is preserved with a
|
|
229
252
|
warning instead of reduced.
|
|
230
|
-
- **Canonical operand order.**
|
|
231
|
-
|
|
253
|
+
- **Canonical operand order.** Sums serialize numbers first, then
|
|
254
|
+
dimensions in first-seen order, then unresolved terms:
|
|
232
255
|
`calc(var(--foo) + 10px)` → `calc(10px + var(--foo))`.
|
|
233
256
|
- **Zero buckets are kept.** `calc(100px - (100px - 100%))` →
|
|
234
257
|
`calc(0px + 100%)`, not `100%` — [WPT calc-serialization-002][wpt-calc-serialization]
|
|
235
258
|
requires the zero term because it carries the length-percentage type.
|
|
236
|
-
- **Constant folding.** `calc(43 + pi)` now folds to `46.14159` (§10.7.1).
|
|
259
|
+
- **Constant folding.** `calc(43 + pi)` now folds to `calc(46.14159)` (§10.7.1).
|
|
237
260
|
Previously `pi` / `e` stayed symbolic.
|
|
238
|
-
- **Reciprocal conversion.** `calc(var(--x) / 2)` becomes
|
|
239
|
-
`calc(var(--x) * 0.5)`. The two are mathematically equivalent;
|
|
240
|
-
previously the division shape was kept.
|
|
241
261
|
- **Distributive multiplication.** `calc(0.5 * (100vw - 10px))` becomes
|
|
242
262
|
`calc(50vw - 5px)`.
|
|
243
263
|
- **Unit case normalization.** `2PX` becomes `2px` (CSS units are case-
|
|
244
264
|
insensitive; lowercase is conventional).
|
|
245
|
-
- **Calc unwrap (§10.6).** `calc(var(--foo))` becomes `var(--foo)` — a
|
|
246
|
-
`calc()` containing a single value is replaced by that value.
|
|
247
265
|
- **Spec-style spaced operators.** `2px*var(--x)` is serialized as
|
|
248
266
|
`2px * var(--x)`. The tokenizer is unaffected; only output spacing
|
|
249
267
|
differs.
|
|
@@ -252,7 +270,6 @@ canonical-form decisions:
|
|
|
252
270
|
if you want validation behavior.
|
|
253
271
|
|
|
254
272
|
[css-values-4]: https://www.w3.org/TR/css-values-4/
|
|
255
|
-
[csstools-css-calc]: https://www.npmjs.com/package/@csstools/css-calc
|
|
256
273
|
[wpt-calc-serialization]: https://github.com/web-platform-tests/wpt/blob/master/css/css-values/calc-serialization-002.html
|
|
257
274
|
[jison]: https://github.com/zaach/jison
|
|
258
275
|
|
|
@@ -265,13 +282,16 @@ To replace the value of CSS custom properties at build time, try [PostCSS Custom
|
|
|
265
282
|
## Contributing
|
|
266
283
|
|
|
267
284
|
Work on a branch, install dev-dependencies, respect coding style & run tests
|
|
268
|
-
before submitting a bug fix or a feature.
|
|
285
|
+
before submitting a bug fix or a feature. See [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
286
|
+
for the full guidelines. The project uses [pnpm](https://pnpm.io/).
|
|
269
287
|
|
|
270
288
|
```bash
|
|
271
289
|
git clone git@github.com:postcss/postcss-calc.git
|
|
290
|
+
cd postcss-calc
|
|
272
291
|
git checkout -b patch-1
|
|
273
|
-
|
|
274
|
-
|
|
292
|
+
pnpm install
|
|
293
|
+
pnpm test
|
|
294
|
+
pnpm lint
|
|
275
295
|
```
|
|
276
296
|
|
|
277
297
|
The normal test run uses a deterministic structural sample of the harvested
|
|
@@ -282,15 +302,8 @@ when changing parsing/simplification behavior:
|
|
|
282
302
|
pnpm test:corpus:full
|
|
283
303
|
```
|
|
284
304
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
and write ignored schema-v2 reports. Compare a saved report with
|
|
288
|
-
`node scripts/compare-parser-benchmarks.js <report>`. Run the correctness-aware
|
|
289
|
-
corpus benchmark with `pnpm benchmark:corpus`.
|
|
290
|
-
|
|
291
|
-
The PostCSS benchmark awaits `postcss().process(...)`, and that await already
|
|
292
|
-
triggers result stringification. It therefore does not add a redundant
|
|
293
|
-
`result.css` access.
|
|
305
|
+
Check performance changes with the benchmarks described in
|
|
306
|
+
[BENCHMARKS.md](BENCHMARKS.md), and read it before interpreting results.
|
|
294
307
|
|
|
295
308
|
## [Changelog](CHANGELOG.md)
|
|
296
309
|
|
|
@@ -304,4 +317,3 @@ triggers result stringification. It therefore does not add a redundant
|
|
|
304
317
|
[PostCSS Calc]: https://github.com/postcss/postcss-calc
|
|
305
318
|
[PostCSS Custom Properties]: https://github.com/postcss/postcss-custom-properties
|
|
306
319
|
[tests]: test/
|
|
307
|
-
[W3C calc() implementation]: https://www.w3.org/TR/css3-values/#calc-notation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postcss-calc",
|
|
3
|
-
"version": "11.2.
|
|
3
|
+
"version": "11.2.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "PostCSS plugin to reduce calc()",
|
|
6
6
|
"keywords": [
|
|
@@ -38,34 +38,34 @@
|
|
|
38
38
|
"devEngines": {
|
|
39
39
|
"packageManager": {
|
|
40
40
|
"name": "pnpm",
|
|
41
|
-
"version": "12.
|
|
41
|
+
"version": "12.10.1"
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@csstools/css-calc": "^3.4.
|
|
46
|
-
"@types/node": "^26.6.
|
|
47
|
-
"fast-check": "^4.10.
|
|
48
|
-
"oxfmt": "^0.
|
|
49
|
-
"oxlint": "^1.
|
|
50
|
-
"postcss": "^8.5.
|
|
45
|
+
"@csstools/css-calc": "^3.4.3",
|
|
46
|
+
"@types/node": "^26.6.4",
|
|
47
|
+
"fast-check": "^4.10.2",
|
|
48
|
+
"oxfmt": "^0.72.0",
|
|
49
|
+
"oxlint": "^1.87.0",
|
|
50
|
+
"postcss": "^8.5.29",
|
|
51
51
|
"typescript": "~7.0.2"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@csstools/css-tokenizer": "^4.0.
|
|
54
|
+
"@csstools/css-tokenizer": "^4.0.2"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"postcss": "^8.5.
|
|
57
|
+
"postcss": "^8.5.29"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"lint": "oxlint . && tsc && oxfmt --check",
|
|
61
61
|
"fmt": "oxfmt",
|
|
62
|
-
"benchmark:arithmetic-chains": "node scripts/benchmark-arithmetic-chains
|
|
63
|
-
"benchmark:nested-fallbacks": "node scripts/benchmark-nested-fallbacks
|
|
64
|
-
"benchmark:corpus": "node scripts/benchmark.js",
|
|
65
|
-
"benchmark:serialization": "node scripts/benchmark-serialization.js",
|
|
62
|
+
"benchmark:arithmetic-chains": "node scripts/benchmark/benchmark-parser.js arithmetic-chains",
|
|
63
|
+
"benchmark:nested-fallbacks": "node scripts/benchmark/benchmark-parser.js nested-fallbacks",
|
|
64
|
+
"benchmark:corpus": "node scripts/benchmark/benchmark-corpus.js",
|
|
65
|
+
"benchmark:serialization": "node scripts/benchmark/benchmark-serialization.js",
|
|
66
66
|
"test:benchmark": "node --test 'test/unit/benchmark-*.test.js' test/unit/compare-parser-benchmarks.test.js test/unit/corpus-benchmark.test.js",
|
|
67
67
|
"test:benchmark:simulation": "node test/benchmark/statistical-simulation.js",
|
|
68
|
-
"benchmark:reanalyze": "node scripts/compare-parser-benchmarks.js",
|
|
68
|
+
"benchmark:reanalyze": "node scripts/benchmark/compare-parser-benchmarks.js",
|
|
69
69
|
"test": "node --test --test-reporter=dot 'test/**/*.test.js' 'test/**/*.test.cjs'",
|
|
70
70
|
"test:mutation:corpus": "node test/mutation/corpus-selection.js",
|
|
71
71
|
"test:corpus:full": "POSTCSS_CALC_FULL_CORPUS=1 node --test test/conformance/corpus.test.js"
|
package/src/index.js
CHANGED
|
@@ -65,12 +65,13 @@ function applyTransform(
|
|
|
65
65
|
function pluginCreator(opts) {
|
|
66
66
|
/** @type {ResolvedOptions} */
|
|
67
67
|
const options = {
|
|
68
|
-
precision: 5,
|
|
69
68
|
warnWhenCannotResolve: false,
|
|
70
69
|
mediaQueries: false,
|
|
71
70
|
selectors: false,
|
|
72
71
|
unwrapSingleValue: false,
|
|
73
72
|
...opts,
|
|
73
|
+
// An explicit `precision: undefined` keeps the default.
|
|
74
|
+
precision: opts?.precision ?? 5,
|
|
74
75
|
};
|
|
75
76
|
|
|
76
77
|
return {
|
package/src/lib/analyze.js
CHANGED
|
@@ -50,9 +50,6 @@ function analyzeType(node, depth = 0) {
|
|
|
50
50
|
case 'Num':
|
|
51
51
|
return resolved(numberType);
|
|
52
52
|
case 'Dim':
|
|
53
|
-
// Percentages are contextual; their percent-ness is tracked so a
|
|
54
|
-
// `% / %` product cancels to a number. Unknown units are opaque, while
|
|
55
|
-
// known families can still reject px + seconds.
|
|
56
53
|
return node.unit === '%'
|
|
57
54
|
? finish(percentageType, true, true)
|
|
58
55
|
: resolved({ kind: 'dimension', base: baseOf(node.unit) });
|
|
@@ -83,9 +80,6 @@ function analyzeSum(node, depth) {
|
|
|
83
80
|
if (isFailure(child.type)) {
|
|
84
81
|
type = failureType;
|
|
85
82
|
} else if (child.type.kind === 'unknown') {
|
|
86
|
-
// A pure percentage sum stays percentage-typed so a surrounding
|
|
87
|
-
// product can cancel `% / %`; any other opaque term must widen the
|
|
88
|
-
// sum back to unknown.
|
|
89
83
|
if (isPercentage(child.type)) hasPercentage = true;
|
|
90
84
|
else hasUnknown = true;
|
|
91
85
|
} else if (type === null) {
|
package/src/lib/compile.js
CHANGED
|
@@ -34,7 +34,7 @@ function compileCandidate(candidate, ctx) {
|
|
|
34
34
|
if (!analysis.valid) {
|
|
35
35
|
throw new Error('Invalid CSS calculation type');
|
|
36
36
|
}
|
|
37
|
-
const tree = simplify(parsed);
|
|
37
|
+
const tree = simplify(parsed, ctx.options.precision);
|
|
38
38
|
const original =
|
|
39
39
|
analysis.unresolved && !candidate.calculation
|
|
40
40
|
? ctx.value.slice(candidate.start, candidate.end)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { ROUND_STRATEGIES } from './simplify/round.js';
|
|
2
|
+
import {
|
|
3
|
+
addTypes,
|
|
4
|
+
failureType,
|
|
5
|
+
isFailure,
|
|
6
|
+
matchingArguments,
|
|
7
|
+
numberType,
|
|
8
|
+
unknownType,
|
|
9
|
+
} from './types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {import('./node.js').Node} Node
|
|
13
|
+
* @typedef {import('./types.js').CalculationType} CalculationType
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
17
|
+
function analyzeTrig(args) {
|
|
18
|
+
return args.length === 1 &&
|
|
19
|
+
(args[0].kind === 'unknown' ||
|
|
20
|
+
args[0].kind === 'number' ||
|
|
21
|
+
(args[0].kind === 'dimension' && args[0].base === 'angle'))
|
|
22
|
+
? numberType
|
|
23
|
+
: failureType;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
27
|
+
function analyzeInverseTrig(args) {
|
|
28
|
+
if (args.length !== 1 || args[0].kind === 'dimension') {
|
|
29
|
+
return failureType;
|
|
30
|
+
}
|
|
31
|
+
return args[0].kind === 'unknown'
|
|
32
|
+
? unknownType
|
|
33
|
+
: { kind: 'dimension', base: 'angle' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
37
|
+
function analyzeIdentity(args) {
|
|
38
|
+
return args.length === 1 ? args[0] : failureType;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
42
|
+
function analyzeSign(args) {
|
|
43
|
+
return args.length === 1 ? numberType : failureType;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @param {CalculationType[]} args @param {Node[]} nodes @return {CalculationType} */
|
|
47
|
+
function analyzeRound(args, nodes) {
|
|
48
|
+
const strategy = nodes[0];
|
|
49
|
+
const hasStrategy =
|
|
50
|
+
strategy?.type === 'Ident' &&
|
|
51
|
+
ROUND_STRATEGIES.has(strategy.name.toLowerCase());
|
|
52
|
+
const start = hasStrategy ? 1 : 0;
|
|
53
|
+
const valueCount = args.length - start;
|
|
54
|
+
if (valueCount === 1) {
|
|
55
|
+
const value = args[start];
|
|
56
|
+
if (value.kind === 'dimension') return failureType;
|
|
57
|
+
if (value.kind === 'unknown') return unknownType;
|
|
58
|
+
return numberType;
|
|
59
|
+
}
|
|
60
|
+
if (valueCount !== 2) return failureType;
|
|
61
|
+
const type = addTypes(args[start], args[start + 1]);
|
|
62
|
+
return isFailure(type) ? failureType : type;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
66
|
+
function analyzeAtan2(args) {
|
|
67
|
+
const type = matchingArguments(args, 2, 2);
|
|
68
|
+
if (isFailure(type)) return type;
|
|
69
|
+
// The type table gives atan2() «["angle" → 1]»; an unresolved result is
|
|
70
|
+
// plain unknown, never the percentage of its arguments.
|
|
71
|
+
if (type.kind === 'unknown') return unknownType;
|
|
72
|
+
return { kind: 'dimension', base: 'angle' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
76
|
+
function analyzeCalc(args) {
|
|
77
|
+
return args.length === 1 ? args[0] : failureType;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @param {Node} node @return {boolean} */
|
|
81
|
+
function isRoundStrategy(node) {
|
|
82
|
+
return node.type === 'Ident' && ROUND_STRATEGIES.has(node.name.toLowerCase());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @param {Node} node @param {number} index @return {boolean} */
|
|
86
|
+
function isClampKeyword(node, index) {
|
|
87
|
+
return (
|
|
88
|
+
(index === 0 || index === 2) &&
|
|
89
|
+
node.type === 'Ident' &&
|
|
90
|
+
node.name.toLowerCase() === 'none'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** @param {CalculationType[]} args @param {Node[]} nodes @return {CalculationType} */
|
|
95
|
+
function analyzeClamp(args, nodes) {
|
|
96
|
+
if (args.length !== 3) return failureType;
|
|
97
|
+
let valueCount = 0;
|
|
98
|
+
/** @type {CalculationType} */ let result = failureType;
|
|
99
|
+
for (let index = 0; index < args.length; index++) {
|
|
100
|
+
if (isClampKeyword(nodes[index], index)) continue;
|
|
101
|
+
result = valueCount === 0 ? args[index] : addTypes(result, args[index]);
|
|
102
|
+
valueCount++;
|
|
103
|
+
}
|
|
104
|
+
if (valueCount < 1 || isFailure(result)) return failureType;
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
analyzeAtan2,
|
|
110
|
+
analyzeCalc,
|
|
111
|
+
analyzeClamp,
|
|
112
|
+
analyzeIdentity,
|
|
113
|
+
analyzeInverseTrig,
|
|
114
|
+
analyzeRound,
|
|
115
|
+
analyzeSign,
|
|
116
|
+
analyzeTrig,
|
|
117
|
+
isClampKeyword,
|
|
118
|
+
isRoundStrategy,
|
|
119
|
+
};
|
package/src/lib/functions.js
CHANGED
|
@@ -3,7 +3,7 @@ import { simplifyClamp } from './simplify/clamp.js';
|
|
|
3
3
|
import { simplifyAbs } from './simplify/abs.js';
|
|
4
4
|
import { simplifySign } from './simplify/sign.js';
|
|
5
5
|
import { simplifyModRem } from './simplify/mod-rem.js';
|
|
6
|
-
import {
|
|
6
|
+
import { simplifyRound } from './simplify/round.js';
|
|
7
7
|
import { simplifyTrig } from './simplify/trig.js';
|
|
8
8
|
import { simplifyInverseTrig } from './simplify/inverse-trig.js';
|
|
9
9
|
import { simplifyAtan2 } from './simplify/atan2.js';
|
|
@@ -12,186 +12,36 @@ import { simplifySqrt } from './simplify/sqrt.js';
|
|
|
12
12
|
import { simplifyExp } from './simplify/exp.js';
|
|
13
13
|
import { simplifyLog } from './simplify/log.js';
|
|
14
14
|
import { simplifyHypot } from './simplify/hypot.js';
|
|
15
|
+
import {
|
|
16
|
+
analyzeAtan2,
|
|
17
|
+
analyzeCalc,
|
|
18
|
+
analyzeClamp,
|
|
19
|
+
analyzeIdentity,
|
|
20
|
+
analyzeInverseTrig,
|
|
21
|
+
analyzeRound,
|
|
22
|
+
analyzeSign,
|
|
23
|
+
analyzeTrig,
|
|
24
|
+
isClampKeyword,
|
|
25
|
+
isRoundStrategy,
|
|
26
|
+
} from './function-analyzers.js';
|
|
27
|
+
import {
|
|
28
|
+
addTypes,
|
|
29
|
+
failureType,
|
|
30
|
+
isFailure,
|
|
31
|
+
isPercentage,
|
|
32
|
+
matchingArguments,
|
|
33
|
+
numberArguments,
|
|
34
|
+
numberType,
|
|
35
|
+
percentageType,
|
|
36
|
+
unknownType,
|
|
37
|
+
} from './types.js';
|
|
15
38
|
|
|
16
39
|
/** @typedef {import('./node.js').Node} Node */
|
|
17
|
-
/**
|
|
18
|
-
* `percent` marks a value that resolves in the same percentage context as its
|
|
19
|
-
* peers (a pure percentage). It is only produced at leaves, by abs(), and by
|
|
20
|
-
* homogeneous sums and calls — never by a product, where an unpaired
|
|
21
|
-
* percentage could no longer cancel against anything.
|
|
22
|
-
* @typedef {{kind: 'number'} | {kind: 'dimension', base: string | null} | {kind: 'unknown', percent?: true} | {kind: 'failure'}} CalculationType
|
|
23
|
-
*/
|
|
40
|
+
/** @typedef {import('./types.js').CalculationType} CalculationType */
|
|
24
41
|
/** @typedef {(name: string, args: Node[]) => Node} MathSimplifier */
|
|
25
42
|
/** @typedef {(args: CalculationType[], nodes: Node[]) => CalculationType} TypeAnalyzer */
|
|
26
43
|
/** @typedef {{analyze: TypeAnalyzer, simplify?: MathSimplifier, isKeyword?: (node: Node, index: number) => boolean, calculation?: boolean}} MathFunction */
|
|
27
44
|
|
|
28
|
-
/** @type {CalculationType} */ const numberType = { kind: 'number' };
|
|
29
|
-
/** @type {CalculationType} */ const unknownType = { kind: 'unknown' };
|
|
30
|
-
/** @type {CalculationType} */ const percentageType = {
|
|
31
|
-
kind: 'unknown',
|
|
32
|
-
percent: true,
|
|
33
|
-
};
|
|
34
|
-
/** @type {CalculationType} */ const failureType = { kind: 'failure' };
|
|
35
|
-
|
|
36
|
-
/** @param {CalculationType} type @return {boolean} */
|
|
37
|
-
function isFailure(type) {
|
|
38
|
-
return type.kind === 'failure';
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** @param {CalculationType} type @return {boolean} */
|
|
42
|
-
function isPercentage(type) {
|
|
43
|
-
return type.kind === 'unknown' && type.percent === true;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** @param {CalculationType} a @param {CalculationType} b @return {CalculationType} */
|
|
47
|
-
function addTypes(a, b) {
|
|
48
|
-
if (isFailure(a) || isFailure(b)) return failureType;
|
|
49
|
-
if (a.kind === 'unknown' || b.kind === 'unknown') {
|
|
50
|
-
// A pure percentage keeps its contextual type so a surrounding product
|
|
51
|
-
// can cancel `% / %`; mixing it with any other opaque operand loses the
|
|
52
|
-
// guarantee that it resolves in the same context as its peers. A known
|
|
53
|
-
// number + percentage sum is likewise kept unknown and valid: the spec
|
|
54
|
-
// resolves the percentage against its surrounding context, which this
|
|
55
|
-
// coarse type model does not track.
|
|
56
|
-
return isPercentage(a) && isPercentage(b) ? percentageType : unknownType;
|
|
57
|
-
}
|
|
58
|
-
if (a.kind === 'number' && b.kind === 'number') return numberType;
|
|
59
|
-
if (a.kind === 'dimension' && b.kind === 'dimension') {
|
|
60
|
-
if (a.base === null || b.base === null) return unknownType;
|
|
61
|
-
return a.base === b.base ? a : failureType;
|
|
62
|
-
}
|
|
63
|
-
return failureType;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Check an all-number function without assuming anything about an unresolved
|
|
68
|
-
* operand. A concrete dimension can never become a number, so it is still a
|
|
69
|
-
* definite error when another argument is opaque.
|
|
70
|
-
* @param {CalculationType[]} args
|
|
71
|
-
* @param {number} min
|
|
72
|
-
* @param {number} max
|
|
73
|
-
* @return {CalculationType}
|
|
74
|
-
*/
|
|
75
|
-
function numberArguments(args, min, max) {
|
|
76
|
-
if (args.length < min || args.length > max) return failureType;
|
|
77
|
-
let hasUnknown = false;
|
|
78
|
-
for (const arg of args) {
|
|
79
|
-
if (arg.kind === 'dimension') return failureType;
|
|
80
|
-
hasUnknown = hasUnknown || arg.kind === 'unknown';
|
|
81
|
-
}
|
|
82
|
-
return hasUnknown ? unknownType : numberType;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Check values that must share a calculation type. Unknown operands remain
|
|
87
|
-
* unknown: they might resolve to the concrete type required by their peers.
|
|
88
|
-
* @param {CalculationType[]} args
|
|
89
|
-
* @param {number} min
|
|
90
|
-
* @param {number} max
|
|
91
|
-
* @return {CalculationType}
|
|
92
|
-
*/
|
|
93
|
-
function matchingArguments(args, min, max) {
|
|
94
|
-
if (args.length < min || args.length > max) return failureType;
|
|
95
|
-
let result = args[0];
|
|
96
|
-
for (let index = 1; index < args.length; index++) {
|
|
97
|
-
result = addTypes(result, args[index]);
|
|
98
|
-
if (isFailure(result)) return failureType;
|
|
99
|
-
}
|
|
100
|
-
return result;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
104
|
-
function analyzeTrig(args) {
|
|
105
|
-
return args.length === 1 &&
|
|
106
|
-
(args[0].kind === 'unknown' ||
|
|
107
|
-
args[0].kind === 'number' ||
|
|
108
|
-
(args[0].kind === 'dimension' && args[0].base === 'angle'))
|
|
109
|
-
? numberType
|
|
110
|
-
: failureType;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
114
|
-
function analyzeInverseTrig(args) {
|
|
115
|
-
if (args.length !== 1 || args[0].kind === 'dimension') {
|
|
116
|
-
return failureType;
|
|
117
|
-
}
|
|
118
|
-
return args[0].kind === 'unknown'
|
|
119
|
-
? unknownType
|
|
120
|
-
: { kind: 'dimension', base: 'angle' };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
124
|
-
function analyzeIdentity(args) {
|
|
125
|
-
return args.length === 1 ? args[0] : failureType;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
129
|
-
function analyzeSign(args) {
|
|
130
|
-
return args.length === 1 ? numberType : failureType;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** @param {CalculationType[]} args @param {Node[]} nodes @return {CalculationType} */
|
|
134
|
-
function analyzeRound(args, nodes) {
|
|
135
|
-
const strategy = nodes[0];
|
|
136
|
-
const hasStrategy =
|
|
137
|
-
strategy?.type === 'Ident' &&
|
|
138
|
-
ROUND_STRATEGIES.has(strategy.name.toLowerCase());
|
|
139
|
-
const start = hasStrategy ? 1 : 0;
|
|
140
|
-
const valueCount = args.length - start;
|
|
141
|
-
if (valueCount === 1) {
|
|
142
|
-
const value = args[start];
|
|
143
|
-
if (value.kind === 'dimension') return failureType;
|
|
144
|
-
if (value.kind === 'unknown') return unknownType;
|
|
145
|
-
return numberType;
|
|
146
|
-
}
|
|
147
|
-
if (valueCount !== 2) return failureType;
|
|
148
|
-
const type = addTypes(args[start], args[start + 1]);
|
|
149
|
-
return isFailure(type) ? failureType : type;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
153
|
-
function analyzeAtan2(args) {
|
|
154
|
-
const type = matchingArguments(args, 2, 2);
|
|
155
|
-
if (isFailure(type)) return type;
|
|
156
|
-
// The type table gives atan2() «["angle" → 1]»; an unresolved result is
|
|
157
|
-
// plain unknown, never the percentage of its arguments.
|
|
158
|
-
if (type.kind === 'unknown') return unknownType;
|
|
159
|
-
return { kind: 'dimension', base: 'angle' };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
163
|
-
function analyzeCalc(args) {
|
|
164
|
-
return args.length === 1 ? args[0] : failureType;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** @param {Node} node @return {boolean} */
|
|
168
|
-
function isRoundStrategy(node) {
|
|
169
|
-
return node.type === 'Ident' && ROUND_STRATEGIES.has(node.name.toLowerCase());
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** @param {Node} node @param {number} index @return {boolean} */
|
|
173
|
-
function isClampKeyword(node, index) {
|
|
174
|
-
return (
|
|
175
|
-
(index === 0 || index === 2) &&
|
|
176
|
-
node.type === 'Ident' &&
|
|
177
|
-
node.name.toLowerCase() === 'none'
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/** @param {CalculationType[]} args @param {Node[]} nodes @return {CalculationType} */
|
|
182
|
-
function analyzeClamp(args, nodes) {
|
|
183
|
-
if (args.length !== 3) return failureType;
|
|
184
|
-
let valueCount = 0;
|
|
185
|
-
/** @type {CalculationType} */ let result = failureType;
|
|
186
|
-
for (let index = 0; index < args.length; index++) {
|
|
187
|
-
if (isClampKeyword(nodes[index], index)) continue;
|
|
188
|
-
result = valueCount === 0 ? args[index] : addTypes(result, args[index]);
|
|
189
|
-
valueCount++;
|
|
190
|
-
}
|
|
191
|
-
if (valueCount < 1 || isFailure(result)) return failureType;
|
|
192
|
-
return result;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
45
|
const mathFunctions = new Map(
|
|
196
46
|
/** @type {[string, MathFunction][]} */ ([
|
|
197
47
|
['calc', { analyze: analyzeCalc, calculation: true }],
|