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.
Files changed (49) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +47 -35
  3. package/package.json +15 -15
  4. package/src/index.js +2 -1
  5. package/src/lib/analyze.js +0 -6
  6. package/src/lib/compile.js +1 -1
  7. package/src/lib/function-analyzers.js +119 -0
  8. package/src/lib/functions.js +25 -175
  9. package/src/lib/node.js +61 -4
  10. package/src/lib/parser/opaque.js +152 -0
  11. package/src/lib/parser/tokens.js +242 -0
  12. package/src/lib/parser.js +32 -377
  13. package/src/lib/serialize/expression.js +329 -0
  14. package/src/lib/serialize/precedence.js +47 -0
  15. package/src/lib/serialize/precision.js +244 -0
  16. package/src/lib/serialize.js +24 -561
  17. package/src/lib/simplify/bucket.js +22 -5
  18. package/src/lib/simplify/cancel.js +35 -11
  19. package/src/lib/simplify/clamp.js +2 -2
  20. package/src/lib/simplify/exact.js +18 -0
  21. package/src/lib/simplify/fold.js +15 -1
  22. package/src/lib/simplify/min-max.js +2 -2
  23. package/src/lib/simplify/product-barrier.js +55 -0
  24. package/src/lib/simplify/product-quotient.js +141 -0
  25. package/src/lib/simplify/product.js +92 -48
  26. package/src/lib/simplify/sum.js +6 -2
  27. package/src/lib/simplify.js +22 -5
  28. package/src/lib/types.js +94 -0
  29. package/src/reduce.js +2 -1
  30. package/types/lib/function-analyzers.d.ts +27 -0
  31. package/types/lib/functions.d.ts +3 -28
  32. package/types/lib/node.d.ts +17 -2
  33. package/types/lib/parser/opaque.d.ts +12 -0
  34. package/types/lib/parser/tokens.d.ts +83 -0
  35. package/types/lib/parser.d.ts +4 -44
  36. package/types/lib/serialize/expression.d.ts +55 -0
  37. package/types/lib/serialize/precedence.d.ts +18 -0
  38. package/types/lib/serialize/precision.d.ts +102 -0
  39. package/types/lib/simplify/bucket.d.ts +4 -1
  40. package/types/lib/simplify/cancel.d.ts +11 -7
  41. package/types/lib/simplify/exact.d.ts +9 -0
  42. package/types/lib/simplify/fold.d.ts +14 -1
  43. package/types/lib/simplify/product-barrier.d.ts +26 -0
  44. package/types/lib/simplify/product-quotient.d.ts +48 -0
  45. package/types/lib/simplify/product.d.ts +3 -7
  46. package/types/lib/simplify/sum.d.ts +2 -1
  47. package/types/lib/simplify.d.ts +3 -8
  48. package/types/lib/types.d.ts +48 -0
  49. 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 multiple units are mixed together in the same expression, the `calc()`
8
- statement is left as is, to fallback to the [W3C calc() implementation].
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(1.10417in)'
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`, the deprecated
72
- `unwrapSingleNegativeNumber` alias,
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
- 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.
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 to parse or simplify. Matches
205
- [`@csstools/css-calc`][csstools-css-calc]'s shape:
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.** Commutative operands serialize
231
- numeric-first, matching [`@csstools/css-calc`][csstools-css-calc]:
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
- npm install
274
- npm test
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
- 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.
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.1",
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.4.2"
41
+ "version": "12.10.1"
42
42
  }
43
43
  },
44
44
  "devDependencies": {
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",
50
- "postcss": "^8.5.28",
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.1"
54
+ "@csstools/css-tokenizer": "^4.0.2"
55
55
  },
56
56
  "peerDependencies": {
57
- "postcss": "^8.5.28"
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.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",
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 {
@@ -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) {
@@ -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
+ };
@@ -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 { ROUND_STRATEGIES, simplifyRound } from './simplify/round.js';
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 }],