postcss-calc 11.2.2 → 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/README.md CHANGED
@@ -6,7 +6,8 @@
6
6
  [PostCSS Calc] lets you reduce `calc()` references whenever it's possible.
7
7
  When an expression mixes units that cannot be combined exactly (such as `px`
8
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
+ unresolved part is left for the browser's [CSS Values 4][css-values-4]
10
+ implementation.
10
11
 
11
12
  ## Installation
12
13
 
@@ -69,9 +70,8 @@ reduceCalc('min(50px, calc(2 * 40px))');
69
70
  // => 'calc(50px)'
70
71
  ```
71
72
 
72
- It accepts `precision`, `unwrapSingleValue`, the deprecated
73
- `unwrapSingleNegativeNumber` alias,
74
- `warnWhenCannotResolve`, `onParseError`, and `onWarn`:
73
+ It accepts `precision`, `unwrapSingleValue`, `warnWhenCannotResolve`,
74
+ `onParseError`, and `onWarn`:
75
75
 
76
76
  ```js
77
77
  const result = reduceCalc('calc(100% + var(--gap))', {
@@ -191,20 +191,20 @@ Example:
191
191
  With `mediaQueries: true`, this becomes:
192
192
 
193
193
  ```css
194
- @media (min-width: 200px) {
194
+ @media (min-width: calc(200px)) {
195
195
  div {
196
196
  width: 100px;
197
197
  }
198
198
  }
199
199
  ```
200
200
 
201
+ Add `unwrapSingleValue: true` to get the bare value, `(min-width: 200px)`.
202
+
201
203
  #### `selectors` (default: `false`)
202
204
 
203
205
  Reduces `calc()` functions found in selectors. Selectors do not accept
204
- `calc()` functions, so the plugin replaces them with their reduced values.
205
- Finite negative and fractional unitless results are serialized as bare values
206
- because a selector cannot contain a `calc()` function; the plugin enables the
207
- `unwrapSingleValue` automatically for selectors.
206
+ `calc()` functions, so the plugin replaces them with their reduced values, as
207
+ if `unwrapSingleValue` were enabled.
208
208
 
209
209
  ```js
210
210
  var out = postcss()
@@ -224,8 +224,8 @@ With `selectors: true`, this becomes `div:nth-child(3)`.
224
224
 
225
225
  #### `onParseError`
226
226
 
227
- Callback invoked when a `calc()` body fails to parse or simplify. Matches
228
- [`@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:
229
229
 
230
230
  ```js
231
231
  postcss().use(
@@ -250,13 +250,13 @@ canonical-form decisions:
250
250
  - **Strict whitespace (§10.1).** `calc(2px+3px)` is invalid CSS (binary
251
251
  `+` / `-` require surrounding whitespace) and is preserved with a
252
252
  warning instead of reduced.
253
- - **Canonical operand order.** Commutative operands serialize
254
- 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:
255
255
  `calc(var(--foo) + 10px)` → `calc(10px + var(--foo))`.
256
256
  - **Zero buckets are kept.** `calc(100px - (100px - 100%))` →
257
257
  `calc(0px + 100%)`, not `100%` — [WPT calc-serialization-002][wpt-calc-serialization]
258
258
  requires the zero term because it carries the length-percentage type.
259
- - **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).
260
260
  Previously `pi` / `e` stayed symbolic.
261
261
  - **Distributive multiplication.** `calc(0.5 * (100vw - 10px))` becomes
262
262
  `calc(50vw - 5px)`.
@@ -270,7 +270,6 @@ canonical-form decisions:
270
270
  if you want validation behavior.
271
271
 
272
272
  [css-values-4]: https://www.w3.org/TR/css-values-4/
273
- [csstools-css-calc]: https://www.npmjs.com/package/@csstools/css-calc
274
273
  [wpt-calc-serialization]: https://github.com/web-platform-tests/wpt/blob/master/css/css-values/calc-serialization-002.html
275
274
  [jison]: https://github.com/zaach/jison
276
275
 
@@ -288,6 +287,7 @@ for the full guidelines. The project uses [pnpm](https://pnpm.io/).
288
287
 
289
288
  ```bash
290
289
  git clone git@github.com:postcss/postcss-calc.git
290
+ cd postcss-calc
291
291
  git checkout -b patch-1
292
292
  pnpm install
293
293
  pnpm test
@@ -302,23 +302,8 @@ when changing parsing/simplification behavior:
302
302
  pnpm test:corpus:full
303
303
  ```
304
304
 
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.
318
-
319
- The PostCSS benchmark awaits `postcss().process(...)`, and that await already
320
- triggers result stringification. It therefore does not add a redundant
321
- `result.css` access.
305
+ Check performance changes with the benchmarks described in
306
+ [BENCHMARKS.md](BENCHMARKS.md), and read it before interpreting results.
322
307
 
323
308
  ## [Changelog](CHANGELOG.md)
324
309
 
@@ -332,4 +317,3 @@ triggers result stringification. It therefore does not add a redundant
332
317
  [PostCSS Calc]: https://github.com/postcss/postcss-calc
333
318
  [PostCSS Custom Properties]: https://github.com/postcss/postcss-custom-properties
334
319
  [tests]: test/
335
- [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.2",
3
+ "version": "11.2.3",
4
4
  "type": "module",
5
5
  "description": "PostCSS plugin to reduce calc()",
6
6
  "keywords": [
@@ -38,23 +38,23 @@
38
38
  "devEngines": {
39
39
  "packageManager": {
40
40
  "name": "pnpm",
41
- "version": "12.8.1"
41
+ "version": "12.10.1"
42
42
  }
43
43
  },
44
44
  "devDependencies": {
45
- "@csstools/css-calc": "^3.4.1",
46
- "@types/node": "^26.6.3",
45
+ "@csstools/css-calc": "^3.4.3",
46
+ "@types/node": "^26.6.4",
47
47
  "fast-check": "^4.10.2",
48
- "oxfmt": "^0.71.0",
49
- "oxlint": "^1.86.0",
50
- "postcss": "^8.5.28",
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
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",
@@ -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 }],
@@ -71,10 +71,11 @@ function customProperty(tokens, start, end) {
71
71
  /**
72
72
  * Append the component tree of `[start, end)` to `root` and return it.
73
73
  * Callers seed `root` so leading components need no copy or spread.
74
- * @param {ParseInput} input @param {number} start @param {number} end @param {ParseRange} parseRange @param {OpaqueComponent[]} root @return {OpaqueComponent[]}
74
+ * @param {ParseInput} input @param {number} start @param {number} rangeEnd @param {ParseRange} parseRange @param {OpaqueComponent[]} root @return {OpaqueComponent[]}
75
75
  */
76
- function componentTree(input, start, end, parseRange, root) {
76
+ function componentTree(input, start, rangeEnd, parseRange, root) {
77
77
  /** @type {OpaqueComponent[]} */ let tree = root;
78
+ let end = rangeEnd;
78
79
  /** @type {{parent: OpaqueComponent[], tree: OpaqueComponent[], close: number, end: number}[]} */
79
80
  const frames = [];
80
81
  const { tokens } = input;