postcss-calc 11.2.0 → 11.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -0
- package/README.md +44 -16
- package/package.json +13 -13
- package/src/index.js +2 -1
- package/src/lib/analyze.js +81 -43
- package/src/lib/compile.js +1 -1
- package/src/lib/functions.js +40 -10
- package/src/lib/node.js +62 -5
- package/src/lib/parser/opaque.js +151 -0
- package/src/lib/parser/tokens.js +242 -0
- package/src/lib/parser.js +27 -382
- package/src/lib/serialize/expression.js +376 -0
- package/src/lib/serialize/precision.js +244 -0
- package/src/lib/serialize.js +24 -517
- 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.js +263 -48
- package/src/lib/simplify/sum.js +6 -2
- package/src/lib/simplify.js +22 -5
- package/src/reduce.js +2 -1
- package/types/lib/analyze.d.ts +27 -0
- package/types/lib/functions.d.ts +19 -1
- 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 +95 -0
- package/types/lib/serialize/precision.d.ts +101 -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.d.ts +3 -7
- package/types/lib/simplify/sum.d.ts +2 -1
- package/types/lib/simplify.d.ts +3 -8
- package/src/lib/calculation-type.js +0 -25
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
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 [W3C calc() implementation].
|
|
9
10
|
|
|
10
11
|
## Installation
|
|
11
12
|
|
|
@@ -62,7 +63,7 @@ leaving all other text untouched.
|
|
|
62
63
|
import reduceCalc from 'postcss-calc/reduce';
|
|
63
64
|
|
|
64
65
|
reduceCalc('calc(1in + 10px)');
|
|
65
|
-
// => 'calc(
|
|
66
|
+
// => 'calc(106px)'
|
|
66
67
|
|
|
67
68
|
reduceCalc('min(50px, calc(2 * 40px))');
|
|
68
69
|
// => 'calc(50px)'
|
|
@@ -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 }))
|
|
@@ -235,15 +258,10 @@ canonical-form decisions:
|
|
|
235
258
|
requires the zero term because it carries the length-percentage type.
|
|
236
259
|
- **Constant folding.** `calc(43 + pi)` now folds to `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.
|
|
@@ -265,13 +283,15 @@ To replace the value of CSS custom properties at build time, try [PostCSS Custom
|
|
|
265
283
|
## Contributing
|
|
266
284
|
|
|
267
285
|
Work on a branch, install dev-dependencies, respect coding style & run tests
|
|
268
|
-
before submitting a bug fix or a feature.
|
|
286
|
+
before submitting a bug fix or a feature. See [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
287
|
+
for the full guidelines. The project uses [pnpm](https://pnpm.io/).
|
|
269
288
|
|
|
270
289
|
```bash
|
|
271
290
|
git clone git@github.com:postcss/postcss-calc.git
|
|
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,11 +302,19 @@ when changing parsing/simplification behavior:
|
|
|
282
302
|
pnpm test:corpus:full
|
|
283
303
|
```
|
|
284
304
|
|
|
285
|
-
|
|
286
|
-
`pnpm benchmark:
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
305
|
+
Performance changes to the parser, analyzer, simplifier, or serializer should be
|
|
306
|
+
checked with the benchmarks. `pnpm benchmark:arithmetic-chains` and
|
|
307
|
+
`pnpm benchmark:nested-fallbacks` compare the working tree against `HEAD` using
|
|
308
|
+
20 fresh-process paired blocks by default and write git-ignored schema-v2
|
|
309
|
+
reports under `reports/benchmarks/`. Re-check a saved report with
|
|
310
|
+
`pnpm benchmark:reanalyze <report>`. `pnpm benchmark:corpus` compares the whole
|
|
311
|
+
pipeline with `@csstools/css-calc` on real-world expressions and is
|
|
312
|
+
report-only.
|
|
313
|
+
|
|
314
|
+
These benchmarks time only the parser (or, for the corpus, the whole reducer)
|
|
315
|
+
on one machine, and a `pass` means "no regression detected at the declared
|
|
316
|
+
margin", not "no change". Read [BENCHMARKS.md](BENCHMARKS.md) before
|
|
317
|
+
interpreting results.
|
|
290
318
|
|
|
291
319
|
The PostCSS benchmark awaits `postcss().process(...)`, and that await already
|
|
292
320
|
triggers result stringification. It therefore does not add a redundant
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postcss-calc",
|
|
3
|
-
"version": "11.2.
|
|
3
|
+
"version": "11.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "PostCSS plugin to reduce calc()",
|
|
6
6
|
"keywords": [
|
|
@@ -38,20 +38,20 @@
|
|
|
38
38
|
"devEngines": {
|
|
39
39
|
"packageManager": {
|
|
40
40
|
"name": "pnpm",
|
|
41
|
-
"version": "12.
|
|
41
|
+
"version": "12.8.1"
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@csstools/css-calc": "^3.
|
|
46
|
-
"@types/node": "^26.
|
|
47
|
-
"fast-check": "^4.10.
|
|
48
|
-
"oxfmt": "^0.
|
|
49
|
-
"oxlint": "^1.
|
|
45
|
+
"@csstools/css-calc": "^3.4.1",
|
|
46
|
+
"@types/node": "^26.6.3",
|
|
47
|
+
"fast-check": "^4.10.2",
|
|
48
|
+
"oxfmt": "^0.71.0",
|
|
49
|
+
"oxlint": "^1.86.0",
|
|
50
50
|
"postcss": "^8.5.28",
|
|
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
57
|
"postcss": "^8.5.28"
|
|
@@ -59,13 +59,13 @@
|
|
|
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
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
import { baseOf } from './convertUnits.js';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
addTypes,
|
|
4
|
+
failureType,
|
|
5
|
+
isFailure,
|
|
6
|
+
isPercentage,
|
|
7
|
+
mathFunctions,
|
|
8
|
+
numberType,
|
|
9
|
+
percentageType,
|
|
10
|
+
unknownType,
|
|
11
|
+
} from './functions.js';
|
|
3
12
|
import { assertDepth } from './limits.js';
|
|
4
13
|
|
|
5
14
|
/** @typedef {import('./node.js').Node} Node */
|
|
6
15
|
/** @typedef {import('./functions.js').CalculationType} CalculationType */
|
|
16
|
+
/** @typedef {Extract<CalculationType, {kind: 'dimension'}>} DimensionType */
|
|
7
17
|
|
|
8
18
|
/** @typedef {'number' | 'unknown' | {dimension: string | null}} AnalysisType */
|
|
9
19
|
/** @typedef {{type: AnalysisType, valid: boolean, unresolved: boolean}} Analysis */
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} ProductFactors
|
|
22
|
+
* @property {boolean} valid
|
|
23
|
+
* @property {boolean} structurallyValid
|
|
24
|
+
* @property {boolean} hasUnresolved
|
|
25
|
+
* @property {DimensionType | null} numerator
|
|
26
|
+
* @property {DimensionType | null} denominator
|
|
27
|
+
* @property {boolean} hasOpaqueNumerator
|
|
28
|
+
* @property {boolean} hasOpaqueDenominator
|
|
29
|
+
*/
|
|
14
30
|
|
|
15
31
|
/**
|
|
16
32
|
* Analyze the original complete tree and return its root summary. Analysis
|
|
@@ -34,10 +50,8 @@ function analyzeType(node, depth = 0) {
|
|
|
34
50
|
case 'Num':
|
|
35
51
|
return resolved(numberType);
|
|
36
52
|
case 'Dim':
|
|
37
|
-
// Percentages are contextual. Unknown units are opaque, while known
|
|
38
|
-
// families can still reject px + seconds.
|
|
39
53
|
return node.unit === '%'
|
|
40
|
-
? finish(
|
|
54
|
+
? finish(percentageType, true, true)
|
|
41
55
|
: resolved({ kind: 'dimension', base: baseOf(node.unit) });
|
|
42
56
|
case 'Ident':
|
|
43
57
|
return markUnresolved(unknownType);
|
|
@@ -56,6 +70,7 @@ function analyzeType(node, depth = 0) {
|
|
|
56
70
|
function analyzeSum(node, depth) {
|
|
57
71
|
let type = null;
|
|
58
72
|
let hasUnknown = false;
|
|
73
|
+
let hasPercentage = false;
|
|
59
74
|
let valid = true;
|
|
60
75
|
let hasUnresolved = false;
|
|
61
76
|
for (const term of node.terms) {
|
|
@@ -65,7 +80,8 @@ function analyzeSum(node, depth) {
|
|
|
65
80
|
if (isFailure(child.type)) {
|
|
66
81
|
type = failureType;
|
|
67
82
|
} else if (child.type.kind === 'unknown') {
|
|
68
|
-
|
|
83
|
+
if (isPercentage(child.type)) hasPercentage = true;
|
|
84
|
+
else hasUnknown = true;
|
|
69
85
|
} else if (type === null) {
|
|
70
86
|
type = child.type;
|
|
71
87
|
} else if (!isFailure(type)) {
|
|
@@ -75,11 +91,10 @@ function analyzeSum(node, depth) {
|
|
|
75
91
|
if (type !== null && isFailure(type)) {
|
|
76
92
|
return finish(failureType, false, hasUnresolved);
|
|
77
93
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
);
|
|
94
|
+
let fallback = numberType;
|
|
95
|
+
if (hasUnknown) fallback = unknownType;
|
|
96
|
+
else if (hasPercentage) fallback = percentageType;
|
|
97
|
+
return finish(type ?? fallback, valid, hasUnresolved);
|
|
83
98
|
}
|
|
84
99
|
|
|
85
100
|
/** @param {Extract<Node, {type: 'Product'}>} node @param {number} depth @return {{type: CalculationType, valid: boolean, unresolved: boolean}} */
|
|
@@ -88,8 +103,13 @@ function analyzeProduct(node, depth) {
|
|
|
88
103
|
let denominator = null;
|
|
89
104
|
let valid = true;
|
|
90
105
|
let structurallyValid = true;
|
|
91
|
-
|
|
92
|
-
|
|
106
|
+
// Opaque factors (unknowns and pure percentages) are counted per side so
|
|
107
|
+
// the pass below can decide whether any unknown remains after cancelling
|
|
108
|
+
// `% / %` pairs.
|
|
109
|
+
let opaqueNumerator = 0;
|
|
110
|
+
let opaqueDenominator = 0;
|
|
111
|
+
let percentageNumerator = 0;
|
|
112
|
+
let percentageDenominator = 0;
|
|
93
113
|
let hasUnresolved = false;
|
|
94
114
|
for (const factor of node.factors) {
|
|
95
115
|
const child = analyzeType(factor.node, depth + 1);
|
|
@@ -99,8 +119,13 @@ function analyzeProduct(node, depth) {
|
|
|
99
119
|
continue;
|
|
100
120
|
}
|
|
101
121
|
if (child.type.kind === 'unknown') {
|
|
102
|
-
if (factor.exponent === 1)
|
|
103
|
-
|
|
122
|
+
if (factor.exponent === 1) {
|
|
123
|
+
opaqueNumerator++;
|
|
124
|
+
if (isPercentage(child.type)) percentageNumerator++;
|
|
125
|
+
} else {
|
|
126
|
+
opaqueDenominator++;
|
|
127
|
+
if (isPercentage(child.type)) percentageDenominator++;
|
|
128
|
+
}
|
|
104
129
|
continue;
|
|
105
130
|
}
|
|
106
131
|
if (child.type.kind !== 'dimension') continue;
|
|
@@ -112,10 +137,36 @@ function analyzeProduct(node, depth) {
|
|
|
112
137
|
else denominator = child.type;
|
|
113
138
|
}
|
|
114
139
|
}
|
|
140
|
+
// A percentage divided by a percentage is always a plain number: both
|
|
141
|
+
// operands resolve in the same context, so their contextual type cancels.
|
|
142
|
+
// Consume one such pair before judging the remaining unknowns so a
|
|
143
|
+
// surrounding sum does not mistake `% / %` for a length-compatible term.
|
|
144
|
+
const cancelled = Math.min(percentageNumerator, percentageDenominator);
|
|
145
|
+
const hasOpaqueNumerator = opaqueNumerator - cancelled > 0;
|
|
146
|
+
const hasOpaqueDenominator = opaqueDenominator - cancelled > 0;
|
|
147
|
+
return finishProduct({
|
|
148
|
+
valid,
|
|
149
|
+
structurallyValid,
|
|
150
|
+
hasUnresolved,
|
|
151
|
+
numerator,
|
|
152
|
+
denominator,
|
|
153
|
+
hasOpaqueNumerator,
|
|
154
|
+
hasOpaqueDenominator,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Classify a product once its factors are analyzed. Opaque factors can supply
|
|
160
|
+
* missing type information, but they cannot make an already-invalid
|
|
161
|
+
* combination of known dimensions valid.
|
|
162
|
+
* @param {ProductFactors} factors
|
|
163
|
+
* @return {{type: CalculationType, valid: boolean, unresolved: boolean}}
|
|
164
|
+
*/
|
|
165
|
+
function finishProduct(factors) {
|
|
166
|
+
const { valid, structurallyValid, hasUnresolved } = factors;
|
|
115
167
|
if (!valid) return finish(failureType, false, hasUnresolved);
|
|
116
|
-
// Opaque factors can supply missing type information, but they cannot make
|
|
117
|
-
// an already-invalid combination of known dimensions valid.
|
|
118
168
|
if (!structurallyValid) return finish(failureType, false, hasUnresolved);
|
|
169
|
+
const { numerator, denominator } = factors;
|
|
119
170
|
if (
|
|
120
171
|
numerator !== null &&
|
|
121
172
|
denominator !== null &&
|
|
@@ -127,18 +178,13 @@ function analyzeProduct(node, depth) {
|
|
|
127
178
|
// only leave that dimension in place (when it resolves to a number) or make
|
|
128
179
|
// the product invalid. It can never make the product a bare number. Keep
|
|
129
180
|
// that known constraint so a surrounding sum can reject `1px * 1% + 1`.
|
|
130
|
-
const constrained = constrainedNumerator(
|
|
131
|
-
hasUnknownNumerator,
|
|
132
|
-
hasUnknownDenominator,
|
|
133
|
-
numerator,
|
|
134
|
-
denominator
|
|
135
|
-
);
|
|
181
|
+
const constrained = constrainedNumerator(factors);
|
|
136
182
|
if (constrained !== null) {
|
|
137
183
|
return finish(constrained, true, hasUnresolved);
|
|
138
184
|
}
|
|
139
185
|
// Other opaque factors may supply type information that changes how the
|
|
140
186
|
// known dimensions combine once the known factors are structurally valid.
|
|
141
|
-
if (
|
|
187
|
+
if (factors.hasOpaqueNumerator || factors.hasOpaqueDenominator) {
|
|
142
188
|
return finish(unknownType, true, hasUnresolved);
|
|
143
189
|
}
|
|
144
190
|
if (numerator !== null && denominator !== null) {
|
|
@@ -153,23 +199,15 @@ function analyzeProduct(node, depth) {
|
|
|
153
199
|
}
|
|
154
200
|
|
|
155
201
|
/**
|
|
156
|
-
* @param {
|
|
157
|
-
* @
|
|
158
|
-
* @param {CalculationType | null} numerator
|
|
159
|
-
* @param {CalculationType | null} denominator
|
|
160
|
-
* @return {CalculationType | null}
|
|
202
|
+
* @param {ProductFactors} factors
|
|
203
|
+
* @return {DimensionType | null}
|
|
161
204
|
*/
|
|
162
|
-
function constrainedNumerator(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return hasUnknownNumerator &&
|
|
169
|
-
!hasUnknownDenominator &&
|
|
170
|
-
numerator !== null &&
|
|
171
|
-
denominator === null
|
|
172
|
-
? numerator
|
|
205
|
+
function constrainedNumerator(factors) {
|
|
206
|
+
return factors.hasOpaqueNumerator &&
|
|
207
|
+
!factors.hasOpaqueDenominator &&
|
|
208
|
+
factors.numerator !== null &&
|
|
209
|
+
factors.denominator === null
|
|
210
|
+
? factors.numerator
|
|
173
211
|
: null;
|
|
174
212
|
}
|
|
175
213
|
|
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)
|
package/src/lib/functions.js
CHANGED
|
@@ -14,13 +14,23 @@ import { simplifyLog } from './simplify/log.js';
|
|
|
14
14
|
import { simplifyHypot } from './simplify/hypot.js';
|
|
15
15
|
|
|
16
16
|
/** @typedef {import('./node.js').Node} Node */
|
|
17
|
-
/**
|
|
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
|
+
*/
|
|
18
24
|
/** @typedef {(name: string, args: Node[]) => Node} MathSimplifier */
|
|
19
25
|
/** @typedef {(args: CalculationType[], nodes: Node[]) => CalculationType} TypeAnalyzer */
|
|
20
26
|
/** @typedef {{analyze: TypeAnalyzer, simplify?: MathSimplifier, isKeyword?: (node: Node, index: number) => boolean, calculation?: boolean}} MathFunction */
|
|
21
27
|
|
|
22
28
|
/** @type {CalculationType} */ const numberType = { kind: 'number' };
|
|
23
29
|
/** @type {CalculationType} */ const unknownType = { kind: 'unknown' };
|
|
30
|
+
/** @type {CalculationType} */ const percentageType = {
|
|
31
|
+
kind: 'unknown',
|
|
32
|
+
percent: true,
|
|
33
|
+
};
|
|
24
34
|
/** @type {CalculationType} */ const failureType = { kind: 'failure' };
|
|
25
35
|
|
|
26
36
|
/** @param {CalculationType} type @return {boolean} */
|
|
@@ -28,10 +38,23 @@ function isFailure(type) {
|
|
|
28
38
|
return type.kind === 'failure';
|
|
29
39
|
}
|
|
30
40
|
|
|
41
|
+
/** @param {CalculationType} type @return {boolean} */
|
|
42
|
+
function isPercentage(type) {
|
|
43
|
+
return type.kind === 'unknown' && type.percent === true;
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
/** @param {CalculationType} a @param {CalculationType} b @return {CalculationType} */
|
|
32
47
|
function addTypes(a, b) {
|
|
33
48
|
if (isFailure(a) || isFailure(b)) return failureType;
|
|
34
|
-
if (a.kind === 'unknown' || b.kind === 'unknown')
|
|
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
|
+
}
|
|
35
58
|
if (a.kind === 'number' && b.kind === 'number') return numberType;
|
|
36
59
|
if (a.kind === 'dimension' && b.kind === 'dimension') {
|
|
37
60
|
if (a.base === null || b.base === null) return unknownType;
|
|
@@ -129,9 +152,11 @@ function analyzeRound(args, nodes) {
|
|
|
129
152
|
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
130
153
|
function analyzeAtan2(args) {
|
|
131
154
|
const type = matchingArguments(args, 2, 2);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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' };
|
|
135
160
|
}
|
|
136
161
|
|
|
137
162
|
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
@@ -354,11 +379,16 @@ function hasPotentialMathFunction(value) {
|
|
|
354
379
|
|
|
355
380
|
export {
|
|
356
381
|
addTypes,
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
QUICK_MATH_TEST,
|
|
360
|
-
isFailure,
|
|
382
|
+
failureType,
|
|
383
|
+
hasPotentialMathFunction,
|
|
361
384
|
isCalculationFunction,
|
|
385
|
+
isFailure,
|
|
386
|
+
isPercentage,
|
|
362
387
|
isSupportedMathFunction,
|
|
363
|
-
|
|
388
|
+
lookupMathFunction,
|
|
389
|
+
mathFunctions,
|
|
390
|
+
numberType,
|
|
391
|
+
percentageType,
|
|
392
|
+
QUICK_MATH_TEST,
|
|
393
|
+
unknownType,
|
|
364
394
|
};
|
package/src/lib/node.js
CHANGED
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
// - No ungrouped Sum directly contains another Sum (flattened on
|
|
9
9
|
// construction). A grouped Sum is retained so a later negative sign
|
|
10
10
|
// cannot be distributed across opaque terms.
|
|
11
|
-
// - No Product directly contains another Product (flattened).
|
|
11
|
+
// - No Product directly contains another ungrouped Product (flattened). A
|
|
12
|
+
// grouped Product is retained: it is a parenthesized group around a
|
|
13
|
+
// substitution function (`var()`, `env()`, ...), whose tokens must not
|
|
14
|
+
// merge with the surrounding factors. A grouped Product may hold a single
|
|
15
|
+
// factor.
|
|
12
16
|
// - A Sum/Product with one positive element collapses to that element.
|
|
13
17
|
// - A Sum/Product with no elements collapses to Num(0) / Num(1).
|
|
14
18
|
// - Positive zero-valued Nums are dropped from all-number sums. They are
|
|
@@ -28,7 +32,7 @@
|
|
|
28
32
|
* @typedef {{sign: 1 | -1, node: Node}} SumTerm Sign is always +1 when node is Num or Dim.
|
|
29
33
|
* @typedef {{type: 'Sum', terms: SumTerm[], grouped?: boolean}} Sum
|
|
30
34
|
* @typedef {{exponent: 1 | -1, node: Node}} ProductFactor exponent +1 = numerator, -1 = denominator.
|
|
31
|
-
* @typedef {{type: 'Product', factors: ProductFactor[]}} Product
|
|
35
|
+
* @typedef {{type: 'Product', factors: ProductFactor[], grouped?: boolean}} Product
|
|
32
36
|
* @typedef {Num | Dim | Ident | Call | OpaqueCall | Sum | Product} Node
|
|
33
37
|
*/
|
|
34
38
|
|
|
@@ -185,7 +189,7 @@ function mkProduct(rawFactors) {
|
|
|
185
189
|
*/
|
|
186
190
|
function pushProductFactor(out, f) {
|
|
187
191
|
const n = f.node;
|
|
188
|
-
if (n.type === 'Product') {
|
|
192
|
+
if (n.type === 'Product' && !n.grouped) {
|
|
189
193
|
for (const inner of n.factors) {
|
|
190
194
|
out.push({
|
|
191
195
|
exponent: /** @type {1 | -1} */ (f.exponent * inner.exponent),
|
|
@@ -201,6 +205,48 @@ function pushProductFactor(out, f) {
|
|
|
201
205
|
out.push(f);
|
|
202
206
|
}
|
|
203
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Mark a node as a source parenthesized group. A Sum or Product keeps its
|
|
210
|
+
* structure; any other node becomes a single-factor grouped Product.
|
|
211
|
+
* @param {Node} node
|
|
212
|
+
* @return {Node}
|
|
213
|
+
*/
|
|
214
|
+
function mkGroup(node) {
|
|
215
|
+
if (node.type === 'Sum' || node.type === 'Product') {
|
|
216
|
+
return { ...node, grouped: true };
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
type: 'Product',
|
|
220
|
+
factors: [{ exponent: 1, node }],
|
|
221
|
+
grouped: true,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Whether a node is a substitution function call (`var()`, `env()`, ...), or
|
|
227
|
+
* an ungrouped Product that directly contains one. Nested groups are not
|
|
228
|
+
* inspected: they already bound their own tokens.
|
|
229
|
+
* @param {Node} node
|
|
230
|
+
* @return {boolean}
|
|
231
|
+
*/
|
|
232
|
+
function isSubstitutionNode(node) {
|
|
233
|
+
if (node.type === 'OpaqueCall') return true;
|
|
234
|
+
if (node.type === 'Product' && !node.grouped) {
|
|
235
|
+
return node.factors.some((factor) => isSubstitutionNode(factor.node));
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Group a parenthesized expression when its tokens must stay bound: a sum, or
|
|
242
|
+
* a product around a substitution function.
|
|
243
|
+
* @param {Node} node
|
|
244
|
+
* @return {Node}
|
|
245
|
+
*/
|
|
246
|
+
function groupSubstitution(node) {
|
|
247
|
+
return node.type === 'Sum' || isSubstitutionNode(node) ? mkGroup(node) : node;
|
|
248
|
+
}
|
|
249
|
+
|
|
204
250
|
/**
|
|
205
251
|
* Negate any node, preserving canonical form.
|
|
206
252
|
* @param {Node} node
|
|
@@ -215,7 +261,7 @@ function negate(node) {
|
|
|
215
261
|
}
|
|
216
262
|
if (node.type === 'Sum') {
|
|
217
263
|
// A grouped sum may contain opaque terms whose meaning depends on the
|
|
218
|
-
// surrounding context. Keep the group intact so `-(a + b)` cannot turn
|
|
264
|
+
// surrounding context. Keep the group intact so `-1 * (a + b)` cannot turn
|
|
219
265
|
// into `-a - b` while it is still unresolved.
|
|
220
266
|
if (node.grouped) {
|
|
221
267
|
return mkSum([{ sign: -1, node }]);
|
|
@@ -233,4 +279,15 @@ function negate(node) {
|
|
|
233
279
|
return mkSum([{ sign: -1, node }]);
|
|
234
280
|
}
|
|
235
281
|
|
|
236
|
-
export {
|
|
282
|
+
export {
|
|
283
|
+
num,
|
|
284
|
+
dim,
|
|
285
|
+
ident,
|
|
286
|
+
call,
|
|
287
|
+
opaqueCall,
|
|
288
|
+
mkSum,
|
|
289
|
+
mkProduct,
|
|
290
|
+
mkGroup,
|
|
291
|
+
groupSubstitution,
|
|
292
|
+
negate,
|
|
293
|
+
};
|