postcss-calc 11.1.2 → 11.2.1
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 +40 -18
- package/package.json +18 -14
- package/src/index.js +8 -5
- package/src/lib/analyze.js +272 -0
- package/src/lib/block-index.js +118 -0
- package/src/lib/compile.js +80 -0
- package/src/lib/functions.js +394 -0
- package/src/lib/limits.js +49 -0
- package/src/lib/node.js +63 -21
- package/src/lib/opaque.js +12 -32
- package/src/lib/parser.js +373 -328
- package/src/lib/print.js +75 -0
- package/src/lib/regex.js +4 -0
- package/src/lib/scan.js +61 -0
- package/src/lib/serialize.js +626 -223
- package/src/lib/simplify/call.js +5 -98
- package/src/lib/simplify/round.js +4 -4
- package/src/lib/simplify/sum.js +14 -5
- package/src/lib/simplify.js +20 -5
- package/src/reduce.js +46 -115
- package/types/index.d.ts +4 -0
- package/types/lib/analyze.d.ts +45 -0
- package/types/lib/block-index.d.ts +35 -0
- package/types/lib/calculation-type.d.ts +18 -0
- package/types/lib/compile.d.ts +37 -0
- package/types/lib/functions.d.ts +58 -0
- package/types/lib/limits.d.ts +8 -0
- package/types/lib/node.d.ts +19 -3
- package/types/lib/opaque.d.ts +8 -14
- package/types/lib/parser.d.ts +42 -48
- package/types/lib/print.d.ts +16 -0
- package/types/lib/regex.d.ts +2 -0
- package/types/lib/scan.d.ts +35 -0
- package/types/lib/serialize.d.ts +19 -2
- package/types/lib/simplify/call.d.ts +4 -18
- package/types/lib/simplify/round.d.ts +3 -1
- package/types/lib/simplify.d.ts +5 -2
- package/types/reduce.d.ts +47 -6
- package/src/lib/tokenizer.js +0 -10
- package/types/lib/tokenizer.d.ts +0 -3
package/README.md
CHANGED
|
@@ -43,10 +43,10 @@ you will get:
|
|
|
43
43
|
|
|
44
44
|
```css
|
|
45
45
|
h1 {
|
|
46
|
-
font-size: 32px;
|
|
46
|
+
font-size: calc(32px);
|
|
47
47
|
height: calc(100px - 2em);
|
|
48
48
|
width: calc(2 * var(--base-width));
|
|
49
|
-
margin-bottom: 24px;
|
|
49
|
+
margin-bottom: calc(24px);
|
|
50
50
|
}
|
|
51
51
|
```
|
|
52
52
|
|
|
@@ -62,14 +62,15 @@ leaving all other text untouched.
|
|
|
62
62
|
import reduceCalc from 'postcss-calc/reduce';
|
|
63
63
|
|
|
64
64
|
reduceCalc('calc(1in + 10px)');
|
|
65
|
-
// => '1.10417in'
|
|
65
|
+
// => 'calc(1.10417in)'
|
|
66
66
|
|
|
67
67
|
reduceCalc('min(50px, calc(2 * 40px))');
|
|
68
|
-
// => '50px'
|
|
68
|
+
// => 'calc(50px)'
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
It accepts `precision`, `
|
|
72
|
-
|
|
71
|
+
It accepts `precision`, `unwrapSingleValue`, the deprecated
|
|
72
|
+
`unwrapSingleNegativeNumber` alias,
|
|
73
|
+
`warnWhenCannotResolve`, `onParseError`, and `onWarn`:
|
|
73
74
|
|
|
74
75
|
```js
|
|
75
76
|
const result = reduceCalc('calc(100% + var(--gap))', {
|
|
@@ -87,21 +88,27 @@ by default; provide `onParseError` and/or `onWarn` if you want diagnostics.
|
|
|
87
88
|
|
|
88
89
|
### Standalone reducer options
|
|
89
90
|
|
|
90
|
-
#### `
|
|
91
|
+
#### `unwrapSingleValue` (default: `false`)
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
as a selector:
|
|
93
|
+
Serializes a fully resolved finite scalar result without calculation syntax.
|
|
94
|
+
Keep the default for standard CSS so the browser can perform range clamping
|
|
95
|
+
and integer rounding. Set it to `true` for a non-standard context that requires
|
|
96
|
+
a bare value, such as a selector:
|
|
96
97
|
|
|
97
98
|
```js
|
|
98
99
|
reduceCalc('calc(5px - 10px)');
|
|
99
100
|
// => 'calc(-5px)'
|
|
100
101
|
|
|
101
|
-
reduceCalc('calc(5px - 10px)', {
|
|
102
|
+
reduceCalc('calc(5px - 10px)', { unwrapSingleValue: true });
|
|
102
103
|
// => '-5px'
|
|
104
|
+
|
|
105
|
+
reduceCalc('calc(1 / 2)', { unwrapSingleValue: true });
|
|
106
|
+
// => '.5'
|
|
103
107
|
```
|
|
104
108
|
|
|
109
|
+
The published `unwrapSingleNegativeNumber` option is retained as a deprecated
|
|
110
|
+
alias for `unwrapSingleValue`.
|
|
111
|
+
|
|
105
112
|
### PostCSS plugin options
|
|
106
113
|
|
|
107
114
|
These options apply when using the PostCSS plugin:
|
|
@@ -113,7 +120,8 @@ postcss().use(calc({ precision: 10 }));
|
|
|
113
120
|
#### `precision` (default: `5`)
|
|
114
121
|
|
|
115
122
|
Allows you to define the precision for decimal numbers. Set it to `false` to
|
|
116
|
-
disable rounding
|
|
123
|
+
disable rounding and preserve full IEEE-754 floating-point precision (emitting
|
|
124
|
+
the shortest round-tripping decimal representation).
|
|
117
125
|
|
|
118
126
|
```js
|
|
119
127
|
var out = postcss()
|
|
@@ -121,6 +129,12 @@ var out = postcss()
|
|
|
121
129
|
.process(css).css;
|
|
122
130
|
```
|
|
123
131
|
|
|
132
|
+
#### `unwrapSingleValue` (default: `false`)
|
|
133
|
+
|
|
134
|
+
Serializes fully resolved finite scalar results without calculation syntax.
|
|
135
|
+
This can discard browser-applied range clamping or integer rounding. Selectors
|
|
136
|
+
enable it automatically because selectors cannot contain `calc()`.
|
|
137
|
+
|
|
124
138
|
#### `warnWhenCannotResolve` (default: `false`)
|
|
125
139
|
|
|
126
140
|
Adds warnings when calc() are not reduced to a single value.
|
|
@@ -165,9 +179,9 @@ With `mediaQueries: true`, this becomes:
|
|
|
165
179
|
|
|
166
180
|
Reduces `calc()` functions found in selectors. Selectors do not accept
|
|
167
181
|
`calc()` functions, so the plugin replaces them with their reduced values.
|
|
168
|
-
Finite negative results are serialized as bare values
|
|
169
|
-
contain a `calc()` function; the plugin enables
|
|
170
|
-
for selectors.
|
|
182
|
+
Finite negative and fractional unitless results are serialized as bare values
|
|
183
|
+
because a selector cannot contain a `calc()` function; the plugin enables the
|
|
184
|
+
`unwrapSingleValue` automatically for selectors.
|
|
171
185
|
|
|
172
186
|
```js
|
|
173
187
|
var out = postcss()
|
|
@@ -268,7 +282,15 @@ when changing parsing/simplification behavior:
|
|
|
268
282
|
pnpm test:corpus:full
|
|
269
283
|
```
|
|
270
284
|
|
|
271
|
-
Profile
|
|
285
|
+
Profile parser chains with `pnpm benchmark:arithmetic-chains` or
|
|
286
|
+
`pnpm benchmark:nested-fallbacks`; both use 20 fresh paired blocks by default
|
|
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.
|
|
272
294
|
|
|
273
295
|
## [Changelog](CHANGELOG.md)
|
|
274
296
|
|
|
@@ -281,5 +303,5 @@ Profile long arithmetic parser chains with `pnpm benchmark:arithmetic-chains`.
|
|
|
281
303
|
[PostCSS]: https://github.com/postcss
|
|
282
304
|
[PostCSS Calc]: https://github.com/postcss/postcss-calc
|
|
283
305
|
[PostCSS Custom Properties]: https://github.com/postcss/postcss-custom-properties
|
|
284
|
-
[tests]: test/
|
|
306
|
+
[tests]: test/
|
|
285
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.1
|
|
3
|
+
"version": "11.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "PostCSS plugin to reduce calc()",
|
|
6
6
|
"keywords": [
|
|
@@ -38,21 +38,20 @@
|
|
|
38
38
|
"devEngines": {
|
|
39
39
|
"packageManager": {
|
|
40
40
|
"name": "pnpm",
|
|
41
|
-
"version": "12.
|
|
41
|
+
"version": "12.4.2"
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@csstools/css-calc": "^3.
|
|
46
|
-
"@
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"oxlint": "^1.82.0",
|
|
45
|
+
"@csstools/css-calc": "^3.4.0",
|
|
46
|
+
"@types/node": "^26.6.1",
|
|
47
|
+
"fast-check": "^4.10.1",
|
|
48
|
+
"oxfmt": "^0.68.0",
|
|
49
|
+
"oxlint": "^1.83.0",
|
|
51
50
|
"postcss": "^8.5.28",
|
|
52
51
|
"typescript": "~7.0.2"
|
|
53
52
|
},
|
|
54
53
|
"dependencies": {
|
|
55
|
-
"@csstools/css-tokenizer": "^4.0.
|
|
54
|
+
"@csstools/css-tokenizer": "^4.0.1"
|
|
56
55
|
},
|
|
57
56
|
"peerDependencies": {
|
|
58
57
|
"postcss": "^8.5.28"
|
|
@@ -60,10 +59,15 @@
|
|
|
60
59
|
"scripts": {
|
|
61
60
|
"lint": "oxlint . && tsc && oxfmt --check",
|
|
62
61
|
"fmt": "oxfmt",
|
|
63
|
-
"benchmark:arithmetic-chains": "node scripts/benchmark-arithmetic-chains.
|
|
64
|
-
"benchmark:nested-fallbacks": "node scripts/benchmark-nested-fallbacks.
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"test:
|
|
62
|
+
"benchmark:arithmetic-chains": "node scripts/benchmark-arithmetic-chains.js",
|
|
63
|
+
"benchmark:nested-fallbacks": "node scripts/benchmark-nested-fallbacks.js",
|
|
64
|
+
"benchmark:corpus": "node scripts/benchmark.js",
|
|
65
|
+
"benchmark:serialization": "node scripts/benchmark-serialization.js",
|
|
66
|
+
"test:benchmark": "node --test 'test/unit/benchmark-*.test.js' test/unit/compare-parser-benchmarks.test.js test/unit/corpus-benchmark.test.js",
|
|
67
|
+
"test:benchmark:simulation": "node test/benchmark/statistical-simulation.js",
|
|
68
|
+
"benchmark:reanalyze": "node scripts/compare-parser-benchmarks.js",
|
|
69
|
+
"test": "node --test --test-reporter=dot 'test/**/*.test.js' 'test/**/*.test.cjs'",
|
|
70
|
+
"test:mutation:corpus": "node test/mutation/corpus-selection.js",
|
|
71
|
+
"test:corpus:full": "POSTCSS_CALC_FULL_CORPUS=1 node --test test/conformance/corpus.test.js"
|
|
68
72
|
}
|
|
69
73
|
}
|
package/src/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// PostCSS adapter over the standalone component-value reducer.
|
|
2
|
-
import reduceCalc
|
|
3
|
-
|
|
2
|
+
import reduceCalc from './reduce.js';
|
|
3
|
+
import { hasPotentialMathFunction } from './lib/functions.js';
|
|
4
4
|
/**
|
|
5
5
|
* @typedef {object} PostCssCalcOptions
|
|
6
6
|
* @property {number | false} [precision]
|
|
7
7
|
* @property {boolean} [warnWhenCannotResolve]
|
|
8
8
|
* @property {boolean} [mediaQueries]
|
|
9
9
|
* @property {boolean} [selectors]
|
|
10
|
+
* @property {boolean} [unwrapSingleValue] Serialize fully resolved finite scalar results without calculation syntax. Defaults to `false`.
|
|
10
11
|
* @property {(error: Error, input: string) => void} [onParseError] Invoked when parse/simplify throws. Replaces the default `result.warn`.
|
|
11
12
|
*/
|
|
12
13
|
|
|
@@ -24,7 +25,7 @@ import reduceCalc, { hasPotentialMathFunction } from './reduce.js';
|
|
|
24
25
|
* @param {(target: import('postcss').ChildNode, value: string) => void} setProp
|
|
25
26
|
* @param {ResolvedOptions} options
|
|
26
27
|
* @param {import('postcss').Result} result
|
|
27
|
-
* @param {boolean}
|
|
28
|
+
* @param {boolean} selectorContext
|
|
28
29
|
* @return {void}
|
|
29
30
|
*/
|
|
30
31
|
function applyTransform(
|
|
@@ -33,7 +34,7 @@ function applyTransform(
|
|
|
33
34
|
setProp,
|
|
34
35
|
options,
|
|
35
36
|
result,
|
|
36
|
-
|
|
37
|
+
selectorContext
|
|
37
38
|
) {
|
|
38
39
|
if (!hasPotentialMathFunction(current)) {
|
|
39
40
|
return;
|
|
@@ -49,7 +50,8 @@ function applyTransform(
|
|
|
49
50
|
onWarn: (message) => {
|
|
50
51
|
result.warn(message, { plugin: 'postcss-calc', node });
|
|
51
52
|
},
|
|
52
|
-
|
|
53
|
+
// Selectors cannot contain calc(), so they always unwrap resolved values.
|
|
54
|
+
unwrapSingleValue: selectorContext || options.unwrapSingleValue,
|
|
53
55
|
});
|
|
54
56
|
if (transformed !== current) {
|
|
55
57
|
setProp(node, transformed);
|
|
@@ -67,6 +69,7 @@ function pluginCreator(opts) {
|
|
|
67
69
|
warnWhenCannotResolve: false,
|
|
68
70
|
mediaQueries: false,
|
|
69
71
|
selectors: false,
|
|
72
|
+
unwrapSingleValue: false,
|
|
70
73
|
...opts,
|
|
71
74
|
};
|
|
72
75
|
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { baseOf } from './convertUnits.js';
|
|
2
|
+
import {
|
|
3
|
+
addTypes,
|
|
4
|
+
failureType,
|
|
5
|
+
isFailure,
|
|
6
|
+
isPercentage,
|
|
7
|
+
mathFunctions,
|
|
8
|
+
numberType,
|
|
9
|
+
percentageType,
|
|
10
|
+
unknownType,
|
|
11
|
+
} from './functions.js';
|
|
12
|
+
import { assertDepth } from './limits.js';
|
|
13
|
+
|
|
14
|
+
/** @typedef {import('./node.js').Node} Node */
|
|
15
|
+
/** @typedef {import('./functions.js').CalculationType} CalculationType */
|
|
16
|
+
/** @typedef {Extract<CalculationType, {kind: 'dimension'}>} DimensionType */
|
|
17
|
+
|
|
18
|
+
/** @typedef {'number' | 'unknown' | {dimension: string | null}} AnalysisType */
|
|
19
|
+
/** @typedef {{type: AnalysisType, valid: boolean, unresolved: boolean}} Analysis */
|
|
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
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Analyze the original complete tree and return its root summary. Analysis
|
|
33
|
+
* validates and classifies the tree; it is not a rewrite plan.
|
|
34
|
+
* @param {Node} node
|
|
35
|
+
* @return {Analysis}
|
|
36
|
+
*/
|
|
37
|
+
function analyze(node) {
|
|
38
|
+
const result = analyzeType(node);
|
|
39
|
+
return {
|
|
40
|
+
type: publicType(result.type),
|
|
41
|
+
valid: result.valid,
|
|
42
|
+
unresolved: result.unresolved,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @param {Node} node @param {number} [depth] @return {{type: CalculationType, valid: boolean, unresolved: boolean}} */
|
|
47
|
+
function analyzeType(node, depth = 0) {
|
|
48
|
+
assertDepth(depth);
|
|
49
|
+
switch (node.type) {
|
|
50
|
+
case 'Num':
|
|
51
|
+
return resolved(numberType);
|
|
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
|
+
return node.unit === '%'
|
|
57
|
+
? finish(percentageType, true, true)
|
|
58
|
+
: resolved({ kind: 'dimension', base: baseOf(node.unit) });
|
|
59
|
+
case 'Ident':
|
|
60
|
+
return markUnresolved(unknownType);
|
|
61
|
+
case 'Sum':
|
|
62
|
+
return analyzeSum(node, depth);
|
|
63
|
+
case 'Product':
|
|
64
|
+
return analyzeProduct(node, depth);
|
|
65
|
+
case 'Call':
|
|
66
|
+
return analyzeCall(node, depth);
|
|
67
|
+
case 'OpaqueCall':
|
|
68
|
+
return finish(unknownType, true, true);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @param {Extract<Node, {type: 'Sum'}>} node @param {number} depth @return {{type: CalculationType, valid: boolean, unresolved: boolean}} */
|
|
73
|
+
function analyzeSum(node, depth) {
|
|
74
|
+
let type = null;
|
|
75
|
+
let hasUnknown = false;
|
|
76
|
+
let hasPercentage = false;
|
|
77
|
+
let valid = true;
|
|
78
|
+
let hasUnresolved = false;
|
|
79
|
+
for (const term of node.terms) {
|
|
80
|
+
const child = analyzeType(term.node, depth + 1);
|
|
81
|
+
valid = valid && child.valid;
|
|
82
|
+
hasUnresolved = hasUnresolved || child.unresolved;
|
|
83
|
+
if (isFailure(child.type)) {
|
|
84
|
+
type = failureType;
|
|
85
|
+
} 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
|
+
if (isPercentage(child.type)) hasPercentage = true;
|
|
90
|
+
else hasUnknown = true;
|
|
91
|
+
} else if (type === null) {
|
|
92
|
+
type = child.type;
|
|
93
|
+
} else if (!isFailure(type)) {
|
|
94
|
+
type = addTypes(type, child.type);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (type !== null && isFailure(type)) {
|
|
98
|
+
return finish(failureType, false, hasUnresolved);
|
|
99
|
+
}
|
|
100
|
+
let fallback = numberType;
|
|
101
|
+
if (hasUnknown) fallback = unknownType;
|
|
102
|
+
else if (hasPercentage) fallback = percentageType;
|
|
103
|
+
return finish(type ?? fallback, valid, hasUnresolved);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** @param {Extract<Node, {type: 'Product'}>} node @param {number} depth @return {{type: CalculationType, valid: boolean, unresolved: boolean}} */
|
|
107
|
+
function analyzeProduct(node, depth) {
|
|
108
|
+
let numerator = null;
|
|
109
|
+
let denominator = null;
|
|
110
|
+
let valid = true;
|
|
111
|
+
let structurallyValid = true;
|
|
112
|
+
// Opaque factors (unknowns and pure percentages) are counted per side so
|
|
113
|
+
// the pass below can decide whether any unknown remains after cancelling
|
|
114
|
+
// `% / %` pairs.
|
|
115
|
+
let opaqueNumerator = 0;
|
|
116
|
+
let opaqueDenominator = 0;
|
|
117
|
+
let percentageNumerator = 0;
|
|
118
|
+
let percentageDenominator = 0;
|
|
119
|
+
let hasUnresolved = false;
|
|
120
|
+
for (const factor of node.factors) {
|
|
121
|
+
const child = analyzeType(factor.node, depth + 1);
|
|
122
|
+
valid = valid && child.valid;
|
|
123
|
+
hasUnresolved = hasUnresolved || child.unresolved;
|
|
124
|
+
if (isFailure(child.type)) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (child.type.kind === 'unknown') {
|
|
128
|
+
if (factor.exponent === 1) {
|
|
129
|
+
opaqueNumerator++;
|
|
130
|
+
if (isPercentage(child.type)) percentageNumerator++;
|
|
131
|
+
} else {
|
|
132
|
+
opaqueDenominator++;
|
|
133
|
+
if (isPercentage(child.type)) percentageDenominator++;
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (child.type.kind !== 'dimension') continue;
|
|
138
|
+
if (factor.exponent === 1) {
|
|
139
|
+
if (numerator !== null) structurallyValid = false;
|
|
140
|
+
else numerator = child.type;
|
|
141
|
+
} else {
|
|
142
|
+
if (denominator !== null) structurallyValid = false;
|
|
143
|
+
else denominator = child.type;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// A percentage divided by a percentage is always a plain number: both
|
|
147
|
+
// operands resolve in the same context, so their contextual type cancels.
|
|
148
|
+
// Consume one such pair before judging the remaining unknowns so a
|
|
149
|
+
// surrounding sum does not mistake `% / %` for a length-compatible term.
|
|
150
|
+
const cancelled = Math.min(percentageNumerator, percentageDenominator);
|
|
151
|
+
const hasOpaqueNumerator = opaqueNumerator - cancelled > 0;
|
|
152
|
+
const hasOpaqueDenominator = opaqueDenominator - cancelled > 0;
|
|
153
|
+
return finishProduct({
|
|
154
|
+
valid,
|
|
155
|
+
structurallyValid,
|
|
156
|
+
hasUnresolved,
|
|
157
|
+
numerator,
|
|
158
|
+
denominator,
|
|
159
|
+
hasOpaqueNumerator,
|
|
160
|
+
hasOpaqueDenominator,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Classify a product once its factors are analyzed. Opaque factors can supply
|
|
166
|
+
* missing type information, but they cannot make an already-invalid
|
|
167
|
+
* combination of known dimensions valid.
|
|
168
|
+
* @param {ProductFactors} factors
|
|
169
|
+
* @return {{type: CalculationType, valid: boolean, unresolved: boolean}}
|
|
170
|
+
*/
|
|
171
|
+
function finishProduct(factors) {
|
|
172
|
+
const { valid, structurallyValid, hasUnresolved } = factors;
|
|
173
|
+
if (!valid) return finish(failureType, false, hasUnresolved);
|
|
174
|
+
if (!structurallyValid) return finish(failureType, false, hasUnresolved);
|
|
175
|
+
const { numerator, denominator } = factors;
|
|
176
|
+
if (
|
|
177
|
+
numerator !== null &&
|
|
178
|
+
denominator !== null &&
|
|
179
|
+
numerator.base !== denominator.base
|
|
180
|
+
) {
|
|
181
|
+
return finish(failureType, false, hasUnresolved);
|
|
182
|
+
}
|
|
183
|
+
// An unresolved numerator multiplied by one known numerator dimension can
|
|
184
|
+
// only leave that dimension in place (when it resolves to a number) or make
|
|
185
|
+
// the product invalid. It can never make the product a bare number. Keep
|
|
186
|
+
// that known constraint so a surrounding sum can reject `1px * 1% + 1`.
|
|
187
|
+
const constrained = constrainedNumerator(factors);
|
|
188
|
+
if (constrained !== null) {
|
|
189
|
+
return finish(constrained, true, hasUnresolved);
|
|
190
|
+
}
|
|
191
|
+
// Other opaque factors may supply type information that changes how the
|
|
192
|
+
// known dimensions combine once the known factors are structurally valid.
|
|
193
|
+
if (factors.hasOpaqueNumerator || factors.hasOpaqueDenominator) {
|
|
194
|
+
return finish(unknownType, true, hasUnresolved);
|
|
195
|
+
}
|
|
196
|
+
if (numerator !== null && denominator !== null) {
|
|
197
|
+
return finish(
|
|
198
|
+
numerator.base === denominator.base ? numberType : failureType,
|
|
199
|
+
valid && numerator.base === denominator.base,
|
|
200
|
+
hasUnresolved
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (denominator !== null) return finish(failureType, false, hasUnresolved);
|
|
204
|
+
return finish(numerator ?? numberType, valid, hasUnresolved);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @param {ProductFactors} factors
|
|
209
|
+
* @return {DimensionType | null}
|
|
210
|
+
*/
|
|
211
|
+
function constrainedNumerator(factors) {
|
|
212
|
+
return factors.hasOpaqueNumerator &&
|
|
213
|
+
!factors.hasOpaqueDenominator &&
|
|
214
|
+
factors.numerator !== null &&
|
|
215
|
+
factors.denominator === null
|
|
216
|
+
? factors.numerator
|
|
217
|
+
: null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** @param {Extract<Node, {type: 'Call'}>} node @param {number} depth @return {{type: CalculationType, valid: boolean, unresolved: boolean}} */
|
|
221
|
+
function analyzeCall(node, depth) {
|
|
222
|
+
const name = node.name.toLowerCase();
|
|
223
|
+
const definition = mathFunctions.get(name);
|
|
224
|
+
/** @type {CalculationType[]} */
|
|
225
|
+
const childTypes = [];
|
|
226
|
+
let valid = true;
|
|
227
|
+
let unresolvedArgs = false;
|
|
228
|
+
for (let index = 0; index < node.args.length; index++) {
|
|
229
|
+
const child = analyzeType(node.args[index], depth + 1);
|
|
230
|
+
childTypes.push(child.type);
|
|
231
|
+
valid = valid && child.valid;
|
|
232
|
+
if (!definition?.isKeyword?.(node.args[index], index) && child.unresolved) {
|
|
233
|
+
unresolvedArgs = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (!definition) return finish(unknownType, valid, true);
|
|
237
|
+
const type = definition.analyze(childTypes, node.args);
|
|
238
|
+
const unresolvedType = type.kind === 'unknown';
|
|
239
|
+
return finish(
|
|
240
|
+
type,
|
|
241
|
+
valid && !isFailure(type),
|
|
242
|
+
unresolvedArgs || unresolvedType
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** @param {CalculationType} type @param {boolean} valid @param {boolean} unresolved @return {{type: CalculationType, valid: boolean, unresolved: boolean}} */
|
|
247
|
+
function finish(type, valid, unresolved) {
|
|
248
|
+
return {
|
|
249
|
+
type,
|
|
250
|
+
valid: valid && !isFailure(type),
|
|
251
|
+
unresolved,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** @param {CalculationType} type @return {{type: CalculationType, valid: boolean, unresolved: boolean}} */
|
|
256
|
+
function resolved(type) {
|
|
257
|
+
return finish(type, true, false);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** @param {CalculationType} type @return {{type: CalculationType, valid: boolean, unresolved: boolean}} */
|
|
261
|
+
function markUnresolved(type) {
|
|
262
|
+
return finish(type, true, true);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** @param {CalculationType} type @return {AnalysisType} */
|
|
266
|
+
function publicType(type) {
|
|
267
|
+
if (type.kind === 'number') return 'number';
|
|
268
|
+
if (type.kind === 'unknown' || type.kind === 'failure') return 'unknown';
|
|
269
|
+
return { dimension: type.base };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export { analyze };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { TokenType as CssType } from '@csstools/css-tokenizer';
|
|
2
|
+
|
|
3
|
+
/** @typedef {import('@csstools/css-tokenizer').CSSToken} CSSToken */
|
|
4
|
+
|
|
5
|
+
const BLOCK_CLOSE = new Map([
|
|
6
|
+
[CssType.Function, CssType.CloseParen],
|
|
7
|
+
[CssType.OpenParen, CssType.CloseParen],
|
|
8
|
+
[CssType.OpenSquare, CssType.CloseSquare],
|
|
9
|
+
[CssType.OpenCurly, CssType.CloseCurly],
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read-only delimiter navigation for one native token stream range.
|
|
14
|
+
*/
|
|
15
|
+
class BlockIndex {
|
|
16
|
+
/** @type {CSSToken[]} */
|
|
17
|
+
#tokens;
|
|
18
|
+
/** @type {number} */
|
|
19
|
+
#rangeStart;
|
|
20
|
+
/** @type {number} */
|
|
21
|
+
#rangeEnd;
|
|
22
|
+
/** @type {Map<number, number>} */
|
|
23
|
+
#closes = new Map();
|
|
24
|
+
/** @type {number} */
|
|
25
|
+
#maxDepth = 0;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build the delimiter index once for a token stream range. Matching remains
|
|
29
|
+
* LIFO: a mismatched closer is ignored and does not disturb the open stack.
|
|
30
|
+
*
|
|
31
|
+
* @param {CSSToken[]} tokens
|
|
32
|
+
* @param {number} start
|
|
33
|
+
* @param {number} end
|
|
34
|
+
*/
|
|
35
|
+
constructor(tokens, start, end) {
|
|
36
|
+
this.#tokens = tokens;
|
|
37
|
+
this.#rangeStart = Math.max(0, start);
|
|
38
|
+
this.#rangeEnd = Math.min(tokens.length, Math.max(this.#rangeStart, end));
|
|
39
|
+
/** @type {{index: number, close: import('@csstools/css-tokenizer').TokenType}[]} */
|
|
40
|
+
const stack = [];
|
|
41
|
+
|
|
42
|
+
for (let i = this.#rangeStart; i < this.#rangeEnd; i++) {
|
|
43
|
+
const type = tokens[i][0];
|
|
44
|
+
const close = BLOCK_CLOSE.get(type);
|
|
45
|
+
if (close !== undefined) {
|
|
46
|
+
stack.push({ index: i, close });
|
|
47
|
+
this.#maxDepth = Math.max(this.#maxDepth, stack.length);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
type === CssType.CloseParen ||
|
|
53
|
+
type === CssType.CloseSquare ||
|
|
54
|
+
type === CssType.CloseCurly
|
|
55
|
+
) {
|
|
56
|
+
const open = stack.at(-1);
|
|
57
|
+
if (open === undefined || open.close !== type) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
stack.pop();
|
|
61
|
+
this.#closes.set(open.index, i);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @return {number} */
|
|
67
|
+
get maxDepth() {
|
|
68
|
+
return this.#maxDepth;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @param {number | undefined} end @return {number} */
|
|
72
|
+
#boundedEnd(end) {
|
|
73
|
+
return Math.min(
|
|
74
|
+
this.#rangeEnd,
|
|
75
|
+
Math.max(this.#rangeStart, end ?? this.#rangeEnd)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @param {number} openPosition @param {number} [endPosition] @return {number} */
|
|
80
|
+
closeOf(openPosition, endPosition) {
|
|
81
|
+
const close = this.#closes.get(openPosition) ?? -1;
|
|
82
|
+
const bound = this.#boundedEnd(endPosition);
|
|
83
|
+
return close >= this.#rangeStart && close < bound ? close : -1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** @param {number} position @param {number} endPosition @return {number} */
|
|
87
|
+
nextComponent(position, endPosition) {
|
|
88
|
+
const bound = this.#boundedEnd(endPosition);
|
|
89
|
+
if (position < this.#rangeStart || position >= bound) return bound;
|
|
90
|
+
const close = this.closeOf(position, bound);
|
|
91
|
+
return close === -1 ? position + 1 : close + 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** @param {number} startPosition @param {number} endPosition @return {number} */
|
|
95
|
+
firstTopLevelComma(startPosition, endPosition) {
|
|
96
|
+
const bound = this.#boundedEnd(endPosition);
|
|
97
|
+
for (let i = Math.max(this.#rangeStart, startPosition); i < bound;) {
|
|
98
|
+
if (this.#tokens[i][0] === CssType.Comma) return i;
|
|
99
|
+
i = this.nextComponent(i, bound);
|
|
100
|
+
}
|
|
101
|
+
return -1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build the delimiter index once for a token stream range. Matching remains
|
|
107
|
+
* LIFO: a mismatched closer is ignored and does not disturb the open stack.
|
|
108
|
+
*
|
|
109
|
+
* @param {CSSToken[]} tokens
|
|
110
|
+
* @param {number} [start]
|
|
111
|
+
* @param {number} [end]
|
|
112
|
+
* @return {BlockIndex}
|
|
113
|
+
*/
|
|
114
|
+
function indexBlocks(tokens, start = 0, end = tokens.length) {
|
|
115
|
+
return new BlockIndex(tokens, start, end);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export { indexBlocks };
|