postcss-calc 11.0.0-rc.1 → 11.0.0-rc.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 (57) hide show
  1. package/README.md +23 -30
  2. package/package.json +25 -20
  3. package/src/index.js +39 -18
  4. package/src/lib/convertUnits.js +58 -21
  5. package/src/lib/node.js +10 -9
  6. package/src/lib/parser.js +17 -13
  7. package/src/lib/serialize.js +46 -16
  8. package/src/lib/simplify/abs.js +8 -6
  9. package/src/lib/simplify/atan2.js +14 -10
  10. package/src/lib/simplify/bucket.js +16 -11
  11. package/src/lib/simplify/call.js +57 -31
  12. package/src/lib/simplify/cancel.js +2 -4
  13. package/src/lib/simplify/clamp.js +3 -5
  14. package/src/lib/simplify/exp.js +2 -4
  15. package/src/lib/simplify/fold.js +14 -12
  16. package/src/lib/simplify/hypot.js +6 -6
  17. package/src/lib/simplify/inverse-trig.js +11 -7
  18. package/src/lib/simplify/log.js +3 -9
  19. package/src/lib/simplify/min-max.js +3 -5
  20. package/src/lib/simplify/mod-rem.js +23 -13
  21. package/src/lib/simplify/pow.js +2 -4
  22. package/src/lib/simplify/product.js +11 -9
  23. package/src/lib/simplify/round.js +24 -12
  24. package/src/lib/simplify/sign.js +8 -6
  25. package/src/lib/simplify/sqrt.js +2 -5
  26. package/src/lib/simplify/sum.js +26 -8
  27. package/src/lib/simplify/trig.js +12 -8
  28. package/src/lib/simplify.js +4 -8
  29. package/src/lib/tokenizer.js +11 -13
  30. package/types/index.d.ts +17 -17
  31. package/types/lib/convertUnits.d.ts +4 -3
  32. package/types/lib/node.d.ts +16 -20
  33. package/types/lib/parser.d.ts +13 -13
  34. package/types/lib/serialize.d.ts +8 -7
  35. package/types/lib/simplify/abs.d.ts +3 -2
  36. package/types/lib/simplify/atan2.d.ts +3 -2
  37. package/types/lib/simplify/bucket.d.ts +3 -2
  38. package/types/lib/simplify/call.d.ts +5 -4
  39. package/types/lib/simplify/cancel.d.ts +2 -1
  40. package/types/lib/simplify/clamp.d.ts +3 -2
  41. package/types/lib/simplify/exp.d.ts +3 -2
  42. package/types/lib/simplify/fold.d.ts +4 -3
  43. package/types/lib/simplify/hypot.d.ts +3 -2
  44. package/types/lib/simplify/inverse-trig.d.ts +3 -2
  45. package/types/lib/simplify/log.d.ts +3 -2
  46. package/types/lib/simplify/min-max.d.ts +3 -2
  47. package/types/lib/simplify/mod-rem.d.ts +3 -2
  48. package/types/lib/simplify/pow.d.ts +3 -2
  49. package/types/lib/simplify/product.d.ts +6 -5
  50. package/types/lib/simplify/round.d.ts +4 -3
  51. package/types/lib/simplify/sign.d.ts +3 -2
  52. package/types/lib/simplify/sqrt.d.ts +3 -2
  53. package/types/lib/simplify/sum.d.ts +7 -6
  54. package/types/lib/simplify/trig.d.ts +3 -2
  55. package/types/lib/simplify.d.ts +3 -6
  56. package/types/lib/tokenizer.d.ts +4 -3
  57. package/types/lib/type.d.ts +8 -1
package/README.md CHANGED
@@ -17,18 +17,15 @@ npm install postcss-calc
17
17
 
18
18
  ```js
19
19
  // dependencies
20
- var fs = require("fs")
21
- var postcss = require("postcss")
22
- var calc = require("postcss-calc")
20
+ var fs = require('fs');
21
+ var postcss = require('postcss');
22
+ var calc = require('postcss-calc');
23
23
 
24
24
  // css to be processed
25
- var css = fs.readFileSync("input.css", "utf8")
25
+ var css = fs.readFileSync('input.css', 'utf8');
26
26
 
27
27
  // process css
28
- var output = postcss()
29
- .use(calc())
30
- .process(css)
31
- .css
28
+ var output = postcss().use(calc()).process(css).css;
32
29
  ```
33
30
 
34
31
  Using this `input.css`:
@@ -37,7 +34,7 @@ Using this `input.css`:
37
34
  h1 {
38
35
  font-size: calc(16px * 2);
39
36
  height: calc(100px - 2em);
40
- width: calc(2*var(--base-width));
37
+ width: calc(2 * var(--base-width));
41
38
  margin-bottom: calc(16px * 1.5);
42
39
  }
43
40
  ```
@@ -48,10 +45,11 @@ you will get:
48
45
  h1 {
49
46
  font-size: 32px;
50
47
  height: calc(100px - 2em);
51
- width: calc(2*var(--base-width));
52
- margin-bottom: 24px
48
+ width: calc(2 * var(--base-width));
49
+ margin-bottom: 24px;
53
50
  }
54
51
  ```
52
+
55
53
  Checkout [tests] for more examples.
56
54
 
57
55
  ### Options
@@ -62,9 +60,8 @@ Allow you to define the precision for decimal numbers.
62
60
 
63
61
  ```js
64
62
  var out = postcss()
65
- .use(calc({precision: 10}))
66
- .process(css)
67
- .css
63
+ .use(calc({ precision: 10 }))
64
+ .process(css).css;
68
65
  ```
69
66
 
70
67
  #### `preserve` (default: `false`)
@@ -74,9 +71,8 @@ precision themselves.
74
71
 
75
72
  ```js
76
73
  var out = postcss()
77
- .use(calc({preserve: true}))
78
- .process(css)
79
- .css
74
+ .use(calc({ preserve: true }))
75
+ .process(css).css;
80
76
  ```
81
77
 
82
78
  #### `warnWhenCannotResolve` (default: `false`)
@@ -85,9 +81,8 @@ Adds warnings when calc() are not reduced to a single value.
85
81
 
86
82
  ```js
87
83
  var out = postcss()
88
- .use(calc({warnWhenCannotResolve: true}))
89
- .process(css)
90
- .css
84
+ .use(calc({ warnWhenCannotResolve: true }))
85
+ .process(css).css;
91
86
  ```
92
87
 
93
88
  #### `mediaQueries` (default: `false`)
@@ -96,9 +91,8 @@ Allows calc() usage as part of media query declarations.
96
91
 
97
92
  ```js
98
93
  var out = postcss()
99
- .use(calc({mediaQueries: true}))
100
- .process(css)
101
- .css
94
+ .use(calc({ mediaQueries: true }))
95
+ .process(css).css;
102
96
  ```
103
97
 
104
98
  #### `selectors` (default: `false`)
@@ -107,15 +101,14 @@ Allows calc() usage as part of selectors.
107
101
 
108
102
  ```js
109
103
  var out = postcss()
110
- .use(calc({selectors: true}))
111
- .process(css)
112
- .css
104
+ .use(calc({ selectors: true }))
105
+ .process(css).css;
113
106
  ```
114
107
 
115
108
  Example:
116
109
 
117
110
  ```css
118
- div[data-size="calc(3*3)"] {
111
+ div[data-size='calc(3*3)'] {
119
112
  width: 100px;
120
113
  }
121
114
  ```
@@ -129,8 +122,8 @@ Callback invoked when a `calc()` body fails to parse or simplify. Matches
129
122
  calc({
130
123
  onParseError: (err, input) => {
131
124
  throw err; // or log, route to a different channel, etc.
132
- }
133
- })
125
+ },
126
+ });
134
127
  ```
135
128
 
136
129
  When omitted, errors are reported via PostCSS `result.warn()` so the
@@ -178,6 +171,7 @@ canonical-form decisions:
178
171
  ---
179
172
 
180
173
  ## Related PostCSS plugins
174
+
181
175
  To replace the value of CSS custom properties at build time, try [PostCSS Custom Properties].
182
176
 
183
177
  ## Contributing
@@ -200,7 +194,6 @@ npm test
200
194
  [git-url]: https://gitter.im/postcss/postcss
201
195
  [npm-img]: https://img.shields.io/npm/v/postcss-calc.svg
202
196
  [npm-url]: https://www.npmjs.com/package/postcss-calc
203
-
204
197
  [PostCSS]: https://github.com/postcss
205
198
  [PostCSS Calc]: https://github.com/postcss/postcss-calc
206
199
  [PostCSS Custom Properties]: https://github.com/postcss/postcss-custom-properties
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "postcss-calc",
3
- "version": "11.0.0-rc.1",
3
+ "version": "11.0.0-rc.3",
4
+ "type": "module",
4
5
  "description": "PostCSS plugin to reduce calc()",
5
6
  "keywords": [
6
7
  "css",
@@ -14,8 +15,12 @@
14
15
  "type": "git",
15
16
  "url": "https://github.com/postcss/postcss-calc.git"
16
17
  },
17
- "main": "src/index.js",
18
- "types": "types/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./types/index.d.ts",
21
+ "default": "./src/index.js"
22
+ }
23
+ },
19
24
  "files": [
20
25
  "src",
21
26
  "types",
@@ -24,34 +29,34 @@
24
29
  "author": "Andy Jansson",
25
30
  "license": "MIT",
26
31
  "engines": {
27
- "node": "^22.12 || ^24.11 || >=26.0"
32
+ "node": "^22.18 || ^24.11 || >=26.0"
33
+ },
34
+ "devEngines": {
35
+ "packageManager": {
36
+ "name": "pnpm",
37
+ "version": "11.21.0"
38
+ }
28
39
  },
29
40
  "devDependencies": {
30
- "@csstools/css-calc": "^3.2.1",
31
- "@eslint/js": "^10.0.1",
41
+ "@csstools/css-calc": "^3.3.0",
32
42
  "@rmenke/css-tokenizer-tests": "^1.2.0",
33
- "@stryker-mutator/core": "^9.6.1",
34
- "@stryker-mutator/tap-runner": "^9.6.1",
35
- "@stryker-mutator/typescript-checker": "^9.6.1",
36
- "@types/node": "^26.1.1",
37
- "eslint": "^10.7.0",
38
- "eslint-config-prettier": "^10.1.8",
39
- "eslint-plugin-sonarjs": "^4.2.0",
43
+ "@types/node": "^26.2.0",
40
44
  "fast-check": "^4.9.0",
41
- "postcss": "^8.5.19",
42
- "prettier": "^3.9.5",
43
- "typescript": "~6.0.3"
45
+ "oxfmt": "^0.63.0",
46
+ "oxlint": "^1.78.0",
47
+ "postcss": "^8.5.26",
48
+ "typescript": "~7.0.2"
44
49
  },
45
50
  "dependencies": {
46
51
  "@csstools/css-tokenizer": "^4.0.0",
47
52
  "postcss-value-parser": "^4.2.0"
48
53
  },
49
54
  "peerDependencies": {
50
- "postcss": "^8.5.19"
55
+ "postcss": "^8.5.25"
51
56
  },
52
57
  "scripts": {
53
- "lint": "eslint . && tsc",
54
- "test": "node --test",
55
- "test:mutation": "stryker run"
58
+ "lint": "oxlint . && tsc && oxfmt --check",
59
+ "fmt": "oxfmt",
60
+ "test": "node --test"
56
61
  }
57
62
  }
package/src/index.js CHANGED
@@ -1,26 +1,37 @@
1
- 'use strict';
2
-
3
1
  // PostCSS adapter. Walks declaration values (and optionally @rule params
4
2
  // and selectors), feeds calc() bodies through tokenize → parse → simplify
5
3
  // → serialize, and writes the result back.
6
-
7
- const valueParser = require('postcss-value-parser');
8
-
9
- const { tokenize } = require('./lib/tokenizer.js');
10
- const { parse } = require('./lib/parser.js');
11
- const { simplify } = require('./lib/simplify.js');
12
- const { serialize } = require('./lib/serialize.js');
4
+ import valueParser from 'postcss-value-parser';
5
+ import { tokenize } from './lib/tokenizer.js';
6
+ import { parse } from './lib/parser.js';
7
+ import { simplify } from './lib/simplify.js';
8
+ import { serialize } from './lib/serialize.js';
13
9
 
14
10
  const MATCH_CALC = /^(?:-(?:moz|webkit)-)?calc$/i;
15
11
 
16
12
  // Bare math-function calls (no calc() wrapper) — fed to the same pipeline.
17
13
  // Mirrors the dispatch in lib/simplify/call.js.
18
14
  const MATH_FUNCTIONS = new Set([
19
- 'min', 'max', 'clamp',
20
- 'abs', 'sign',
21
- 'mod', 'rem', 'round',
22
- 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2',
23
- 'pow', 'sqrt', 'hypot', 'log', 'exp',
15
+ 'min',
16
+ 'max',
17
+ 'clamp',
18
+ 'abs',
19
+ 'sign',
20
+ 'mod',
21
+ 'rem',
22
+ 'round',
23
+ 'sin',
24
+ 'cos',
25
+ 'tan',
26
+ 'asin',
27
+ 'acos',
28
+ 'atan',
29
+ 'atan2',
30
+ 'pow',
31
+ 'sqrt',
32
+ 'hypot',
33
+ 'log',
34
+ 'exp',
24
35
  ]);
25
36
 
26
37
  /**
@@ -45,10 +56,14 @@ const MATH_FUNCTIONS = new Set([
45
56
  function transformValue(value, options, result, item) {
46
57
  return valueParser(value)
47
58
  .walk((node) => {
48
- if (node.type !== 'function') {return;}
59
+ if (node.type !== 'function') {
60
+ return;
61
+ }
49
62
  const isCalc = MATCH_CALC.test(node.value);
50
63
  const isMath = !isCalc && MATH_FUNCTIONS.has(node.value.toLowerCase());
51
- if (!isCalc && !isMath) {return;}
64
+ if (!isCalc && !isMath) {
65
+ return;
66
+ }
52
67
 
53
68
  // calc(): feed the body. Bare math: feed the whole call.
54
69
  const inner = valueParser.stringify(node.nodes);
@@ -85,7 +100,6 @@ function transformValue(value, options, result, item) {
85
100
  }
86
101
 
87
102
  /**
88
- * @type {import('postcss').PluginCreator<PluginOptions>}
89
103
  * @param {PluginOptions} [opts]
90
104
  * @return {import('postcss').Plugin}
91
105
  */
@@ -102,6 +116,10 @@ function pluginCreator(opts) {
102
116
 
103
117
  return {
104
118
  postcssPlugin: 'postcss-calc',
119
+ /**
120
+ * @param {import('postcss').Root} css
121
+ * @param {import('postcss').Helpers} helpers
122
+ */
105
123
  OnceExit(css, { result }) {
106
124
  css.walk((node) => {
107
125
  if (node.type === 'decl') {
@@ -143,4 +161,7 @@ function pluginCreator(opts) {
143
161
 
144
162
  pluginCreator.postcss = true;
145
163
 
146
- module.exports = pluginCreator;
164
+ export default /** @type import('postcss').PluginCreator<PluginOptions>*/ (
165
+ pluginCreator
166
+ );
167
+ export { pluginCreator as 'module.exports' };
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  // Spec: https://www.w3.org/TR/css-values-4/#calc-type-checking
4
2
 
5
3
  /**
@@ -8,28 +6,67 @@
8
6
 
9
7
  /** @type {Record<string, BaseType>} */
10
8
  const UNIT_TO_BASE = {
11
- px: 'length', cm: 'length', mm: 'length', q: 'length',
12
- in: 'length', pt: 'length', pc: 'length',
13
- em: 'length', ex: 'length', ch: 'length', rem: 'length',
14
- lh: 'length', rlh: 'length', ic: 'length', cap: 'length',
15
- vw: 'length', vh: 'length', vmin: 'length', vmax: 'length',
16
- vb: 'length', vi: 'length',
17
- svw: 'length', svh: 'length', svmin: 'length', svmax: 'length',
18
- svb: 'length', svi: 'length',
19
- lvw: 'length', lvh: 'length', lvmin: 'length', lvmax: 'length',
20
- lvb: 'length', lvi: 'length',
21
- dvw: 'length', dvh: 'length', dvmin: 'length', dvmax: 'length',
22
- dvb: 'length', dvi: 'length',
23
- cqw: 'length', cqh: 'length', cqi: 'length', cqb: 'length',
24
- cqmin: 'length', cqmax: 'length',
9
+ px: 'length',
10
+ cm: 'length',
11
+ mm: 'length',
12
+ q: 'length',
13
+ in: 'length',
14
+ pt: 'length',
15
+ pc: 'length',
16
+ em: 'length',
17
+ ex: 'length',
18
+ ch: 'length',
19
+ rem: 'length',
20
+ lh: 'length',
21
+ rlh: 'length',
22
+ ic: 'length',
23
+ cap: 'length',
24
+ vw: 'length',
25
+ vh: 'length',
26
+ vmin: 'length',
27
+ vmax: 'length',
28
+ vb: 'length',
29
+ vi: 'length',
30
+ svw: 'length',
31
+ svh: 'length',
32
+ svmin: 'length',
33
+ svmax: 'length',
34
+ svb: 'length',
35
+ svi: 'length',
36
+ lvw: 'length',
37
+ lvh: 'length',
38
+ lvmin: 'length',
39
+ lvmax: 'length',
40
+ lvb: 'length',
41
+ lvi: 'length',
42
+ dvw: 'length',
43
+ dvh: 'length',
44
+ dvmin: 'length',
45
+ dvmax: 'length',
46
+ dvb: 'length',
47
+ dvi: 'length',
48
+ cqw: 'length',
49
+ cqh: 'length',
50
+ cqi: 'length',
51
+ cqb: 'length',
52
+ cqmin: 'length',
53
+ cqmax: 'length',
25
54
 
26
- deg: 'angle', grad: 'angle', rad: 'angle', turn: 'angle',
55
+ deg: 'angle',
56
+ grad: 'angle',
57
+ rad: 'angle',
58
+ turn: 'angle',
27
59
 
28
- s: 'time', ms: 'time',
60
+ s: 'time',
61
+ ms: 'time',
29
62
 
30
- hz: 'frequency', khz: 'frequency',
63
+ hz: 'frequency',
64
+ khz: 'frequency',
31
65
 
32
- dpi: 'resolution', dpcm: 'resolution', dppx: 'resolution', x: 'resolution',
66
+ dpi: 'resolution',
67
+ dpcm: 'resolution',
68
+ dppx: 'resolution',
69
+ x: 'resolution',
33
70
 
34
71
  fr: 'flex',
35
72
 
@@ -102,4 +139,4 @@ function convert(value, from, to) {
102
139
  return (value * f) / t;
103
140
  }
104
141
 
105
- module.exports = { baseOf, convert };
142
+ export { baseOf, convert };
package/src/lib/node.js CHANGED
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  // Canonical AST. N-ary Sum and Product with signed numeric leaves.
4
2
  // Invariants enforced by the constructors below:
5
3
  //
@@ -7,7 +5,9 @@
7
5
  // is `Num(-5)`, never `Sum([{sign:-1, Num(5)}])`. One form per value.
8
6
  // - In a SumTerm with Num/Dim node, sign is always +1; the sign slot is
9
7
  // reserved for opaque nodes (Ident, Call, Product, multi-term Sum).
10
- // - No Sum directly contains another Sum (flattened on construction).
8
+ // - No ungrouped Sum directly contains another Sum (flattened on
9
+ // construction). A grouped Sum is retained so a later negative sign
10
+ // cannot be distributed across opaque terms.
11
11
  // - No Product directly contains another Product (flattened).
12
12
  // - A Sum/Product with one positive element collapses to that element.
13
13
  // - A Sum/Product with no elements collapses to Num(0) / Num(1).
@@ -20,7 +20,7 @@
20
20
  * @typedef {{type: 'Ident', name: string}} Ident
21
21
  * @typedef {{type: 'Call', name: string, args: Node[]}} Call
22
22
  * @typedef {{sign: 1 | -1, node: Node}} SumTerm Sign is always +1 when node is Num or Dim.
23
- * @typedef {{type: 'Sum', terms: SumTerm[]}} Sum
23
+ * @typedef {{type: 'Sum', terms: SumTerm[], grouped?: boolean}} Sum
24
24
  * @typedef {{exponent: 1 | -1, node: Node}} ProductFactor exponent +1 = numerator, -1 = denominator.
25
25
  * @typedef {{type: 'Product', factors: ProductFactor[]}} Product
26
26
  * @typedef {Num | Dim | Ident | Call | Sum | Product} Node
@@ -87,7 +87,7 @@ function mkSum(rawTerms) {
87
87
  function pushSumTerm(out, term) {
88
88
  let { sign, node } = term;
89
89
 
90
- if (node.type === 'Sum') {
90
+ if (node.type === 'Sum' && !node.grouped) {
91
91
  for (const inner of node.terms) {
92
92
  pushSumTerm(out, {
93
93
  sign: /** @type {1 | -1} */ (sign * inner.sign),
@@ -172,18 +172,19 @@ function negate(node) {
172
172
  return dim(-node.value, node.unit);
173
173
  }
174
174
  if (node.type === 'Sum') {
175
- return mkSum(
175
+ const result = mkSum(
176
176
  node.terms.map((t) => ({
177
177
  sign: /** @type {1 | -1} */ (-t.sign),
178
178
  node: t.node,
179
179
  }))
180
180
  );
181
+ return node.grouped && result.type === 'Sum'
182
+ ? { ...result, grouped: true }
183
+ : result;
181
184
  }
182
185
  // Opaque (Ident, Call, Product): wrap as a single negative-sign term —
183
186
  // the only case where sign=-1 remains on a SumTerm.
184
187
  return { type: 'Sum', terms: [{ sign: -1, node }] };
185
188
  }
186
189
 
187
- // Stryker disable next-line all: instrumenting this line breaks Node's
188
- // cjs-module-lexer named-export detection for .mjs `import { x } from` consumers.
189
- module.exports = { num, dim, ident, call, mkSum, mkProduct, negate };
190
+ export { num, dim, ident, call, mkSum, mkProduct, negate };
package/src/lib/parser.js CHANGED
@@ -1,10 +1,7 @@
1
- 'use strict';
2
-
3
1
  // Pratt parser. +/- emit Sum nodes; */÷ emit Product nodes. node.js
4
- // constructors flatten and normalize on construction, so the parser
5
- // never produces a Binary node.
6
-
7
- const { mkSum, mkProduct, negate } = require('./node.js');
2
+ // constructors flatten and normalize on construction, while parenthesized
3
+ // sums retain a grouping marker for the opaque-subtraction invariant.
4
+ import { mkSum, mkProduct, negate } from './node.js';
8
5
 
9
6
  /**
10
7
  * @typedef {import('./tokenizer.js').Token} Token
@@ -157,7 +154,9 @@ const OPAQUE_ARG_FUNCTIONS = new Set(['anchor', 'anchor-size']);
157
154
  * @return {string}
158
155
  */
159
156
  function tokenText(t) {
160
- if (t.type === 'dimension') {return `${t.value}${t.unit ?? ''}`;}
157
+ if (t.type === 'dimension') {
158
+ return `${t.value}${t.unit ?? ''}`;
159
+ }
161
160
  return t.value;
162
161
  }
163
162
 
@@ -174,7 +173,9 @@ function parseOpaqueCall(p, name) {
174
173
  let depth = 1;
175
174
  const flush = () => {
176
175
  const trimmed = buf.trim();
177
- if (trimmed) {args.push({ type: 'Ident', name: trimmed });}
176
+ if (trimmed) {
177
+ args.push({ type: 'Ident', name: trimmed });
178
+ }
178
179
  buf = '';
179
180
  };
180
181
  while (true) {
@@ -183,8 +184,9 @@ function parseOpaqueCall(p, name) {
183
184
  throw new Error(`Unclosed ${name}( at position ${tk.pos}`);
184
185
  }
185
186
  if (tk.type === 'punct') {
186
- if (tk.value === '(') {depth++;}
187
- else if (tk.value === ')') {
187
+ if (tk.value === '(') {
188
+ depth++;
189
+ } else if (tk.value === ')') {
188
190
  depth--;
189
191
  if (depth === 0) {
190
192
  p.next();
@@ -197,7 +199,9 @@ function parseOpaqueCall(p, name) {
197
199
  continue;
198
200
  }
199
201
  }
200
- if (tk.ws && buf) {buf += ' ';}
202
+ if (tk.ws && buf) {
203
+ buf += ' ';
204
+ }
201
205
  buf += tokenText(tk);
202
206
  p.next();
203
207
  }
@@ -262,7 +266,7 @@ const PREFIX = {
262
266
  '(': (p) => {
263
267
  const e = p.parseExpr(0);
264
268
  p.expect('punct', ')');
265
- return e;
269
+ return e.type === 'Sum' ? { ...e, grouped: true } : e;
266
270
  },
267
271
 
268
272
  '-': (p) => negate(p.parseExpr(UNARY_BP)),
@@ -311,4 +315,4 @@ function parse(tokens) {
311
315
  return ast;
312
316
  }
313
317
 
314
- module.exports = { parse };
318
+ export { parse };
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  // Spec: https://www.w3.org/TR/css-values-4/#serialize-a-calculation-tree
4
2
  // Outer calc() is added only when the top-level result contains an
5
3
  // arithmetic operator. A Sum inside a Product is the only place parens
@@ -55,7 +53,9 @@ function isDegenerate(v) {
55
53
  * @return {string}
56
54
  */
57
55
  function degenerateKeyword(v) {
58
- if (isNaN(v)) {return 'NaN';}
56
+ if (isNaN(v)) {
57
+ return 'NaN';
58
+ }
59
59
  return v > 0 ? 'infinity' : '-infinity';
60
60
  }
61
61
 
@@ -77,6 +77,25 @@ function serialize(node, opts = {}) {
77
77
  return `${calcName}(${degenerateKeyword(node.value)} * 1${node.unit})`;
78
78
  }
79
79
 
80
+ // A grouped sum with a leading negative term is the canonical result of
81
+ // negating a parenthesized expression. Re-invert its terms for the body so
82
+ // the grouping survives as `-(...)` instead of becoming `-a - b`.
83
+ if (
84
+ node.type === 'Sum' &&
85
+ node.grouped &&
86
+ node.terms.length > 1 &&
87
+ displaySign(node.terms[0]).sign === -1
88
+ ) {
89
+ const body = /** @type {Sum} */ ({
90
+ type: 'Sum',
91
+ terms: node.terms.map((t) => ({
92
+ sign: /** @type {1 | -1} */ (-t.sign),
93
+ node: t.node,
94
+ })),
95
+ });
96
+ return `${calcName}(-(${serializeExpr(body, prec)}))`;
97
+ }
98
+
80
99
  if (
81
100
  node.type === 'Num' ||
82
101
  node.type === 'Dim' ||
@@ -105,7 +124,9 @@ function serialize(node, opts = {}) {
105
124
  function serializeExpr(node, prec) {
106
125
  switch (node.type) {
107
126
  case 'Num':
108
- if (isDegenerate(node.value)) {return degenerateKeyword(node.value);}
127
+ if (isDegenerate(node.value)) {
128
+ return degenerateKeyword(node.value);
129
+ }
109
130
  return String(round(node.value, prec));
110
131
  case 'Dim':
111
132
  if (isDegenerate(node.value)) {
@@ -161,18 +182,27 @@ function displaySign(term) {
161
182
  */
162
183
  function serializeSum(sum, prec) {
163
184
  let out = '';
164
- sum.terms.forEach((t, i) => {
185
+ for (const [i, t] of sum.terms.entries()) {
165
186
  const { sign, magnitude } = displaySign(t);
166
187
  if (i === 0) {
167
- out = sign === 1
168
- ? serializeExpr(magnitude, prec)
169
- : serializeLeadingNeg(magnitude, prec);
188
+ if (magnitude.type === 'Sum' && magnitude.grouped) {
189
+ const body = `(${serializeExpr(magnitude, prec)})`;
190
+ out = sign === 1 ? body : `-${body}`;
191
+ continue;
192
+ }
193
+ out =
194
+ sign === 1
195
+ ? serializeExpr(magnitude, prec)
196
+ : serializeLeadingNeg(magnitude, prec);
170
197
  } else {
171
198
  // `-` binds looser than `*`/`/` so the right side never needs parens.
172
- const body = serializeExpr(magnitude, prec);
199
+ let body = serializeExpr(magnitude, prec);
200
+ if (magnitude.type === 'Sum' && magnitude.grouped) {
201
+ body = `(${body})`;
202
+ }
173
203
  out += sign === 1 ? ` + ${body}` : ` - ${body}`;
174
204
  }
175
- });
205
+ }
176
206
  return out;
177
207
  }
178
208
 
@@ -207,7 +237,9 @@ function serializeLeadingNeg(node, prec) {
207
237
  return serializeProduct({ type: 'Product', factors: negatedFactors }, prec);
208
238
  }
209
239
  const body = serializeExpr(node, prec);
210
- return node.type === 'Sum' || node.type === 'Product' ? `-(${body})` : `-${body}`;
240
+ return node.type === 'Sum' || node.type === 'Product'
241
+ ? `-(${body})`
242
+ : `-${body}`;
211
243
  }
212
244
 
213
245
  /**
@@ -217,7 +249,7 @@ function serializeLeadingNeg(node, prec) {
217
249
  */
218
250
  function serializeProduct(product, prec) {
219
251
  let out = '';
220
- product.factors.forEach((f, i) => {
252
+ for (const [i, f] of product.factors.entries()) {
221
253
  let body = serializeExpr(f.node, prec);
222
254
  // A Sum factor needs parens: `a * (b + c)`. Flat canonical form means
223
255
  // this is the only place parens are required.
@@ -230,10 +262,8 @@ function serializeProduct(product, prec) {
230
262
  } else {
231
263
  out += f.exponent === 1 ? ` * ${body}` : ` / ${body}`;
232
264
  }
233
- });
265
+ }
234
266
  return out;
235
267
  }
236
268
 
237
- // Stryker disable next-line all: instrumenting this line breaks Node's
238
- // cjs-module-lexer named-export detection for .mjs `import { x } from` consumers.
239
- module.exports = { serialize };
269
+ export { serialize };
@@ -1,6 +1,4 @@
1
- 'use strict';
2
-
3
- const { num, dim } = require('../node.js');
1
+ import { num, dim } from '../node.js';
4
2
 
5
3
  /** @typedef {import('../node.js').Node} Node */
6
4
 
@@ -13,9 +11,13 @@ function simplifyAbs(args) {
13
11
  return { type: 'Call', name: 'abs', args };
14
12
  }
15
13
  const a = args[0];
16
- if (a.type === 'Num') {return num(Math.abs(a.value));}
17
- if (a.type === 'Dim' && a.unit !== '%') {return dim(Math.abs(a.value), a.unit);}
14
+ if (a.type === 'Num') {
15
+ return num(Math.abs(a.value));
16
+ }
17
+ if (a.type === 'Dim' && a.unit !== '%') {
18
+ return dim(Math.abs(a.value), a.unit);
19
+ }
18
20
  return { type: 'Call', name: 'abs', args: [a] };
19
21
  }
20
22
 
21
- module.exports = { simplifyAbs };
23
+ export { simplifyAbs };