postcss-calc 11.1.0 → 11.1.1

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.1",
4
4
  "type": "module",
5
5
  "description": "PostCSS plugin to reduce calc()",
6
6
  "keywords": [
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
  });
@@ -1,7 +1,6 @@
1
1
  // Spec: https://www.w3.org/TR/css-values-4/#serialize-a-calculation-tree
2
- // Outer calc() is added only when the top-level result contains an
3
- // arithmetic operator. A Sum inside a Product is the only place parens
4
- // are ever required on valid canonical input.
2
+ // Outer calc() is added when the top-level result contains an arithmetic
3
+ // operator, or when a finite scalar is negative.
5
4
 
6
5
  import { num, dim } from './node.js';
7
6
 
@@ -13,6 +12,7 @@ import { num, dim } from './node.js';
13
12
  * @typedef {object} SerializeOptions
14
13
  * @property {number | false} [precision] Decimal places for numbers. `false` disables rounding. Default 5.
15
14
  * @property {string} [calcName] Wrapper name to use when `calc()` is needed. Default `'calc'`.
15
+ * @property {boolean} [unwrapSingleNegativeNumber] Serialize finite negative scalars without a wrapper. Internal selector-only mode.
16
16
  */
17
17
 
18
18
  // Below this is float noise, not a value: `0.1 + 0.2 - 0.3` is 5.5e-17.
@@ -80,6 +80,20 @@ function serializeNumber(v) {
80
80
  return text;
81
81
  }
82
82
 
83
+ /**
84
+ * Round and serialize a finite scalar once so callers can use the same value
85
+ * to decide its syntactic context and render its text.
86
+ *
87
+ * @param {import('./node.js').Num | import('./node.js').Dim} node
88
+ * @param {number | false} prec
89
+ * @return {{value: number, text: string}}
90
+ */
91
+ function serializeScalar(node, prec) {
92
+ const value = round(node.value, prec);
93
+ const text = `${serializeNumber(value)}${node.type === 'Dim' ? node.unit : ''}`;
94
+ return { value, text };
95
+ }
96
+
83
97
  /**
84
98
  * @param {Node} node
85
99
  * @param {SerializeOptions} [opts]
@@ -98,6 +112,22 @@ function serialize(node, opts = {}) {
98
112
  return `${calcName}(${degenerateKeyword(node.value)} * 1${node.unit})`;
99
113
  }
100
114
 
115
+ if (node.type === 'Num' || node.type === 'Dim') {
116
+ const scalar = serializeScalar(node, prec);
117
+
118
+ // A finite negative scalar must stay inside calc() so CSS parses it as a
119
+ // calculation result (and can apply range clamping) rather than as an
120
+ // invalid bare value. Base this on the serialized value so tiny negative
121
+ // floating-point noise that rounds to zero does not get wrapped.
122
+ if (scalar.value < 0) {
123
+ return opts.unwrapSingleNegativeNumber
124
+ ? scalar.text
125
+ : `${calcName}(${scalar.text})`;
126
+ }
127
+
128
+ return scalar.text;
129
+ }
130
+
101
131
  // A grouped sum with a leading negative term is the canonical result of
102
132
  // negating a parenthesized expression. Re-invert its terms for the body so
103
133
  // the grouping survives as `-(...)` instead of becoming `-a - b`.
@@ -114,12 +144,7 @@ function serialize(node, opts = {}) {
114
144
  return `${calcName}(-(${serializeSumTerms(invertedTerms, prec)}))`;
115
145
  }
116
146
 
117
- if (
118
- node.type === 'Num' ||
119
- node.type === 'Dim' ||
120
- node.type === 'Ident' ||
121
- node.type === 'Call'
122
- ) {
147
+ if (node.type === 'Ident' || node.type === 'Call') {
123
148
  return serializeExpr(node, prec);
124
149
  }
125
150
 
@@ -145,7 +170,7 @@ function serializeExpr(node, prec) {
145
170
  if (isDegenerate(node.value)) {
146
171
  return degenerateKeyword(node.value);
147
172
  }
148
- return serializeNumber(round(node.value, prec));
173
+ return serializeScalar(node, prec).text;
149
174
  case 'Dim':
150
175
  if (isDegenerate(node.value)) {
151
176
  // Nested degenerate Dim wraps in calc() so the `<kw> * 1<unit>` form
@@ -153,7 +178,7 @@ function serializeExpr(node, prec) {
153
178
  // inside a Product — `0 * Dim(Infinity, px)` would re-fold as NaN.
154
179
  return `calc(${degenerateKeyword(node.value)} * 1${node.unit})`;
155
180
  }
156
- return `${serializeNumber(round(node.value, prec))}${node.unit}`;
181
+ return serializeScalar(node, prec).text;
157
182
  case 'Ident':
158
183
  return node.name;
159
184
  case 'Call': {
package/src/reduce.js CHANGED
@@ -27,6 +27,7 @@ const BLOCK_CLOSE = new Map([
27
27
  * @typedef {object} ReduceCalcOptions
28
28
  * @property {number | false} [precision]
29
29
  * @property {boolean} [warnWhenCannotResolve]
30
+ * @property {boolean} [unwrapSingleNegativeNumber] Serialize finite negative results without a `calc()` wrapper. Defaults to `false`.
30
31
  * @property {(error: Error, input: string) => void} [onParseError] Invoked when parse/simplify throws.
31
32
  * @property {(message: string) => void} [onWarn] Invoked when `warnWhenCannotResolve` is set and an expression cannot be reduced to a single value.
32
33
  */
@@ -34,9 +35,7 @@ const BLOCK_CLOSE = new Map([
34
35
  /** @typedef {Required<Omit<ReduceCalcOptions, 'onParseError' | 'onWarn'>> & Pick<ReduceCalcOptions, 'onParseError' | 'onWarn'>} ResolvedReduceCalcOptions */
35
36
 
36
37
  /**
37
- * Fields threaded unchanged through the token-range walk.
38
- * `value` is the original full property text, used only for the
39
- * warnWhenCannotResolve message.
38
+ * Fields threaded through the internal token-range walk.
40
39
  *
41
40
  * @typedef {object} TransformContext
42
41
  * @property {ResolvedReduceCalcOptions} options
@@ -51,7 +50,6 @@ const BLOCK_CLOSE = new Map([
51
50
  * @property {number} end
52
51
  * @property {import('./lib/node.js').Node} node
53
52
  * @property {string} calcName
54
- * @property {string} matchedName
55
53
  */
56
54
 
57
55
  /**
@@ -105,7 +103,6 @@ function walkTokens(start, expectedClose, ctx, transform) {
105
103
  end,
106
104
  node,
107
105
  calcName: isCalc ? name : 'calc',
108
- matchedName: name,
109
106
  });
110
107
  } catch (error) {
111
108
  const err = error instanceof Error ? error : new Error('Error');
@@ -117,6 +114,17 @@ function walkTokens(start, expectedClose, ctx, transform) {
117
114
  return ctx.tokens.length - 1;
118
115
  }
119
116
 
117
+ /**
118
+ * @param {import('./lib/node.js').Node} node
119
+ * @return {boolean}
120
+ */
121
+ function isUnresolvedResult(node) {
122
+ if (node.type === 'Sum' || node.type === 'Product') {
123
+ return true;
124
+ }
125
+ return node.type === 'Call' && isSupportedMathFunction(node.name);
126
+ }
127
+
120
128
  /**
121
129
  * Simplify every supported CSS math function in a component-value string.
122
130
  * Text outside those functions is preserved byte-for-byte.
@@ -131,7 +139,12 @@ function reduceCalc(value, opts) {
131
139
  }
132
140
 
133
141
  /** @type {ResolvedReduceCalcOptions} */
134
- const options = { precision: 5, warnWhenCannotResolve: false, ...opts };
142
+ const options = {
143
+ precision: 5,
144
+ warnWhenCannotResolve: false,
145
+ unwrapSingleNegativeNumber: false,
146
+ ...opts,
147
+ };
135
148
  const tokens = cssTokenize({ css: value });
136
149
  /** @type {Replacement[]} */
137
150
  const replacements = [];
@@ -147,11 +160,9 @@ function reduceCalc(value, opts) {
147
160
  const text = serialize(replacement.node, {
148
161
  precision: options.precision,
149
162
  calcName: replacement.calcName,
163
+ unwrapSingleNegativeNumber: options.unwrapSingleNegativeNumber,
150
164
  });
151
- if (
152
- options.warnWhenCannotResolve &&
153
- text.startsWith(`${replacement.matchedName}(`)
154
- ) {
165
+ if (options.warnWhenCannotResolve && isUnresolvedResult(replacement.node)) {
155
166
  options.onWarn?.('Could not reduce expression: ' + value);
156
167
  }
157
168
  output += value.slice(lastIndex, replacement.start) + text;
@@ -11,6 +11,10 @@ export type SerializeOptions = {
11
11
  * Wrapper name to use when `calc()` is needed. Default `'calc'`.
12
12
  */
13
13
  calcName?: string;
14
+ /**
15
+ * Serialize finite negative scalars without a wrapper. Internal selector-only mode.
16
+ */
17
+ unwrapSingleNegativeNumber?: boolean;
14
18
  };
15
19
  /**
16
20
  * @param {Node} node
package/types/reduce.d.ts CHANGED
@@ -2,6 +2,10 @@ import { hasPotentialMathFunction, QUICK_MATH_TEST } from './lib/simplify/call.j
2
2
  export type ReduceCalcOptions = {
3
3
  precision?: number | false;
4
4
  warnWhenCannotResolve?: boolean;
5
+ /**
6
+ * Serialize finite negative results without a `calc()` wrapper. Defaults to `false`.
7
+ */
8
+ unwrapSingleNegativeNumber?: boolean;
5
9
  /**
6
10
  * Invoked when parse/simplify throws.
7
11
  */
@@ -23,7 +27,6 @@ export type Replacement = {
23
27
  end: number;
24
28
  node: import('./lib/node.js').Node;
25
29
  calcName: string;
26
- matchedName: string;
27
30
  };
28
31
  /**
29
32
  * Simplify every supported CSS math function in a component-value string.