postcss-calc 11.0.3 → 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.
@@ -1,7 +1,8 @@
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.
4
+
5
+ import { num, dim } from './node.js';
5
6
 
6
7
  /**
7
8
  * @typedef {import('./node.js').Node} Node
@@ -11,6 +12,7 @@
11
12
  * @typedef {object} SerializeOptions
12
13
  * @property {number | false} [precision] Decimal places for numbers. `false` disables rounding. Default 5.
13
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.
14
16
  */
15
17
 
16
18
  // Below this is float noise, not a value: `0.1 + 0.2 - 0.3` is 5.5e-17.
@@ -78,6 +80,20 @@ function serializeNumber(v) {
78
80
  return text;
79
81
  }
80
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
+
81
97
  /**
82
98
  * @param {Node} node
83
99
  * @param {SerializeOptions} [opts]
@@ -96,6 +112,22 @@ function serialize(node, opts = {}) {
96
112
  return `${calcName}(${degenerateKeyword(node.value)} * 1${node.unit})`;
97
113
  }
98
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
+
99
131
  // A grouped sum with a leading negative term is the canonical result of
100
132
  // negating a parenthesized expression. Re-invert its terms for the body so
101
133
  // the grouping survives as `-(...)` instead of becoming `-a - b`.
@@ -105,22 +137,14 @@ function serialize(node, opts = {}) {
105
137
  node.terms.length > 1 &&
106
138
  displaySign(node.terms[0]).sign === -1
107
139
  ) {
108
- const body = /** @type {Sum} */ ({
109
- type: 'Sum',
110
- terms: node.terms.map((t) => ({
111
- sign: /** @type {1 | -1} */ (-t.sign),
112
- node: t.node,
113
- })),
114
- });
115
- return `${calcName}(-(${serializeExpr(body, prec)}))`;
140
+ const invertedTerms = node.terms.map((t) => ({
141
+ sign: /** @type {1 | -1} */ (-t.sign),
142
+ node: t.node,
143
+ }));
144
+ return `${calcName}(-(${serializeSumTerms(invertedTerms, prec)}))`;
116
145
  }
117
146
 
118
- if (
119
- node.type === 'Num' ||
120
- node.type === 'Dim' ||
121
- node.type === 'Ident' ||
122
- node.type === 'Call'
123
- ) {
147
+ if (node.type === 'Ident' || node.type === 'Call') {
124
148
  return serializeExpr(node, prec);
125
149
  }
126
150
 
@@ -146,7 +170,7 @@ function serializeExpr(node, prec) {
146
170
  if (isDegenerate(node.value)) {
147
171
  return degenerateKeyword(node.value);
148
172
  }
149
- return serializeNumber(round(node.value, prec));
173
+ return serializeScalar(node, prec).text;
150
174
  case 'Dim':
151
175
  if (isDegenerate(node.value)) {
152
176
  // Nested degenerate Dim wraps in calc() so the `<kw> * 1<unit>` form
@@ -154,7 +178,7 @@ function serializeExpr(node, prec) {
154
178
  // inside a Product — `0 * Dim(Infinity, px)` would re-fold as NaN.
155
179
  return `calc(${degenerateKeyword(node.value)} * 1${node.unit})`;
156
180
  }
157
- return `${serializeNumber(round(node.value, prec))}${node.unit}`;
181
+ return serializeScalar(node, prec).text;
158
182
  case 'Ident':
159
183
  return node.name;
160
184
  case 'Call': {
@@ -182,27 +206,27 @@ function displaySign(term) {
182
206
  if (node.type === 'Num' && Number.isFinite(node.value) && node.value < 0) {
183
207
  return {
184
208
  sign: /** @type {1 | -1} */ (-sign),
185
- magnitude: { type: 'Num', value: -node.value },
209
+ magnitude: num(-node.value),
186
210
  };
187
211
  }
188
212
  if (node.type === 'Dim' && Number.isFinite(node.value) && node.value < 0) {
189
213
  return {
190
214
  sign: /** @type {1 | -1} */ (-sign),
191
- magnitude: { type: 'Dim', value: -node.value, unit: node.unit },
215
+ magnitude: dim(-node.value, node.unit),
192
216
  };
193
217
  }
194
218
  return { sign, magnitude: node };
195
219
  }
196
220
 
197
221
  /**
198
- * @param {Sum} sum
222
+ * @param {import('./node.js').SumTerm[]} terms
199
223
  * @param {number | false} prec
200
224
  * @return {string}
201
225
  */
202
- function serializeSum(sum, prec) {
226
+ function serializeSumTerms(terms, prec) {
203
227
  let out = '';
204
- for (const [i, t] of sum.terms.entries()) {
205
- const { sign, magnitude } = displaySign(t);
228
+ for (let i = 0; i < terms.length; i++) {
229
+ const { sign, magnitude } = displaySign(terms[i]);
206
230
  if (i === 0) {
207
231
  if (magnitude.type === 'Sum' && magnitude.grouped) {
208
232
  const body = `(${serializeExpr(magnitude, prec)})`;
@@ -225,6 +249,15 @@ function serializeSum(sum, prec) {
225
249
  return out;
226
250
  }
227
251
 
252
+ /**
253
+ * @param {Sum} sum
254
+ * @param {number | false} prec
255
+ * @return {string}
256
+ */
257
+ function serializeSum(sum, prec) {
258
+ return serializeSumTerms(sum.terms, prec);
259
+ }
260
+
228
261
  /**
229
262
  * Fold a leading negation into a finite leading Num if there is one
230
263
  * (`-(0.5 * x)` → `-0.5 * x`); else use `-(…)` for Sum/Product or `-x`.
@@ -249,11 +282,8 @@ function serializeLeadingNeg(node, prec) {
249
282
  const negatedFactors =
250
283
  negatedValue === 1
251
284
  ? rest
252
- : [
253
- { exponent: 1, node: { type: 'Num', value: negatedValue } },
254
- ...rest,
255
- ];
256
- return serializeProduct({ type: 'Product', factors: negatedFactors }, prec);
285
+ : [{ exponent: 1, node: num(negatedValue) }, ...rest];
286
+ return serializeFactors(negatedFactors, prec);
257
287
  }
258
288
  const body = serializeExpr(node, prec);
259
289
  return node.type === 'Sum' || node.type === 'Product'
@@ -262,13 +292,14 @@ function serializeLeadingNeg(node, prec) {
262
292
  }
263
293
 
264
294
  /**
265
- * @param {Product} product
295
+ * @param {ProductFactor[]} factors
266
296
  * @param {number | false} prec
267
297
  * @return {string}
268
298
  */
269
- function serializeProduct(product, prec) {
299
+ function serializeFactors(factors, prec) {
270
300
  let out = '';
271
- for (const [i, f] of product.factors.entries()) {
301
+ for (let i = 0; i < factors.length; i++) {
302
+ const f = factors[i];
272
303
  let body = serializeExpr(f.node, prec);
273
304
  // A Sum factor needs parens: `a * (b + c)`. Flat canonical form means
274
305
  // this is the only place parens are required.
@@ -285,4 +316,13 @@ function serializeProduct(product, prec) {
285
316
  return out;
286
317
  }
287
318
 
319
+ /**
320
+ * @param {Product} product
321
+ * @param {number | false} prec
322
+ * @return {string}
323
+ */
324
+ function serializeProduct(product, prec) {
325
+ return serializeFactors(product.factors, prec);
326
+ }
327
+
288
328
  export { serialize };
@@ -21,7 +21,7 @@ import { convert } from '../convertUnits.js';
21
21
  * @return {UnitBucket[]}
22
22
  */
23
23
  function mergeConvertibleBuckets(buckets) {
24
- const ordered = [...buckets].sort((a, b) => a.order - b.order);
24
+ const ordered = buckets.sort((a, b) => a.order - b.order);
25
25
  /** @type {Set<string>} */ const merged = new Set();
26
26
  /** @type {UnitBucket[]} */ const out = [];
27
27
  for (const b of ordered) {
@@ -26,7 +26,7 @@ import { call } from '../node.js';
26
26
  // Bare CSS math functions with implemented simplification semantics, keyed
27
27
  // by lowercase name. calc() and its vendor-prefixed forms are handled
28
28
  // separately as wrappers in simplifyCall. This map is the single source of
29
- // truth for both dispatch and `isSupportedMathFunction`.
29
+ // truth for dispatch, `isSupportedMathFunction`, and `QUICK_MATH_TEST`.
30
30
  /** @type {Map<string, MathSimplifier>} */
31
31
  const MATH_SIMPLIFIERS = new Map([
32
32
  ['min', simplifyMinMax],
@@ -51,6 +51,29 @@ const MATH_SIMPLIFIERS = new Map([
51
51
  ['exp', (_name, args) => simplifyExp(args)],
52
52
  ]);
53
53
 
54
+ const mathFnNames = [...MATH_SIMPLIFIERS.keys()].sort(
55
+ (a, b) => b.length - a.length
56
+ );
57
+
58
+ const QUICK_MATH_TEST = new RegExp(
59
+ `(?:-(?:webkit|moz)-)?(?:calc|${mathFnNames.join('|')})\\(`,
60
+ 'i'
61
+ );
62
+
63
+ /**
64
+ * Fast check to determine whether a CSS component value could contain
65
+ * a supported calculation or math function call (or an escape sequence
66
+ * that could decode to one).
67
+ *
68
+ * @param {string} value
69
+ * @return {boolean}
70
+ */
71
+ function hasPotentialMathFunction(value) {
72
+ return (
73
+ value.includes('(') && (QUICK_MATH_TEST.test(value) || value.includes('\\'))
74
+ );
75
+ }
76
+
54
77
  /**
55
78
  * Whether a bare CSS math function has an implemented simplifier.
56
79
  *
@@ -91,4 +114,9 @@ function simplifyCall(node, simplify) {
91
114
  return call(node.name, args);
92
115
  }
93
116
 
94
- export { isSupportedMathFunction, simplifyCall };
117
+ export {
118
+ isSupportedMathFunction,
119
+ simplifyCall,
120
+ hasPotentialMathFunction,
121
+ QUICK_MATH_TEST,
122
+ };
@@ -1,5 +1,6 @@
1
- import { num, dim, call } from '../node.js';
2
- import { foldConstArgs } from './fold.js';
1
+ import { call } from '../node.js';
2
+ import { foldConstArgs, foldResult } from './fold.js';
3
+ import { simplifyMinMax } from './min-max.js';
3
4
 
4
5
  /** @typedef {import('../node.js').Node} Node */
5
6
 
@@ -9,16 +10,37 @@ import { foldConstArgs } from './fold.js';
9
10
  */
10
11
  function simplifyClamp(args) {
11
12
  if (args.length === 3) {
13
+ const minNone = isNone(args[0]);
14
+ const maxNone = isNone(args[2]);
15
+
16
+ if (minNone && maxNone) {
17
+ return args[1];
18
+ }
19
+ if (minNone) {
20
+ return simplifyMinMax('min', [args[1], args[2]]);
21
+ }
22
+ if (maxNone) {
23
+ return simplifyMinMax('max', [args[0], args[1]]);
24
+ }
25
+
12
26
  const fold = foldConstArgs(args);
13
27
  if (fold !== null) {
14
28
  const [lo, v, hi] = /** @type {[number, number, number]} */ (fold.values);
15
29
  // Spec §10.8: clamp(MIN, VAL, MAX) = max(MIN, min(VAL, MAX)). The
16
30
  // outer max(MIN, …) means MIN wins when MIN > MAX — not MAX.
17
31
  const clamped = Math.max(lo, Math.min(v, hi));
18
- return fold.unit === '' ? num(clamped) : dim(clamped, fold.unit);
32
+ return foldResult(fold, clamped);
19
33
  }
20
34
  }
21
35
  return call('clamp', args);
22
36
  }
23
37
 
38
+ /**
39
+ * @param {Node} node
40
+ * @return {boolean}
41
+ */
42
+ function isNone(node) {
43
+ return node.type === 'Ident' && node.name.toLowerCase() === 'none';
44
+ }
45
+
24
46
  export { simplifyClamp };
@@ -1,8 +1,21 @@
1
+ import { num, dim } from '../node.js';
1
2
  import { baseOf, convert } from '../convertUnits.js';
2
3
 
3
4
  /** @typedef {import('../node.js').Node} Node */
5
+ /** @typedef {import('../node.js').Num} Num */
6
+ /** @typedef {import('../node.js').Dim} Dim */
4
7
  /** @typedef {import('../convertUnits.js').BaseType} BaseType */
5
8
 
9
+ /**
10
+ * Construct a Num or Dim node from a folded result.
11
+ * @param {{ unit: string }} fold
12
+ * @param {number} value
13
+ * @return {Num | Dim}
14
+ */
15
+ function foldResult(fold, value) {
16
+ return fold.unit === '' ? num(value) : dim(value, fold.unit);
17
+ }
18
+
6
19
  /**
7
20
  * @param {Node[]} args
8
21
  * @return {{ values: number[], unit: string } | null}
@@ -62,4 +75,4 @@ function foldDimArgs(args, unit, base) {
62
75
  return { values, unit };
63
76
  }
64
77
 
65
- export { foldConstArgs };
78
+ export { foldConstArgs, foldResult };
@@ -1,7 +1,7 @@
1
1
  // §10.5 — hypot. Empty args return null from foldConstArgs naturally.
2
2
 
3
- import { num, dim, call } from '../node.js';
4
- import { foldConstArgs } from './fold.js';
3
+ import { call } from '../node.js';
4
+ import { foldConstArgs, foldResult } from './fold.js';
5
5
 
6
6
  /** @typedef {import('../node.js').Node} Node */
7
7
 
@@ -16,7 +16,7 @@ function simplifyHypot(args) {
16
16
  }
17
17
  const sumSq = fold.values.reduce((acc, v) => acc + v * v, 0);
18
18
  const result = Math.sqrt(sumSq);
19
- return fold.unit === '' ? num(result) : dim(result, fold.unit);
19
+ return foldResult(fold, result);
20
20
  }
21
21
 
22
22
  export { simplifyHypot };
@@ -1,5 +1,5 @@
1
- import { num, dim, call } from '../node.js';
2
- import { foldConstArgs } from './fold.js';
1
+ import { call } from '../node.js';
2
+ import { foldConstArgs, foldResult } from './fold.js';
3
3
 
4
4
  /** @typedef {import('../node.js').Node} Node */
5
5
 
@@ -13,7 +13,7 @@ function simplifyMinMax(name, args) {
13
13
  if (fold !== null) {
14
14
  const fn = name.toLowerCase() === 'min' ? Math.min : Math.max;
15
15
  const value = fn(...fold.values);
16
- return fold.unit === '' ? num(value) : dim(value, fold.unit);
16
+ return foldResult(fold, value);
17
17
  }
18
18
  return call(name, args);
19
19
  }
@@ -1,5 +1,5 @@
1
- import { num, dim, call } from '../node.js';
2
- import { foldConstArgs } from './fold.js';
1
+ import { num, call } from '../node.js';
2
+ import { foldConstArgs, foldResult } from './fold.js';
3
3
 
4
4
  /** @typedef {import('../node.js').Node} Node */
5
5
 
@@ -23,7 +23,7 @@ function simplifyModRem(name, args) {
23
23
  if (Number.isNaN(result)) {
24
24
  return num(Number.NaN);
25
25
  }
26
- return fold.unit === '' ? num(result) : dim(result, fold.unit);
26
+ return foldResult(fold, result);
27
27
  }
28
28
 
29
29
  /**
@@ -1,5 +1,5 @@
1
- import { num, dim, ident, call } from '../node.js';
2
- import { foldConstArgs } from './fold.js';
1
+ import { num, ident, call } from '../node.js';
2
+ import { foldConstArgs, foldResult } from './fold.js';
3
3
 
4
4
  /** @typedef {import('../node.js').Node} Node */
5
5
 
@@ -58,14 +58,14 @@ function simplifyRound(args) {
58
58
  } else {
59
59
  result = a < 0 || Object.is(a, -0) ? -0 : 0;
60
60
  }
61
- return fold.unit === '' ? num(result) : dim(result, fold.unit);
61
+ return foldResult(fold, result);
62
62
  }
63
63
 
64
64
  const result = applyRound(strategy, a, b);
65
65
  if (Number.isNaN(result)) {
66
66
  return num(Number.NaN);
67
67
  }
68
- return fold.unit === '' ? num(result) : dim(result, fold.unit);
68
+ return foldResult(fold, result);
69
69
  }
70
70
 
71
71
  /**
@@ -26,6 +26,32 @@ function tokenize(input) {
26
26
  return tokenizeTokens(tokenizeCss({ css: input }), input.length);
27
27
  }
28
28
 
29
+ /**
30
+ * CSS absorbs leading signs (`-5px` is one token); the parser expects
31
+ * punct sign + unsigned numeric, so split them back out.
32
+ * @param {Token[]} tokens
33
+ * @param {string} raw
34
+ * @param {string | undefined} unit
35
+ * @param {number} pos
36
+ * @param {boolean} ws
37
+ * @return {void}
38
+ */
39
+ function pushNumeric(tokens, raw, unit, pos, ws) {
40
+ let value = /** @type {RegExpExecArray} */ (NUMERIC_RAW.exec(raw))[0];
41
+ const sign = value[0];
42
+ if (sign === '+' || sign === '-') {
43
+ tokens.push({ type: 'punct', value: sign, pos, ws });
44
+ value = value.slice(1);
45
+ pos += 1;
46
+ ws = false;
47
+ }
48
+ if (unit === undefined) {
49
+ tokens.push({ type: 'number', value, pos, ws });
50
+ } else {
51
+ tokens.push({ type: 'dimension', value, unit, pos, ws });
52
+ }
53
+ }
54
+
29
55
  /**
30
56
  * Convert a slice of an existing CSS token stream into the token subset used
31
57
  * by the calculation parser. Token positions remain relative to the original
@@ -33,52 +59,38 @@ function tokenize(input) {
33
59
  *
34
60
  * @param {import('@csstools/css-tokenizer').CSSToken[]} cssTokens
35
61
  * @param {number} eofPosition
62
+ * @param {number} [start]
63
+ * @param {number} [end]
36
64
  * @return {Token[]}
37
65
  */
38
- function tokenizeTokens(cssTokens, eofPosition) {
66
+ function tokenizeTokens(
67
+ cssTokens,
68
+ eofPosition,
69
+ start = 0,
70
+ end = cssTokens.length
71
+ ) {
39
72
  /** @type {Token[]} */
40
73
  const tokens = [];
41
74
  let ws = true;
42
75
 
43
- // CSS absorbs leading signs (`-5px` is one token); the parser expects
44
- // punct sign + unsigned numeric, so split them back out.
45
- /**
46
- * @param {string} raw
47
- * @param {string | undefined} unit
48
- * @param {number} pos
49
- * @return {void}
50
- */
51
- function pushNumeric(raw, unit, pos) {
52
- let value = /** @type {RegExpExecArray} */ (NUMERIC_RAW.exec(raw))[0];
53
- const sign = value[0];
54
- if (sign === '+' || sign === '-') {
55
- tokens.push({ type: 'punct', value: sign, pos, ws });
56
- value = value.slice(1);
57
- pos += 1;
58
- ws = false;
59
- }
60
- if (unit === undefined) {
61
- tokens.push({ type: 'number', value, pos, ws });
62
- } else {
63
- tokens.push({ type: 'dimension', value, unit, pos, ws });
64
- }
65
- ws = false;
66
- }
67
-
68
- for (const t of cssTokens) {
76
+ for (let i = start; i < end; i++) {
77
+ const t = cssTokens[i];
69
78
  switch (t[0]) {
70
79
  case CssType.Whitespace:
71
80
  case CssType.Comment:
72
81
  ws = true;
73
82
  continue;
74
83
  case CssType.Number:
75
- pushNumeric(t[1], undefined, t[2]);
84
+ pushNumeric(tokens, t[1], undefined, t[2], ws);
85
+ ws = false;
76
86
  continue;
77
87
  case CssType.Dimension:
78
- pushNumeric(t[1], t[4].unit, t[2]);
88
+ pushNumeric(tokens, t[1], t[4].unit, t[2], ws);
89
+ ws = false;
79
90
  continue;
80
91
  case CssType.Percentage:
81
- pushNumeric(t[1], '%', t[2]);
92
+ pushNumeric(tokens, t[1], '%', t[2], ws);
93
+ ws = false;
82
94
  continue;
83
95
  case CssType.Ident:
84
96
  tokens.push({ type: 'ident', value: t[4].value, pos: t[2], ws });