postcss-calc 11.0.0-rc.3 → 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.3",
3
+ "version": "11.0.0",
4
4
  "type": "module",
5
5
  "description": "PostCSS plugin to reduce calc()",
6
6
  "keywords": [
@@ -29,12 +29,12 @@
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
33
  },
34
34
  "devEngines": {
35
35
  "packageManager": {
36
36
  "name": "pnpm",
37
- "version": "11.21.0"
37
+ "version": "11.22.0"
38
38
  }
39
39
  },
40
40
  "devDependencies": {
@@ -42,14 +42,14 @@
42
42
  "@rmenke/css-tokenizer-tests": "^1.2.0",
43
43
  "@types/node": "^26.2.0",
44
44
  "fast-check": "^4.9.0",
45
- "oxfmt": "^0.63.0",
46
- "oxlint": "^1.78.0",
45
+ "oxfmt": "^0.64.0",
46
+ "oxlint": "^1.79.0",
47
47
  "postcss": "^8.5.26",
48
48
  "typescript": "~7.0.2"
49
49
  },
50
50
  "dependencies": {
51
- "@csstools/css-tokenizer": "^4.0.0",
52
- "postcss-value-parser": "^4.2.0"
51
+ "@csstools/css-parser-algorithms": "^4.0.0",
52
+ "@csstools/css-tokenizer": "^4.0.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "postcss": "^8.5.25"
@@ -57,6 +57,8 @@
57
57
  "scripts": {
58
58
  "lint": "oxlint . && tsc && oxfmt --check",
59
59
  "fmt": "oxfmt",
60
- "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"
61
63
  }
62
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/parser.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Pratt parser. +/- emit Sum nodes; */÷ emit Product nodes. node.js
2
2
  // constructors flatten and normalize on construction, while parenthesized
3
3
  // sums retain a grouping marker for the opaque-subtraction invariant.
4
- import { mkSum, mkProduct, negate } from './node.js';
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,13 +254,13 @@ 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) => {
@@ -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';
@@ -160,13 +160,13 @@ function serializeExpr(node, prec) {
160
160
  */
161
161
  function displaySign(term) {
162
162
  const { sign, node } = term;
163
- if (node.type === 'Num' && isFinite(node.value) && node.value < 0) {
163
+ if (node.type === 'Num' && Number.isFinite(node.value) && node.value < 0) {
164
164
  return {
165
165
  sign: /** @type {1 | -1} */ (-sign),
166
166
  magnitude: { type: 'Num', value: -node.value },
167
167
  };
168
168
  }
169
- if (node.type === 'Dim' && isFinite(node.value) && node.value < 0) {
169
+ if (node.type === 'Dim' && Number.isFinite(node.value) && node.value < 0) {
170
170
  return {
171
171
  sign: /** @type {1 | -1} */ (-sign),
172
172
  magnitude: { type: 'Dim', value: -node.value, unit: node.unit },
@@ -219,7 +219,7 @@ function serializeLeadingNeg(node, prec) {
219
219
  node.factors.length > 0 &&
220
220
  node.factors[0].exponent === 1 &&
221
221
  node.factors[0].node.type === 'Num' &&
222
- isFinite(node.factors[0].node.value) &&
222
+ Number.isFinite(node.factors[0].node.value) &&
223
223
  node.factors[0].node.value !== 0
224
224
  ) {
225
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
  }
@@ -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' };
@@ -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 };