postcss-calc 11.0.0-rc.2 → 11.0.0

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
@@ -56,7 +56,8 @@ Checkout [tests] for more examples.
56
56
 
57
57
  #### `precision` (default: `5`)
58
58
 
59
- Allow you to define the precision for decimal numbers.
59
+ Allows you to define the precision for decimal numbers. Set it to `false` to
60
+ disable rounding.
60
61
 
61
62
  ```js
62
63
  var out = postcss()
@@ -64,17 +65,6 @@ var out = postcss()
64
65
  .process(css).css;
65
66
  ```
66
67
 
67
- #### `preserve` (default: `false`)
68
-
69
- Allow you to preserve calc() usage in output so browsers will handle decimal
70
- precision themselves.
71
-
72
- ```js
73
- var out = postcss()
74
- .use(calc({ preserve: true }))
75
- .process(css).css;
76
- ```
77
-
78
68
  #### `warnWhenCannotResolve` (default: `false`)
79
69
 
80
70
  Adds warnings when calc() are not reduced to a single value.
@@ -87,7 +77,7 @@ var out = postcss()
87
77
 
88
78
  #### `mediaQueries` (default: `false`)
89
79
 
90
- Allows calc() usage as part of media query declarations.
80
+ Allows calc() usage in media query parameters.
91
81
 
92
82
  ```js
93
83
  var out = postcss()
@@ -95,6 +85,26 @@ var out = postcss()
95
85
  .process(css).css;
96
86
  ```
97
87
 
88
+ Example:
89
+
90
+ ```css
91
+ @media (min-width: calc(100px + 100px)) {
92
+ div {
93
+ width: 100px;
94
+ }
95
+ }
96
+ ```
97
+
98
+ With `mediaQueries: true`, this becomes:
99
+
100
+ ```css
101
+ @media (min-width: 200px) {
102
+ div {
103
+ width: 100px;
104
+ }
105
+ }
106
+ ```
107
+
98
108
  #### `selectors` (default: `false`)
99
109
 
100
110
  Allows calc() usage as part of selectors.
@@ -108,11 +118,13 @@ var out = postcss()
108
118
  Example:
109
119
 
110
120
  ```css
111
- div[data-size='calc(3*3)'] {
121
+ div:nth-child(calc(1 + 2)) {
112
122
  width: 100px;
113
123
  }
114
124
  ```
115
125
 
126
+ With `selectors: true`, this becomes `div:nth-child(3)`.
127
+
116
128
  #### `onParseError`
117
129
 
118
130
  Callback invoked when a `calc()` body fails to parse or simplify. Matches
@@ -186,6 +198,14 @@ npm install
186
198
  npm test
187
199
  ```
188
200
 
201
+ The normal test run uses a deterministic structural sample of the harvested
202
+ real-world corpus. Run the complete differential corpus before releases or
203
+ when changing parsing/simplification behavior:
204
+
205
+ ```bash
206
+ pnpm test:corpus:full
207
+ ```
208
+
189
209
  ## [Changelog](CHANGELOG.md)
190
210
 
191
211
  ## [License](LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postcss-calc",
3
- "version": "11.0.0-rc.2",
3
+ "version": "11.0.0",
4
4
  "type": "module",
5
5
  "description": "PostCSS plugin to reduce calc()",
6
6
  "keywords": [
@@ -29,21 +29,27 @@
29
29
  "author": "Andy Jansson",
30
30
  "license": "MIT",
31
31
  "engines": {
32
- "node": "^22.18 || ^24.11 || >=26.0"
32
+ "node": "^22.22.3 || ^24.15 || >=26.0"
33
+ },
34
+ "devEngines": {
35
+ "packageManager": {
36
+ "name": "pnpm",
37
+ "version": "11.22.0"
38
+ }
33
39
  },
34
40
  "devDependencies": {
35
41
  "@csstools/css-calc": "^3.3.0",
36
42
  "@rmenke/css-tokenizer-tests": "^1.2.0",
37
- "@types/node": "^26.1.2",
43
+ "@types/node": "^26.2.0",
38
44
  "fast-check": "^4.9.0",
39
- "oxfmt": "^0.61.0",
40
- "oxlint": "^1.76.0",
41
- "postcss": "^8.5.25",
45
+ "oxfmt": "^0.64.0",
46
+ "oxlint": "^1.79.0",
47
+ "postcss": "^8.5.26",
42
48
  "typescript": "~7.0.2"
43
49
  },
44
50
  "dependencies": {
45
- "@csstools/css-tokenizer": "^4.0.0",
46
- "postcss-value-parser": "^4.2.0"
51
+ "@csstools/css-parser-algorithms": "^4.0.0",
52
+ "@csstools/css-tokenizer": "^4.0.0"
47
53
  },
48
54
  "peerDependencies": {
49
55
  "postcss": "^8.5.25"
@@ -51,6 +57,8 @@
51
57
  "scripts": {
52
58
  "lint": "oxlint . && tsc && oxfmt --check",
53
59
  "fmt": "oxfmt",
54
- "test": "node --test"
60
+ "test": "node --test --test-reporter=dot 'test/**/*.test.mjs' test/index.cjs test/convertUnit.cjs",
61
+ "test:mutation:corpus": "node test/mutation/corpus-selection.mjs",
62
+ "test:corpus:full": "POSTCSS_CALC_FULL_CORPUS=1 node --test test/conformance/corpus.test.mjs"
55
63
  }
56
64
  }
package/src/index.js CHANGED
@@ -1,50 +1,111 @@
1
1
  // PostCSS adapter. Walks declaration values (and optionally @rule params
2
2
  // and selectors), feeds calc() bodies through tokenize → parse → simplify
3
3
  // → serialize, and writes the result back.
4
- import valueParser from 'postcss-value-parser';
4
+ import { tokenize as cssTokenize } from '@csstools/css-tokenizer';
5
+ import {
6
+ isFunctionNode,
7
+ isSimpleBlockNode,
8
+ parseListOfComponentValues,
9
+ } from '@csstools/css-parser-algorithms';
5
10
  import { tokenize } from './lib/tokenizer.js';
6
11
  import { parse } from './lib/parser.js';
7
12
  import { simplify } from './lib/simplify.js';
13
+ import { isSupportedMathFunction } from './lib/simplify/call.js';
8
14
  import { serialize } from './lib/serialize.js';
9
15
 
10
- const MATCH_CALC = /^(?:-(?:moz|webkit)-)?calc$/i;
16
+ // The outer walk is deliberately forgiving: it only needs to locate calc()/
17
+ // math-function boundaries in otherwise arbitrary (and possibly non-standard)
18
+ // CSS values, so parse errors from the outer tokenizer/parser are swallowed.
19
+ // Genuine syntax problems inside a matched call are
20
+ // caught below via our own tokenize/parse/simplify pipeline.
21
+ const NOOP_PARSE_ERROR = { onParseError: () => {} };
11
22
 
12
- // Bare math-function calls (no calc() wrapper) — fed to the same pipeline.
13
- // Mirrors the dispatch in lib/simplify/call.js.
14
- const MATH_FUNCTIONS = new Set([
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',
35
- ]);
23
+ const MATCH_CALC = /^(?:-(?:moz|webkit)-)?calc$/i;
36
24
 
37
25
  /**
38
- * @typedef {object} PluginOptions
26
+ * @typedef {object} PostCssCalcOptions
39
27
  * @property {number | false} [precision]
40
- * @property {boolean} [preserve]
41
28
  * @property {boolean} [warnWhenCannotResolve]
42
29
  * @property {boolean} [mediaQueries]
43
30
  * @property {boolean} [selectors]
44
31
  * @property {(error: Error, input: string) => void} [onParseError] Invoked when parse/simplify throws. Replaces the default `result.warn`.
45
32
  */
46
33
 
47
- /** @typedef {Required<Omit<PluginOptions, 'onParseError'>> & Pick<PluginOptions, 'onParseError'>} ResolvedOptions */
34
+ /** @typedef {Required<Omit<PostCssCalcOptions, 'onParseError'>> & Pick<PostCssCalcOptions, 'onParseError'>} ResolvedOptions */
35
+
36
+ /**
37
+ * Fields threaded unchanged through the recursive `transformList` walk.
38
+ * `value` is the original full property text, used only for the
39
+ * warnWhenCannotResolve message.
40
+ *
41
+ * @typedef {object} TransformContext
42
+ * @property {ResolvedOptions} options
43
+ * @property {import('postcss').Result} result
44
+ * @property {import('postcss').ChildNode} item
45
+ * @property {string} value
46
+ */
47
+
48
+ /**
49
+ * Walks a list of component values in place, replacing matched calc()/math
50
+ * function nodes with their simplified form. Unlike the library's generic
51
+ * `walk` helper, this recurses manually so a matched node's own (stale,
52
+ * pre-simplification) children are never independently re-visited.
53
+ *
54
+ * @param {import('@csstools/css-parser-algorithms').ComponentValue[]} list
55
+ * @param {TransformContext} ctx
56
+ * @return {void}
57
+ */
58
+ function transformList(list, ctx) {
59
+ for (let i = 0; i < list.length; i++) {
60
+ const node = list[i];
61
+ if (!isFunctionNode(node)) {
62
+ if (isSimpleBlockNode(node)) {
63
+ transformList(node.value, ctx);
64
+ }
65
+ continue;
66
+ }
67
+
68
+ const name = node.getName();
69
+ const isCalc = MATCH_CALC.test(name);
70
+ const isMath = !isCalc && isSupportedMathFunction(name);
71
+ if (!isCalc && !isMath) {
72
+ transformList(node.value, ctx);
73
+ continue;
74
+ }
75
+
76
+ // calc(): feed the body. Bare math: feed the whole call.
77
+ const inner = node.value.map((child) => child.toString()).join('');
78
+ const contents = isCalc ? inner : `${name}(${inner})`;
79
+ try {
80
+ const simplified = simplify(parse(tokenize(contents)));
81
+ const str = serialize(simplified, {
82
+ precision: ctx.options.precision,
83
+ calcName: isCalc ? name : 'calc', // preserve vendor prefix on calc()
84
+ });
85
+
86
+ if (ctx.options.warnWhenCannotResolve && str.startsWith(`${name}(`)) {
87
+ ctx.result.warn('Could not reduce expression: ' + ctx.value, {
88
+ plugin: 'postcss-calc',
89
+ node: ctx.item,
90
+ });
91
+ }
92
+
93
+ const replacement = parseListOfComponentValues(
94
+ cssTokenize({ css: str }),
95
+ NOOP_PARSE_ERROR
96
+ );
97
+ list.splice(i, 1, ...replacement);
98
+ i += replacement.length - 1;
99
+ } catch (error) {
100
+ const err = error instanceof Error ? error : new Error('Error');
101
+ if (ctx.options.onParseError) {
102
+ ctx.options.onParseError(err, contents);
103
+ } else {
104
+ ctx.result.warn(err.message, { node: ctx.item });
105
+ }
106
+ }
107
+ }
108
+ }
48
109
 
49
110
  /**
50
111
  * @param {string} value
@@ -54,60 +115,42 @@ const MATH_FUNCTIONS = new Set([
54
115
  * @return {string}
55
116
  */
56
117
  function transformValue(value, options, result, item) {
57
- return valueParser(value)
58
- .walk((node) => {
59
- if (node.type !== 'function') {
60
- return;
61
- }
62
- const isCalc = MATCH_CALC.test(node.value);
63
- const isMath = !isCalc && MATH_FUNCTIONS.has(node.value.toLowerCase());
64
- if (!isCalc && !isMath) {
65
- return;
66
- }
118
+ const componentValues = parseListOfComponentValues(
119
+ cssTokenize({ css: value }),
120
+ NOOP_PARSE_ERROR
121
+ );
67
122
 
68
- // calc(): feed the body. Bare math: feed the whole call.
69
- const inner = valueParser.stringify(node.nodes);
70
- const contents = isCalc ? inner : `${node.value}(${inner})`;
71
- try {
72
- const simplified = simplify(parse(tokenize(contents)));
73
- const str = serialize(simplified, {
74
- precision: options.precision,
75
- calcName: isCalc ? node.value : 'calc', // preserve vendor prefix on calc()
76
- });
123
+ transformList(componentValues, { options, result, item, value });
77
124
 
78
- if (options.warnWhenCannotResolve && str.startsWith(`${node.value}(`)) {
79
- result.warn('Could not reduce expression: ' + value, {
80
- plugin: 'postcss-calc',
81
- node: item,
82
- });
83
- }
125
+ return componentValues.map((node) => node.toString()).join('');
126
+ }
84
127
 
85
- // Re-tag as `word` so value-parser emits `str` verbatim instead of
86
- // re-wrapping it as `name(...)`. Cast widens the `'function'` literal.
87
- /** @type {{type: string}} */ (node).type = 'word';
88
- node.value = str;
89
- } catch (error) {
90
- const err = error instanceof Error ? error : new Error('Error');
91
- if (options.onParseError) {
92
- options.onParseError(err, contents);
93
- } else {
94
- result.warn(err.message, { node: item });
95
- }
96
- }
97
- return false;
98
- })
99
- .toString();
128
+ /**
129
+ * Runs `transformValue` over one text property of a decl/atrule/rule node
130
+ * and updates it in place.
131
+ * `setProp` closes over the property name and the concrete node type at
132
+ * each call site, since `Declaration`/`AtRule`/`Rule` don't share a typed
133
+ * "text property" name to index generically.
134
+ *
135
+ * @param {import('postcss').ChildNode} node
136
+ * @param {string} current
137
+ * @param {(target: import('postcss').ChildNode, value: string) => void} setProp
138
+ * @param {ResolvedOptions} options
139
+ * @param {import('postcss').Result} result
140
+ * @return {void}
141
+ */
142
+ function applyTransform(node, current, setProp, options, result) {
143
+ setProp(node, transformValue(current, options, result, node));
100
144
  }
101
145
 
102
146
  /**
103
- * @param {PluginOptions} [opts]
147
+ * @param {PostCssCalcOptions} [opts]
104
148
  * @return {import('postcss').Plugin}
105
149
  */
106
150
  function pluginCreator(opts) {
107
151
  /** @type {ResolvedOptions} */
108
152
  const options = {
109
153
  precision: 5,
110
- preserve: false,
111
154
  warnWhenCannotResolve: false,
112
155
  mediaQueries: false,
113
156
  selectors: false,
@@ -123,45 +166,49 @@ function pluginCreator(opts) {
123
166
  OnceExit(css, { result }) {
124
167
  css.walk((node) => {
125
168
  if (node.type === 'decl') {
126
- const next = transformValue(node.value, options, result, node);
127
- if (options.preserve && node.value !== next && node.parent) {
128
- const clone = node.clone();
129
- clone.value = next;
130
- node.parent.insertBefore(node, clone);
131
- } else {
132
- node.value = next;
133
- }
169
+ applyTransform(
170
+ node,
171
+ node.value,
172
+ (n, v) => {
173
+ /** @type {import('postcss').Declaration} */ (n).value = v;
174
+ },
175
+ options,
176
+ result
177
+ );
134
178
  }
135
179
  if (node.type === 'atrule' && options.mediaQueries) {
136
- const next = transformValue(node.params, options, result, node);
137
- if (options.preserve && node.params !== next && node.parent) {
138
- const clone = node.clone();
139
- clone.params = next;
140
- node.parent.insertBefore(node, clone);
141
- } else {
142
- node.params = next;
143
- }
180
+ applyTransform(
181
+ node,
182
+ node.params,
183
+ (n, v) => {
184
+ /** @type {import('postcss').AtRule} */ (n).params = v;
185
+ },
186
+ options,
187
+ result
188
+ );
144
189
  }
145
190
  if (node.type === 'rule' && options.selectors) {
146
191
  // Reduces `:nth-child(calc(...))` via the function walk. calc() in a
147
192
  // quoted attribute value is a literal match, so it's left untouched.
148
- const next = transformValue(node.selector, options, result, node);
149
- if (options.preserve && node.selector !== next && node.parent) {
150
- const clone = node.clone();
151
- clone.selector = next;
152
- node.parent.insertBefore(node, clone);
153
- } else {
154
- node.selector = next;
155
- }
193
+ applyTransform(
194
+ node,
195
+ node.selector,
196
+ (n, v) => {
197
+ /** @type {import('postcss').Rule} */ (n).selector = v;
198
+ },
199
+ options,
200
+ result
201
+ );
156
202
  }
157
203
  });
158
204
  },
159
205
  };
160
206
  }
161
207
 
208
+ /** @type {true} */
162
209
  pluginCreator.postcss = true;
163
210
 
164
- export default /** @type import('postcss').PluginCreator<PluginOptions>*/ (
211
+ export default /** @type import('postcss').PluginCreator<PostCssCalcOptions>*/ (
165
212
  pluginCreator
166
213
  );
167
214
  export { pluginCreator as 'module.exports' };
package/src/lib/node.js CHANGED
@@ -5,7 +5,9 @@
5
5
  // is `Num(-5)`, never `Sum([{sign:-1, Num(5)}])`. One form per value.
6
6
  // - In a SumTerm with Num/Dim node, sign is always +1; the sign slot is
7
7
  // reserved for opaque nodes (Ident, Call, Product, multi-term Sum).
8
- // - 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.
9
11
  // - No Product directly contains another Product (flattened).
10
12
  // - A Sum/Product with one positive element collapses to that element.
11
13
  // - A Sum/Product with no elements collapses to Num(0) / Num(1).
@@ -18,7 +20,7 @@
18
20
  * @typedef {{type: 'Ident', name: string}} Ident
19
21
  * @typedef {{type: 'Call', name: string, args: Node[]}} Call
20
22
  * @typedef {{sign: 1 | -1, node: Node}} SumTerm Sign is always +1 when node is Num or Dim.
21
- * @typedef {{type: 'Sum', terms: SumTerm[]}} Sum
23
+ * @typedef {{type: 'Sum', terms: SumTerm[], grouped?: boolean}} Sum
22
24
  * @typedef {{exponent: 1 | -1, node: Node}} ProductFactor exponent +1 = numerator, -1 = denominator.
23
25
  * @typedef {{type: 'Product', factors: ProductFactor[]}} Product
24
26
  * @typedef {Num | Dim | Ident | Call | Sum | Product} Node
@@ -85,7 +87,7 @@ function mkSum(rawTerms) {
85
87
  function pushSumTerm(out, term) {
86
88
  let { sign, node } = term;
87
89
 
88
- if (node.type === 'Sum') {
90
+ if (node.type === 'Sum' && !node.grouped) {
89
91
  for (const inner of node.terms) {
90
92
  pushSumTerm(out, {
91
93
  sign: /** @type {1 | -1} */ (sign * inner.sign),
@@ -170,12 +172,15 @@ function negate(node) {
170
172
  return dim(-node.value, node.unit);
171
173
  }
172
174
  if (node.type === 'Sum') {
173
- return mkSum(
175
+ const result = mkSum(
174
176
  node.terms.map((t) => ({
175
177
  sign: /** @type {1 | -1} */ (-t.sign),
176
178
  node: t.node,
177
179
  }))
178
180
  );
181
+ return node.grouped && result.type === 'Sum'
182
+ ? { ...result, grouped: true }
183
+ : result;
179
184
  }
180
185
  // Opaque (Ident, Call, Product): wrap as a single negative-sign term —
181
186
  // the only case where sign=-1 remains on a SumTerm.
package/src/lib/parser.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Pratt parser. +/- emit Sum nodes; */÷ emit Product nodes. node.js
2
- // constructors flatten and normalize on construction, so the parser
3
- // never produces a Binary node.
4
- import { mkSum, mkProduct, negate } from './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, ident, call } from './node.js';
5
5
 
6
6
  /**
7
7
  * @typedef {import('./tokenizer.js').Token} Token
@@ -30,7 +30,7 @@ function foldCalcKeyword(name) {
30
30
  // form arrives as a single ident because CSS Syntax tokenizes leading
31
31
  // `-` + ident-start as one ident-token.
32
32
  if (name === 'NaN' || name === '-NaN') {
33
- return { type: 'Num', value: NaN };
33
+ return { type: 'Num', value: Number.NaN };
34
34
  }
35
35
  switch (name.toLowerCase()) {
36
36
  case 'pi':
@@ -174,7 +174,7 @@ function parseOpaqueCall(p, name) {
174
174
  const flush = () => {
175
175
  const trimmed = buf.trim();
176
176
  if (trimmed) {
177
- args.push({ type: 'Ident', name: trimmed });
177
+ args.push(ident(trimmed));
178
178
  }
179
179
  buf = '';
180
180
  };
@@ -191,7 +191,7 @@ function parseOpaqueCall(p, name) {
191
191
  if (depth === 0) {
192
192
  p.next();
193
193
  flush();
194
- return { type: 'Call', name, args };
194
+ return call(name, args);
195
195
  }
196
196
  } else if (tk.value === ',' && depth === 1) {
197
197
  p.next();
@@ -228,12 +228,12 @@ function requireSurroundingWs(p, token) {
228
228
 
229
229
  /** @type {Record<string, PrefixParselet>} */
230
230
  const PREFIX = {
231
- number: (_p, t) => ({ type: 'Num', value: parseFloat(t.value) }),
231
+ number: (_p, t) => ({ type: 'Num', value: Number.parseFloat(t.value) }),
232
232
 
233
233
  // Unit case normalization per §10.12: `1PX` serializes as `1px`.
234
234
  dimension: (_p, t) => ({
235
235
  type: 'Dim',
236
- value: parseFloat(t.value),
236
+ value: Number.parseFloat(t.value),
237
237
  unit: t.unit === '%' ? '%' : /** @type {string} */ (t.unit).toLowerCase(),
238
238
  }),
239
239
 
@@ -254,19 +254,19 @@ const PREFIX = {
254
254
  }
255
255
  }
256
256
  p.expect('punct', ')');
257
- return { type: 'Call', name: t.value, args };
257
+ return call(t.value, args);
258
258
  }
259
259
  const kw = foldCalcKeyword(t.value);
260
260
  if (kw) {
261
261
  return kw;
262
262
  }
263
- return { type: 'Ident', name: t.value };
263
+ return ident(t.value);
264
264
  },
265
265
 
266
266
  '(': (p) => {
267
267
  const e = p.parseExpr(0);
268
268
  p.expect('punct', ')');
269
- return e;
269
+ return e.type === 'Sum' ? { ...e, grouped: true } : e;
270
270
  },
271
271
 
272
272
  '-': (p) => negate(p.parseExpr(UNARY_BP)),
@@ -45,7 +45,7 @@ function round(v, prec) {
45
45
  * @return {boolean}
46
46
  */
47
47
  function isDegenerate(v) {
48
- return !isFinite(v) || isNaN(v);
48
+ return !Number.isFinite(v) || Number.isNaN(v);
49
49
  }
50
50
 
51
51
  /**
@@ -53,7 +53,7 @@ function isDegenerate(v) {
53
53
  * @return {string}
54
54
  */
55
55
  function degenerateKeyword(v) {
56
- if (isNaN(v)) {
56
+ if (Number.isNaN(v)) {
57
57
  return 'NaN';
58
58
  }
59
59
  return v > 0 ? 'infinity' : '-infinity';
@@ -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' ||
@@ -141,13 +160,13 @@ function serializeExpr(node, prec) {
141
160
  */
142
161
  function displaySign(term) {
143
162
  const { sign, node } = term;
144
- if (node.type === 'Num' && isFinite(node.value) && node.value < 0) {
163
+ if (node.type === 'Num' && Number.isFinite(node.value) && node.value < 0) {
145
164
  return {
146
165
  sign: /** @type {1 | -1} */ (-sign),
147
166
  magnitude: { type: 'Num', value: -node.value },
148
167
  };
149
168
  }
150
- if (node.type === 'Dim' && isFinite(node.value) && node.value < 0) {
169
+ if (node.type === 'Dim' && Number.isFinite(node.value) && node.value < 0) {
151
170
  return {
152
171
  sign: /** @type {1 | -1} */ (-sign),
153
172
  magnitude: { type: 'Dim', value: -node.value, unit: node.unit },
@@ -166,13 +185,21 @@ function serializeSum(sum, prec) {
166
185
  for (const [i, t] of sum.terms.entries()) {
167
186
  const { sign, magnitude } = displaySign(t);
168
187
  if (i === 0) {
188
+ if (magnitude.type === 'Sum' && magnitude.grouped) {
189
+ const body = `(${serializeExpr(magnitude, prec)})`;
190
+ out = sign === 1 ? body : `-${body}`;
191
+ continue;
192
+ }
169
193
  out =
170
194
  sign === 1
171
195
  ? serializeExpr(magnitude, prec)
172
196
  : serializeLeadingNeg(magnitude, prec);
173
197
  } else {
174
198
  // `-` binds looser than `*`/`/` so the right side never needs parens.
175
- const body = serializeExpr(magnitude, prec);
199
+ let body = serializeExpr(magnitude, prec);
200
+ if (magnitude.type === 'Sum' && magnitude.grouped) {
201
+ body = `(${body})`;
202
+ }
176
203
  out += sign === 1 ? ` + ${body}` : ` - ${body}`;
177
204
  }
178
205
  }
@@ -192,7 +219,7 @@ function serializeLeadingNeg(node, prec) {
192
219
  node.factors.length > 0 &&
193
220
  node.factors[0].exponent === 1 &&
194
221
  node.factors[0].node.type === 'Num' &&
195
- isFinite(node.factors[0].node.value) &&
222
+ Number.isFinite(node.factors[0].node.value) &&
196
223
  node.factors[0].node.value !== 0
197
224
  ) {
198
225
  const head = node.factors[0].node;
@@ -1,4 +1,4 @@
1
- import { num, dim } from '../node.js';
1
+ import { num, dim, call } from '../node.js';
2
2
 
3
3
  /** @typedef {import('../node.js').Node} Node */
4
4
 
@@ -8,7 +8,7 @@ import { num, dim } from '../node.js';
8
8
  */
9
9
  function simplifyAbs(args) {
10
10
  if (args.length !== 1) {
11
- return { type: 'Call', name: 'abs', args };
11
+ return call('abs', args);
12
12
  }
13
13
  const a = args[0];
14
14
  if (a.type === 'Num') {
@@ -17,7 +17,7 @@ function simplifyAbs(args) {
17
17
  if (a.type === 'Dim' && a.unit !== '%') {
18
18
  return dim(Math.abs(a.value), a.unit);
19
19
  }
20
- return { type: 'Call', name: 'abs', args: [a] };
20
+ return call('abs', [a]);
21
21
  }
22
22
 
23
23
  export { simplifyAbs };
@@ -1,7 +1,7 @@
1
1
  /* §10.4 — atan2. foldConstArgs already rejects percentages (property-
2
2
  context-resolved) and enforces shared base + static convertibility. */
3
3
 
4
- import { num, dim } from '../node.js';
4
+ import { num, dim, call } from '../node.js';
5
5
  import { foldConstArgs } from './fold.js';
6
6
 
7
7
  /** @typedef {import('../node.js').Node} Node */
@@ -12,16 +12,16 @@ import { foldConstArgs } from './fold.js';
12
12
  */
13
13
  function simplifyAtan2(args) {
14
14
  if (args.length !== 2) {
15
- return { type: 'Call', name: 'atan2', args };
15
+ return call('atan2', args);
16
16
  }
17
17
  const fold = foldConstArgs(args);
18
18
  if (fold === null) {
19
- return { type: 'Call', name: 'atan2', args };
19
+ return call('atan2', args);
20
20
  }
21
21
  const [y, x] = /** @type {[number, number]} */ (fold.values);
22
22
  const radians = Math.atan2(y, x);
23
- if (isNaN(radians)) {
24
- return num(NaN);
23
+ if (Number.isNaN(radians)) {
24
+ return num(Number.NaN);
25
25
  }
26
26
  return dim((radians * 180) / Math.PI, 'deg');
27
27
  }
@@ -16,9 +16,51 @@ import { simplifyExp } from './exp.js';
16
16
  import { simplifyLog } from './log.js';
17
17
  import { simplifyHypot } from './hypot.js';
18
18
 
19
+ import { call } from '../node.js';
20
+
19
21
  /** @typedef {import('../node.js').Node} Node */
20
22
  /** @typedef {import('../simplify.js').SimplifyFn} SimplifyFn */
21
23
 
24
+ /** @typedef {(name: string, args: Node[]) => Node} MathSimplifier */
25
+
26
+ // Bare CSS math functions with implemented simplification semantics, keyed
27
+ // by lowercase name. calc() and its vendor-prefixed forms are handled
28
+ // separately as wrappers in simplifyCall. This map is the single source of
29
+ // truth for both dispatch and `isSupportedMathFunction`.
30
+ /** @type {Map<string, MathSimplifier>} */
31
+ const MATH_SIMPLIFIERS = new Map([
32
+ ['min', simplifyMinMax],
33
+ ['max', simplifyMinMax],
34
+ ['clamp', (_name, args) => simplifyClamp(args)],
35
+ ['abs', (_name, args) => simplifyAbs(args)],
36
+ ['sign', (_name, args) => simplifySign(args)],
37
+ ['mod', (_name, args) => simplifyModRem('mod', args)],
38
+ ['rem', (_name, args) => simplifyModRem('rem', args)],
39
+ ['round', (_name, args) => simplifyRound(args)],
40
+ ['sin', (_name, args) => simplifyTrig('sin', args)],
41
+ ['cos', (_name, args) => simplifyTrig('cos', args)],
42
+ ['tan', (_name, args) => simplifyTrig('tan', args)],
43
+ ['asin', (_name, args) => simplifyInverseTrig('asin', args)],
44
+ ['acos', (_name, args) => simplifyInverseTrig('acos', args)],
45
+ ['atan', (_name, args) => simplifyInverseTrig('atan', args)],
46
+ ['atan2', (_name, args) => simplifyAtan2(args)],
47
+ ['pow', (_name, args) => simplifyPow(args)],
48
+ ['sqrt', (_name, args) => simplifySqrt(args)],
49
+ ['hypot', (_name, args) => simplifyHypot(args)],
50
+ ['log', (_name, args) => simplifyLog(args)],
51
+ ['exp', (_name, args) => simplifyExp(args)],
52
+ ]);
53
+
54
+ /**
55
+ * Whether a bare CSS math function has an implemented simplifier.
56
+ *
57
+ * @param {string} name
58
+ * @return {boolean}
59
+ */
60
+ function isSupportedMathFunction(name) {
61
+ return MATH_SIMPLIFIERS.has(name.toLowerCase());
62
+ }
63
+
22
64
  /**
23
65
  * @param {Extract<Node, { type: 'Call' }>} node
24
66
  * @param {SimplifyFn} simplify
@@ -36,50 +78,17 @@ function simplifyCall(node, simplify) {
36
78
 
37
79
  const args = node.args.map((a) => simplify(a));
38
80
 
39
- if (name === 'min' || name === 'max') {
40
- return simplifyMinMax(node.name, args);
41
- }
42
- if (name === 'clamp') {
43
- return simplifyClamp(args);
44
- }
45
- if (name === 'abs') {
46
- return simplifyAbs(args);
47
- }
48
- if (name === 'sign') {
49
- return simplifySign(args);
50
- }
51
- if (name === 'mod' || name === 'rem') {
52
- return simplifyModRem(name, args);
53
- }
54
- if (name === 'round') {
55
- return simplifyRound(args);
56
- }
57
- if (name === 'sin' || name === 'cos' || name === 'tan') {
58
- return simplifyTrig(name, args);
59
- }
60
- if (name === 'asin' || name === 'acos' || name === 'atan') {
61
- return simplifyInverseTrig(name, args);
62
- }
63
- if (name === 'atan2') {
64
- return simplifyAtan2(args);
65
- }
66
- if (name === 'pow') {
67
- return simplifyPow(args);
68
- }
69
- if (name === 'sqrt') {
70
- return simplifySqrt(args);
71
- }
72
- if (name === 'hypot') {
73
- return simplifyHypot(args);
74
- }
75
- if (name === 'log') {
76
- return simplifyLog(args);
77
- }
78
- if (name === 'exp') {
79
- return simplifyExp(args);
81
+ const simplifier = MATH_SIMPLIFIERS.get(name);
82
+ if (simplifier) {
83
+ // min/max preserve the call's original casing in their opaque-args
84
+ // fallback; the rest normalize to lowercase internally.
85
+ return simplifier(
86
+ name === 'min' || name === 'max' ? node.name : name,
87
+ args
88
+ );
80
89
  }
81
90
 
82
- return { type: 'Call', name: node.name, args };
91
+ return call(node.name, args);
83
92
  }
84
93
 
85
- export { simplifyCall };
94
+ export { isSupportedMathFunction, simplifyCall };
@@ -1,4 +1,4 @@
1
- import { num, dim } from '../node.js';
1
+ import { num, dim, call } from '../node.js';
2
2
  import { foldConstArgs } from './fold.js';
3
3
 
4
4
  /** @typedef {import('../node.js').Node} Node */
@@ -18,7 +18,7 @@ function simplifyClamp(args) {
18
18
  return fold.unit === '' ? num(clamped) : dim(clamped, fold.unit);
19
19
  }
20
20
  }
21
- return { type: 'Call', name: 'clamp', args };
21
+ return call('clamp', args);
22
22
  }
23
23
 
24
24
  export { simplifyClamp };
@@ -1,4 +1,4 @@
1
- import { num } from '../node.js';
1
+ import { num, call } from '../node.js';
2
2
 
3
3
  /** @typedef {import('../node.js').Node} Node */
4
4
 
@@ -8,7 +8,7 @@ import { num } from '../node.js';
8
8
  */
9
9
  function simplifyExp(args) {
10
10
  if (args.length !== 1 || args[0].type !== 'Num') {
11
- return { type: 'Call', name: 'exp', args };
11
+ return call('exp', args);
12
12
  }
13
13
  return num(Math.exp(args[0].value));
14
14
  }
@@ -1,6 +1,6 @@
1
1
  // §10.5 — hypot. Empty args return null from foldConstArgs naturally.
2
2
 
3
- import { num, dim } from '../node.js';
3
+ import { num, dim, call } from '../node.js';
4
4
  import { foldConstArgs } from './fold.js';
5
5
 
6
6
  /** @typedef {import('../node.js').Node} Node */
@@ -12,7 +12,7 @@ import { foldConstArgs } from './fold.js';
12
12
  function simplifyHypot(args) {
13
13
  const fold = foldConstArgs(args);
14
14
  if (fold === null) {
15
- return { type: 'Call', name: 'hypot', args };
15
+ return call('hypot', args);
16
16
  }
17
17
  const sumSq = fold.values.reduce((acc, v) => acc + v * v, 0);
18
18
  const result = Math.sqrt(sumSq);
@@ -1,6 +1,6 @@
1
1
  // §10.4 — asin/acos/atan. Bare <number> in, <angle> in degrees out.
2
2
 
3
- import { num, dim } from '../node.js';
3
+ import { num, dim, call } from '../node.js';
4
4
 
5
5
  /** @typedef {import('../node.js').Node} Node */
6
6
 
@@ -17,15 +17,15 @@ const INVERSE_TRIG_OPS = /** @type {const} */ ({
17
17
  */
18
18
  function simplifyInverseTrig(name, args) {
19
19
  if (args.length !== 1) {
20
- return { type: 'Call', name, args };
20
+ return call(name, args);
21
21
  }
22
22
  const a = args[0];
23
23
  if (a.type !== 'Num') {
24
- return { type: 'Call', name, args };
24
+ return call(name, args);
25
25
  }
26
26
  const radians = INVERSE_TRIG_OPS[name](a.value);
27
- if (isNaN(radians)) {
28
- return num(NaN);
27
+ if (Number.isNaN(radians)) {
28
+ return num(Number.NaN);
29
29
  }
30
30
  return dim((radians * 180) / Math.PI, 'deg');
31
31
  }
@@ -1,4 +1,4 @@
1
- import { num } from '../node.js';
1
+ import { num, call } from '../node.js';
2
2
 
3
3
  /** @typedef {import('../node.js').Node} Node */
4
4
 
@@ -13,7 +13,7 @@ function simplifyLog(args) {
13
13
  if (args.length === 2 && args[0].type === 'Num' && args[1].type === 'Num') {
14
14
  return num(Math.log(args[0].value) / Math.log(args[1].value));
15
15
  }
16
- return { type: 'Call', name: 'log', args };
16
+ return call('log', args);
17
17
  }
18
18
 
19
19
  export { simplifyLog };
@@ -1,4 +1,4 @@
1
- import { num, dim } from '../node.js';
1
+ import { num, dim, call } from '../node.js';
2
2
  import { foldConstArgs } from './fold.js';
3
3
 
4
4
  /** @typedef {import('../node.js').Node} Node */
@@ -15,7 +15,7 @@ function simplifyMinMax(name, args) {
15
15
  const value = fn(...fold.values);
16
16
  return fold.unit === '' ? num(value) : dim(value, fold.unit);
17
17
  }
18
- return { type: 'Call', name, args };
18
+ return call(name, args);
19
19
  }
20
20
 
21
21
  export { simplifyMinMax };
@@ -1,4 +1,4 @@
1
- import { num, dim } from '../node.js';
1
+ import { num, dim, call } from '../node.js';
2
2
  import { foldConstArgs } from './fold.js';
3
3
 
4
4
  /** @typedef {import('../node.js').Node} Node */
@@ -10,18 +10,18 @@ import { foldConstArgs } from './fold.js';
10
10
  */
11
11
  function simplifyModRem(name, args) {
12
12
  if (args.length !== 2) {
13
- return { type: 'Call', name, args };
13
+ return call(name, args);
14
14
  }
15
15
  const fold = foldConstArgs(args);
16
16
  if (fold === null) {
17
- return { type: 'Call', name, args };
17
+ return call(name, args);
18
18
  }
19
19
  const [a, b] = /** @type {[number, number]} */ (fold.values);
20
20
  const result = applyModRem(name, a, b);
21
21
  // NaN results drop the unit (`mod(5px, 0px)` → `calc(NaN)`, not
22
22
  // `calc(NaN * 1px)`). §10.12 unit-preserving form is a known divergence.
23
- if (isNaN(result)) {
24
- return num(NaN);
23
+ if (Number.isNaN(result)) {
24
+ return num(Number.NaN);
25
25
  }
26
26
  return fold.unit === '' ? num(result) : dim(result, fold.unit);
27
27
  }
@@ -34,16 +34,16 @@ function simplifyModRem(name, args) {
34
34
  */
35
35
  function applyModRem(name, a, b) {
36
36
  if (b === 0) {
37
- return NaN;
37
+ return Number.NaN;
38
38
  }
39
- if (!isFinite(a)) {
40
- return NaN;
39
+ if (!Number.isFinite(a)) {
40
+ return Number.NaN;
41
41
  }
42
- if (!isFinite(b)) {
42
+ if (!Number.isFinite(b)) {
43
43
  // mod: result is NaN when A has opposite sign to B; otherwise A.
44
44
  // rem: result is A regardless of signs.
45
45
  if (name === 'mod' && a !== 0 && Math.sign(a) !== Math.sign(b)) {
46
- return NaN;
46
+ return Number.NaN;
47
47
  }
48
48
  return a;
49
49
  }
@@ -1,6 +1,6 @@
1
1
  // §10.5 — pow is <number>-only.
2
2
 
3
- import { num } from '../node.js';
3
+ import { num, call } from '../node.js';
4
4
 
5
5
  /** @typedef {import('../node.js').Node} Node */
6
6
 
@@ -10,7 +10,7 @@ import { num } from '../node.js';
10
10
  */
11
11
  function simplifyPow(args) {
12
12
  if (args.length !== 2 || args[0].type !== 'Num' || args[1].type !== 'Num') {
13
- return { type: 'Call', name: 'pow', args };
13
+ return call('pow', args);
14
14
  }
15
15
  return num(Math.pow(args[0].value, args[1].value));
16
16
  }
@@ -1,4 +1,4 @@
1
- import { num, dim } from '../node.js';
1
+ import { num, dim, ident, call } from '../node.js';
2
2
  import { foldConstArgs } from './fold.js';
3
3
 
4
4
  /** @typedef {import('../node.js').Node} Node */
@@ -18,21 +18,15 @@ function simplifyRound(args) {
18
18
  const n = first.name.toLowerCase();
19
19
  if (!ROUND_STRATEGIES.has(n)) {
20
20
  // Unrecognized strategy ident — opaque rather than guessing intent.
21
- return { type: 'Call', name: 'round', args };
21
+ return call('round', args);
22
22
  }
23
23
  strategy = /** @type {RoundStrategy} */ (n);
24
24
  rest = args.slice(1);
25
25
  }
26
26
 
27
27
  /** @type {() => Node} */
28
- const passthrough = () => ({
29
- type: 'Call',
30
- name: 'round',
31
- args:
32
- strategy === 'nearest'
33
- ? rest
34
- : [{ type: 'Ident', name: strategy }, ...rest],
35
- });
28
+ const passthrough = () =>
29
+ call('round', strategy === 'nearest' ? rest : [ident(strategy), ...rest]);
36
30
 
37
31
  // B omitted: defaults to 1 when A is <number>; else opaque.
38
32
  const argsForFold = argsForRoundFold(rest);
@@ -49,12 +43,12 @@ function simplifyRound(args) {
49
43
  // case folds to ±0 carrying A's sign. Infinite-A / finite-B falls through
50
44
  // to applyRound, where floor*b===ceil*b===±∞ collapses back to A
51
45
  // (§10.3.1 "result is the same infinity").
52
- if (isNaN(b)) {
53
- return num(NaN);
46
+ if (Number.isNaN(b)) {
47
+ return num(Number.NaN);
54
48
  }
55
- if (!isFinite(b)) {
56
- if (!isFinite(a)) {
57
- return num(NaN);
49
+ if (!Number.isFinite(b)) {
50
+ if (!Number.isFinite(a)) {
51
+ return num(Number.NaN);
58
52
  }
59
53
  let result;
60
54
  if (strategy === 'up' && a > 0) {
@@ -68,8 +62,8 @@ function simplifyRound(args) {
68
62
  }
69
63
 
70
64
  const result = applyRound(strategy, a, b);
71
- if (isNaN(result)) {
72
- return num(NaN);
65
+ if (Number.isNaN(result)) {
66
+ return num(Number.NaN);
73
67
  }
74
68
  return fold.unit === '' ? num(result) : dim(result, fold.unit);
75
69
  }
@@ -96,7 +90,7 @@ function argsForRoundFold(args) {
96
90
  */
97
91
  function applyRound(strategy, a, b) {
98
92
  if (b === 0) {
99
- return NaN;
93
+ return Number.NaN;
100
94
  }
101
95
  const q = a / b;
102
96
  const c1 = Math.floor(q) * b;
@@ -1,4 +1,4 @@
1
- import { num } from '../node.js';
1
+ import { num, call } from '../node.js';
2
2
 
3
3
  /** @typedef {import('../node.js').Node} Node */
4
4
 
@@ -8,7 +8,7 @@ import { num } from '../node.js';
8
8
  */
9
9
  function simplifySign(args) {
10
10
  if (args.length !== 1) {
11
- return { type: 'Call', name: 'sign', args };
11
+ return call('sign', args);
12
12
  }
13
13
  const a = args[0];
14
14
  if (a.type === 'Num') {
@@ -18,7 +18,7 @@ function simplifySign(args) {
18
18
  if (a.type === 'Dim' && a.unit !== '%') {
19
19
  return num(Math.sign(a.value));
20
20
  }
21
- return { type: 'Call', name: 'sign', args: [a] };
21
+ return call('sign', [a]);
22
22
  }
23
23
 
24
24
  export { simplifySign };
@@ -1,4 +1,4 @@
1
- import { num } from '../node.js';
1
+ import { num, call } from '../node.js';
2
2
 
3
3
  /** @typedef {import('../node.js').Node} Node */
4
4
  /**
@@ -7,7 +7,7 @@ import { num } from '../node.js';
7
7
  */
8
8
  function simplifySqrt(args) {
9
9
  if (args.length !== 1 || args[0].type !== 'Num') {
10
- return { type: 'Call', name: 'sqrt', args };
10
+ return call('sqrt', args);
11
11
  }
12
12
  return num(Math.sqrt(args[0].value));
13
13
  }
@@ -48,6 +48,17 @@ function simplifySum(sum, simplify) {
48
48
  */
49
49
  function processTerm(sign, n) {
50
50
  if (n.type === 'Sum') {
51
+ // Parentheses around a sum are normally algebraically disposable. Keep
52
+ // them only for an opaque sum used negatively: distributing that sign
53
+ // changes the CSS value when a var() contains a sum of its own.
54
+ if (
55
+ n.grouped &&
56
+ sign === -1 &&
57
+ n.terms.some((t) => t.node.type !== 'Num' && t.node.type !== 'Dim')
58
+ ) {
59
+ opaque.push({ sign, node: n });
60
+ return;
61
+ }
51
62
  for (const inner of n.terms) {
52
63
  processTerm(/** @type {1 | -1} */ (sign * inner.sign), inner.node);
53
64
  }
@@ -95,7 +106,13 @@ function simplifySum(sum, simplify) {
95
106
  }
96
107
  terms.push(...opaque);
97
108
 
98
- return mkSum(terms);
109
+ const result = mkSum(terms);
110
+ // Keep the source grouping available to an enclosing subtraction. Numeric
111
+ // groups have already folded and therefore do not need the marker.
112
+ if (sum.grouped && result.type === 'Sum' && opaque.length > 0) {
113
+ return { ...result, grouped: true };
114
+ }
115
+ return result;
99
116
  }
100
117
 
101
118
  export { simplifySum };
@@ -1,6 +1,6 @@
1
1
  // §10.4 — sin/cos/tan. <number> is radians; <angle> dim is converted.
2
2
 
3
- import { num } from '../node.js';
3
+ import { num, call } from '../node.js';
4
4
  import { baseOf, convert } from '../convertUnits.js';
5
5
 
6
6
  /** @typedef {import('../node.js').Node} Node */
@@ -18,7 +18,7 @@ const TRIG_OPS = /** @type {const} */ ({
18
18
  */
19
19
  function simplifyTrig(name, args) {
20
20
  if (args.length !== 1) {
21
- return { type: 'Call', name, args };
21
+ return call(name, args);
22
22
  }
23
23
  const a = args[0];
24
24
  /** @type {number | null} */ let radians = null;
@@ -36,7 +36,7 @@ function simplifyTrig(name, args) {
36
36
  }
37
37
  }
38
38
  if (radians === null) {
39
- return { type: 'Call', name, args };
39
+ return call(name, args);
40
40
  }
41
41
  return num(TRIG_OPS[name](radians));
42
42
  }
package/types/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
- export type PluginOptions = {
1
+ export type PostCssCalcOptions = {
2
2
  precision?: number | false;
3
- preserve?: boolean;
4
3
  warnWhenCannotResolve?: boolean;
5
4
  mediaQueries?: boolean;
6
5
  selectors?: boolean;
@@ -9,15 +8,21 @@ export type PluginOptions = {
9
8
  */
10
9
  onParseError?: (error: Error, input: string) => void;
11
10
  };
12
- export type ResolvedOptions = Required<Omit<PluginOptions, 'onParseError'>> & Pick<PluginOptions, 'onParseError'>;
11
+ export type ResolvedOptions = Required<Omit<PostCssCalcOptions, 'onParseError'>> & Pick<PostCssCalcOptions, 'onParseError'>;
12
+ export type TransformContext = {
13
+ options: ResolvedOptions;
14
+ result: import('postcss').Result;
15
+ item: import('postcss').ChildNode;
16
+ value: string;
17
+ };
13
18
  /**
14
- * @param {PluginOptions} [opts]
19
+ * @param {PostCssCalcOptions} [opts]
15
20
  * @return {import('postcss').Plugin}
16
21
  */
17
- declare function pluginCreator(opts?: PluginOptions): import('postcss').Plugin;
22
+ declare function pluginCreator(opts?: PostCssCalcOptions): import('postcss').Plugin;
18
23
  declare namespace pluginCreator {
19
- var postcss: boolean;
24
+ var postcss: true;
20
25
  }
21
- declare const _default: import('postcss').PluginCreator<PluginOptions>;
26
+ declare const _default: import('postcss').PluginCreator<PostCssCalcOptions>;
22
27
  export default _default;
23
28
  export { pluginCreator as 'module.exports' };
@@ -23,6 +23,7 @@ export type SumTerm = {
23
23
  export type Sum = {
24
24
  type: 'Sum';
25
25
  terms: SumTerm[];
26
+ grouped?: boolean;
26
27
  };
27
28
  export type ProductFactor = {
28
29
  exponent: 1 | -1;
@@ -39,7 +40,7 @@ export type Node = Num | Dim | Ident | Call | Sum | Product;
39
40
  * @typedef {{type: 'Ident', name: string}} Ident
40
41
  * @typedef {{type: 'Call', name: string, args: Node[]}} Call
41
42
  * @typedef {{sign: 1 | -1, node: Node}} SumTerm Sign is always +1 when node is Num or Dim.
42
- * @typedef {{type: 'Sum', terms: SumTerm[]}} Sum
43
+ * @typedef {{type: 'Sum', terms: SumTerm[], grouped?: boolean}} Sum
43
44
  * @typedef {{exponent: 1 | -1, node: Node}} ProductFactor exponent +1 = numerator, -1 = denominator.
44
45
  * @typedef {{type: 'Product', factors: ProductFactor[]}} Product
45
46
  * @typedef {Num | Dim | Ident | Call | Sum | Product} Node
@@ -1,7 +1,13 @@
1
1
  export type Node = import('../node.js').Node;
2
2
  export type SimplifyFn = import('../simplify.js').SimplifyFn;
3
- /** @typedef {import('../node.js').Node} Node */
4
- /** @typedef {import('../simplify.js').SimplifyFn} SimplifyFn */
3
+ export type MathSimplifier = (name: string, args: Node[]) => Node;
4
+ /**
5
+ * Whether a bare CSS math function has an implemented simplifier.
6
+ *
7
+ * @param {string} name
8
+ * @return {boolean}
9
+ */
10
+ declare function isSupportedMathFunction(name: string): boolean;
5
11
  /**
6
12
  * @param {Extract<Node, { type: 'Call' }>} node
7
13
  * @param {SimplifyFn} simplify
@@ -10,4 +16,4 @@ export type SimplifyFn = import('../simplify.js').SimplifyFn;
10
16
  declare function simplifyCall(node: Extract<Node, {
11
17
  type: 'Call';
12
18
  }>, simplify: SimplifyFn): Node;
13
- export { simplifyCall };
19
+ export { isSupportedMathFunction, simplifyCall };
@@ -1,4 +1,11 @@
1
- export type BaseType = "length" | "angle" | "time" | "frequency" | "resolution" | "flex" | "percentage";
1
+ export type BaseType =
2
+ | 'length'
3
+ | 'angle'
4
+ | 'time'
5
+ | 'frequency'
6
+ | 'resolution'
7
+ | 'flex'
8
+ | 'percentage';
2
9
  /**
3
10
  * @param {string} unit
4
11
  * @return {BaseType | null}