postcss-calc 11.1.0 → 11.1.2

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
@@ -68,7 +68,8 @@ reduceCalc('min(50px, calc(2 * 40px))');
68
68
  // => '50px'
69
69
  ```
70
70
 
71
- It accepts `precision`, `warnWhenCannotResolve`, `onParseError`, and `onWarn`:
71
+ It accepts `precision`, `unwrapSingleNegativeNumber`, `warnWhenCannotResolve`, `onParseError`,
72
+ and `onWarn`:
72
73
 
73
74
  ```js
74
75
  const result = reduceCalc('calc(100% + var(--gap))', {
@@ -84,7 +85,30 @@ const result = reduceCalc('calc(100% + var(--gap))', {
84
85
  Unlike the PostCSS plugin, the standalone reducer does not show warnings
85
86
  by default; provide `onParseError` and/or `onWarn` if you want diagnostics.
86
87
 
87
- ### Options
88
+ ### Standalone reducer options
89
+
90
+ #### `unwrapSingleNegativeNumber` (default: `false`)
91
+
92
+ Controls whether a finite negative result is serialized as a bare value or
93
+ wrapped in `calc()`. Keep the default when reducing declaration values; set it
94
+ to `true` when the surrounding CSS context requires a bare negative value, such
95
+ as a selector:
96
+
97
+ ```js
98
+ reduceCalc('calc(5px - 10px)');
99
+ // => 'calc(-5px)'
100
+
101
+ reduceCalc('calc(5px - 10px)', { unwrapNegativeNumbers: true });
102
+ // => '-5px'
103
+ ```
104
+
105
+ ### PostCSS plugin options
106
+
107
+ These options apply when using the PostCSS plugin:
108
+
109
+ ```js
110
+ postcss().use(calc({ precision: 10 }));
111
+ ```
88
112
 
89
113
  #### `precision` (default: `5`)
90
114
 
@@ -139,7 +163,11 @@ With `mediaQueries: true`, this becomes:
139
163
 
140
164
  #### `selectors` (default: `false`)
141
165
 
142
- Allows calc() usage as part of selectors.
166
+ Reduces `calc()` functions found in selectors. Selectors do not accept
167
+ `calc()` functions, so the plugin replaces them with their reduced values.
168
+ Finite negative results are serialized as bare values because a selector cannot
169
+ contain a `calc()` function; the plugin enables `unwrapSingleNegativeNumber` automatically
170
+ for selectors.
143
171
 
144
172
  ```js
145
173
  var out = postcss()
@@ -163,11 +191,13 @@ Callback invoked when a `calc()` body fails to parse or simplify. Matches
163
191
  [`@csstools/css-calc`][csstools-css-calc]'s shape:
164
192
 
165
193
  ```js
166
- calc({
167
- onParseError: (err, input) => {
168
- throw err; // or log, route to a different channel, etc.
169
- },
170
- });
194
+ postcss().use(
195
+ calc({
196
+ onParseError: (err, input) => {
197
+ throw err; // or log, route to a different channel, etc.
198
+ },
199
+ })
200
+ );
171
201
  ```
172
202
 
173
203
  When omitted, errors are reported via PostCSS `result.warn()` so the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postcss-calc",
3
- "version": "11.1.0",
3
+ "version": "11.1.2",
4
4
  "type": "module",
5
5
  "description": "PostCSS plugin to reduce calc()",
6
6
  "keywords": [
@@ -38,16 +38,16 @@
38
38
  "devEngines": {
39
39
  "packageManager": {
40
40
  "name": "pnpm",
41
- "version": "11.25.0"
41
+ "version": "12.3.4"
42
42
  }
43
43
  },
44
44
  "devDependencies": {
45
45
  "@csstools/css-calc": "^3.3.0",
46
46
  "@rmenke/css-tokenizer-tests": "^1.2.0",
47
- "@types/node": "^26.4.1",
47
+ "@types/node": "^26.5.0",
48
48
  "fast-check": "^4.9.0",
49
- "oxfmt": "^0.66.0",
50
- "oxlint": "^1.81.0",
49
+ "oxfmt": "^0.67.0",
50
+ "oxlint": "^1.82.0",
51
51
  "postcss": "^8.5.28",
52
52
  "typescript": "~7.0.2"
53
53
  },
@@ -61,6 +61,7 @@
61
61
  "lint": "oxlint . && tsc && oxfmt --check",
62
62
  "fmt": "oxfmt",
63
63
  "benchmark:arithmetic-chains": "node scripts/benchmark-arithmetic-chains.mjs",
64
+ "benchmark:nested-fallbacks": "node scripts/benchmark-nested-fallbacks.mjs",
64
65
  "test": "node --test --test-reporter=dot 'test/**/*.test.mjs' test/index.cjs test/convertUnit.cjs",
65
66
  "test:mutation:corpus": "node test/mutation/corpus-selection.mjs",
66
67
  "test:corpus:full": "POSTCSS_CALC_FULL_CORPUS=1 node --test test/conformance/corpus.test.mjs"
package/src/index.js CHANGED
@@ -24,9 +24,17 @@ import reduceCalc, { hasPotentialMathFunction } from './reduce.js';
24
24
  * @param {(target: import('postcss').ChildNode, value: string) => void} setProp
25
25
  * @param {ResolvedOptions} options
26
26
  * @param {import('postcss').Result} result
27
+ * @param {boolean} unwrapSingleNegativeNumber
27
28
  * @return {void}
28
29
  */
29
- function applyTransform(node, current, setProp, options, result) {
30
+ function applyTransform(
31
+ node,
32
+ current,
33
+ setProp,
34
+ options,
35
+ result,
36
+ unwrapSingleNegativeNumber
37
+ ) {
30
38
  if (!hasPotentialMathFunction(current)) {
31
39
  return;
32
40
  }
@@ -41,6 +49,7 @@ function applyTransform(node, current, setProp, options, result) {
41
49
  onWarn: (message) => {
42
50
  result.warn(message, { plugin: 'postcss-calc', node });
43
51
  },
52
+ unwrapSingleNegativeNumber,
44
53
  });
45
54
  if (transformed !== current) {
46
55
  setProp(node, transformed);
@@ -77,7 +86,8 @@ function pluginCreator(opts) {
77
86
  /** @type {import('postcss').Declaration} */ (n).value = v;
78
87
  },
79
88
  options,
80
- result
89
+ result,
90
+ false
81
91
  );
82
92
  }
83
93
  if (node.type === 'atrule' && options.mediaQueries) {
@@ -88,7 +98,8 @@ function pluginCreator(opts) {
88
98
  /** @type {import('postcss').AtRule} */ (n).params = v;
89
99
  },
90
100
  options,
91
- result
101
+ result,
102
+ false
92
103
  );
93
104
  }
94
105
  if (node.type === 'rule' && options.selectors) {
@@ -101,7 +112,8 @@ function pluginCreator(opts) {
101
112
  /** @type {import('postcss').Rule} */ (n).selector = v;
102
113
  },
103
114
  options,
104
- result
115
+ result,
116
+ true
105
117
  );
106
118
  }
107
119
  });
@@ -139,4 +139,18 @@ function convert(value, from, to) {
139
139
  return (value * f) / t;
140
140
  }
141
141
 
142
- export { baseOf, convert };
142
+ /**
143
+ * Return a base type only for units present in the static conversion table.
144
+ * Units with context-dependent values (em, rem, vw, etc.) return null.
145
+ * @param {string} unit
146
+ * @return {BaseType | null}
147
+ */
148
+ function staticBaseOf(unit) {
149
+ const key = unit.toLowerCase();
150
+ if (TO_CANONICAL[key] === undefined) {
151
+ return null;
152
+ }
153
+ return UNIT_TO_BASE[key] ?? null;
154
+ }
155
+
156
+ export { baseOf, staticBaseOf, convert };
package/src/lib/node.js CHANGED
@@ -16,9 +16,9 @@
16
16
 
17
17
  /**
18
18
  * @typedef {{type: 'Num', value: number}} Num
19
- * @typedef {{type: 'Dim', value: number, unit: string}} Dim
20
- * @typedef {{type: 'Ident', name: string}} Ident
21
- * @typedef {{type: 'Call', name: string, args: Node[]}} Call
19
+ * @typedef {{type: 'Dim', value: number, unit: string, rawUnit?: string}} Dim
20
+ * @typedef {{type: 'Ident', name: string, rawName?: string}} Ident
21
+ * @typedef {{type: 'Call', name: string, args: Node[], rawName?: string}} Call
22
22
  * @typedef {{sign: 1 | -1, node: Node}} SumTerm Sign is always +1 when node is Num or Dim.
23
23
  * @typedef {{type: 'Sum', terms: SumTerm[], grouped?: boolean}} Sum
24
24
  * @typedef {{exponent: 1 | -1, node: Node}} ProductFactor exponent +1 = numerator, -1 = denominator.
@@ -37,27 +37,36 @@ function num(value) {
37
37
  /**
38
38
  * @param {number} value
39
39
  * @param {string} unit
40
+ * @param {string} [rawUnit]
40
41
  * @return {Dim}
41
42
  */
42
- function dim(value, unit) {
43
- return { type: 'Dim', value, unit };
43
+ function dim(value, unit, rawUnit) {
44
+ return rawUnit === undefined
45
+ ? { type: 'Dim', value, unit }
46
+ : { type: 'Dim', value, unit, rawUnit };
44
47
  }
45
48
 
46
49
  /**
47
50
  * @param {string} name
51
+ * @param {string} [rawName]
48
52
  * @return {Ident}
49
53
  */
50
- function ident(name) {
51
- return { type: 'Ident', name };
54
+ function ident(name, rawName) {
55
+ return rawName === undefined
56
+ ? { type: 'Ident', name }
57
+ : { type: 'Ident', name, rawName };
52
58
  }
53
59
 
54
60
  /**
55
61
  * @param {string} name
56
62
  * @param {Node[]} args
63
+ * @param {string} [rawName]
57
64
  * @return {Call}
58
65
  */
59
- function call(name, args) {
60
- return { type: 'Call', name, args };
66
+ function call(name, args, rawName) {
67
+ return rawName === undefined
68
+ ? { type: 'Call', name, args }
69
+ : { type: 'Call', name, args, rawName };
61
70
  }
62
71
 
63
72
  /**
@@ -99,14 +108,9 @@ function pushSumTerm(out, term) {
99
108
 
100
109
  // sign=-1 around a Num/Dim leaf collapses into the value's sign — the
101
110
  // canonical-form rule downstream code relies on.
102
- if (sign === -1) {
103
- if (node.type === 'Num') {
104
- node = num(-node.value);
105
- sign = 1;
106
- } else if (node.type === 'Dim') {
107
- node = dim(-node.value, node.unit);
108
- sign = 1;
109
- }
111
+ if (sign === -1 && (node.type === 'Num' || node.type === 'Dim')) {
112
+ node = negate(node);
113
+ sign = 1;
110
114
  }
111
115
 
112
116
  // Drop zero-valued Nums. Dims with value 0 stay — the unit carries type.
@@ -169,7 +173,7 @@ function negate(node) {
169
173
  return num(-node.value);
170
174
  }
171
175
  if (node.type === 'Dim') {
172
- return dim(-node.value, node.unit);
176
+ return dim(-node.value, node.unit, node.rawUnit);
173
177
  }
174
178
  if (node.type === 'Sum') {
175
179
  const result = mkSum(
@@ -0,0 +1,40 @@
1
+ // Private metadata for opaque function contents (including var() fallbacks).
2
+ // Keeping it in a WeakMap means the public calculation AST remains unchanged.
3
+ /** @typedef {import('./node.js').Node} Node */
4
+ /** @typedef {string | Node | Component[]} Component */
5
+ /** @type {WeakMap<Extract<Node, {type: 'Call'}>, Component[]>} */
6
+ const components = new WeakMap();
7
+ /** @param {Extract<Node, {type: 'Call'}>} node @param {Component[]} tree */
8
+ function setComponents(node, tree) {
9
+ components.set(node, tree);
10
+ return node;
11
+ }
12
+ /** @param {Extract<Node, {type: 'Call'}>} node */
13
+ function getComponents(node) {
14
+ return components.get(node);
15
+ }
16
+ /** @param {Component[]} tree @param {(node: Node) => Node} simplify @return {Component[]} */
17
+ function simplifyComponents(tree, simplify) {
18
+ return tree.map((part) => {
19
+ if (typeof part === 'string') return part;
20
+ if (Array.isArray(part)) return simplifyComponents(part, simplify);
21
+ return simplify(part);
22
+ });
23
+ }
24
+ /** @param {Component[]} tree @param {(node: Node) => string} serialize @return {string} */
25
+ function serializeComponents(tree, serialize) {
26
+ let result = '';
27
+ for (const part of tree) {
28
+ if (typeof part === 'string') result += part;
29
+ else if (Array.isArray(part))
30
+ result += serializeComponents(part, serialize);
31
+ else result += serialize(part);
32
+ }
33
+ return result;
34
+ }
35
+ export {
36
+ getComponents,
37
+ setComponents,
38
+ simplifyComponents,
39
+ serializeComponents,
40
+ };