postcss-calc 11.2.1 → 11.2.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.
@@ -1,17 +1,21 @@
1
1
  import { baseOf, convert } from '../convertUnits.js';
2
+ import { isExact } from './exact.js';
2
3
 
3
4
  /**
4
5
  * If `dims` contain exactly one numerator / one denominator pair with the
5
- * same base type and convertible units, return the numeric factor produced
6
- * by cancelling them and the list of remaining (uncancelled) dims.
7
- * Otherwise return null. Used by `simplifyProduct` for typed division
8
- * (§10.2). More complex cancellation (e.g. `px^2 / px`) is left
6
+ * same base type and convertible units, return the two sides expressed in a
7
+ * common unit, so the caller can fold them into its own numerator and
8
+ * denominator. Otherwise return null. Used by `simplifyProduct` for typed
9
+ * division (§10.2). More complex cancellation (e.g. `px^2 / px`) is left
9
10
  * unreduced — consumers rarely rely on it and the spec doesn't require it.
10
11
  * @template {{ exponent: 1 | -1, value: number, unit: string }} D
11
12
  * @param {D[]} dims
12
- * @return {{ factor: number, remaining: D[] } | null}
13
+ * @param {number | false} [precision] The denominator is converted into the
14
+ * numerator's unit when that is exact at this precision (`1px / 1in` →
15
+ * `1 / 96`); otherwise the numerator is converted (`1in / 1px` → `96 / 1`).
16
+ * @return {{ numerator: number, denominator: number, remaining: D[] } | null}
13
17
  */
14
- function tryCancelPair(dims) {
18
+ function tryCancelPair(dims, precision = false) {
15
19
  if (dims.length !== 2) {
16
20
  return null;
17
21
  }
@@ -26,12 +30,32 @@ function tryCancelPair(dims) {
26
30
  if (!numBase || numBase !== denBase) {
27
31
  return null;
28
32
  }
29
- const converted = convert(numerator.value, numerator.unit, denominator.unit);
30
- if (converted === null) {
31
- return null;
32
- }
33
+ const converted = convert(
34
+ denominator.value,
35
+ denominator.unit,
36
+ numerator.unit
37
+ );
33
38
  // denominator.value === 0 yields ±Infinity / NaN naturally (§10.9.1).
34
- return { factor: converted / denominator.value, remaining: [] };
39
+ if (converted !== null && isExact(converted, precision)) {
40
+ return {
41
+ numerator: numerator.value,
42
+ denominator: converted,
43
+ remaining: [],
44
+ };
45
+ }
46
+ const numConverted = convert(
47
+ numerator.value,
48
+ numerator.unit,
49
+ denominator.unit
50
+ );
51
+ if (numConverted !== null && isExact(numConverted, precision)) {
52
+ return {
53
+ numerator: numConverted,
54
+ denominator: denominator.value,
55
+ remaining: [],
56
+ };
57
+ }
58
+ return null;
35
59
  }
36
60
 
37
61
  export { tryCancelPair };
@@ -1,5 +1,5 @@
1
1
  import { call } from '../node.js';
2
- import { foldConstArgs, foldResult } from './fold.js';
2
+ import { foldConstArgs, chosenArg } from './fold.js';
3
3
  import { simplifyMinMax } from './min-max.js';
4
4
 
5
5
  /** @typedef {import('../node.js').Node} Node */
@@ -29,7 +29,7 @@ function simplifyClamp(args) {
29
29
  // Spec §10.8: clamp(MIN, VAL, MAX) = max(MIN, min(VAL, MAX)). The
30
30
  // outer max(MIN, …) means MIN wins when MIN > MAX — not MAX.
31
31
  const clamped = Math.max(lo, Math.min(v, hi));
32
- return foldResult(fold, clamped);
32
+ return chosenArg(args, fold, clamped);
33
33
  }
34
34
  }
35
35
  return call('clamp', args);
@@ -0,0 +1,18 @@
1
+ import { round } from '../serialize/precision.js';
2
+
3
+ /**
4
+ * A value is exact at `precision` when rounding it for output loses nothing
5
+ * beyond float noise.
6
+ * @param {number} value
7
+ * @param {number | false} precision
8
+ * @return {boolean}
9
+ */
10
+ function isExact(value, precision) {
11
+ if (precision === false || !Number.isFinite(value)) {
12
+ return true;
13
+ }
14
+ const snapped = Number(value.toPrecision(15));
15
+ return round(snapped, precision) === snapped;
16
+ }
17
+
18
+ export { isExact };
@@ -16,6 +16,20 @@ function foldResult(fold, value) {
16
16
  return fold.unit === '' ? num(value) : dim(value, fold.unit);
17
17
  }
18
18
 
19
+ /**
20
+ * Return the argument that produced a folded min/max/clamp value, so the
21
+ * result keeps its own unit, spelling and zero sign. Falls back to a fresh
22
+ * node when no argument matches (NaN).
23
+ * @param {Node[]} args
24
+ * @param {{ values: number[], unit: string }} fold
25
+ * @param {number} value
26
+ * @return {Node}
27
+ */
28
+ function chosenArg(args, fold, value) {
29
+ const index = fold.values.findIndex((v) => Object.is(v, value));
30
+ return index === -1 ? foldResult(fold, value) : args[index];
31
+ }
32
+
19
33
  /**
20
34
  * @param {Node[]} args
21
35
  * @return {{ values: number[], unit: string } | null}
@@ -75,4 +89,4 @@ function foldDimArgs(args, unit, base) {
75
89
  return { values, unit };
76
90
  }
77
91
 
78
- export { foldConstArgs, foldResult };
92
+ export { foldConstArgs, foldResult, chosenArg };
@@ -1,5 +1,5 @@
1
1
  import { call } from '../node.js';
2
- import { foldConstArgs, foldResult } from './fold.js';
2
+ import { foldConstArgs, chosenArg } 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 foldResult(fold, value);
16
+ return chosenArg(args, fold, value);
17
17
  }
18
18
  return call(name, args);
19
19
  }
@@ -1,5 +1,6 @@
1
- import { mkSum, mkProduct, num, dim } from '../node.js';
1
+ import { mkSum, mkProduct, mkGroup, num, dim } from '../node.js';
2
2
  import { tryCancelPair } from './cancel.js';
3
+ import { isExact } from './exact.js';
3
4
 
4
5
  /**
5
6
  * @typedef {import('../node.js').Node} Node
@@ -9,12 +10,150 @@ import { tryCancelPair } from './cancel.js';
9
10
  */
10
11
 
11
12
  /**
12
- * @param {Product} product
13
+ * @param {number} a non-negative
14
+ * @param {number} b non-negative
15
+ * @return {number}
16
+ */
17
+ function gcd(a, b) {
18
+ while (b !== 0) {
19
+ [a, b] = [b, a % b];
20
+ }
21
+ return a || 1;
22
+ }
23
+
24
+ /**
25
+ * @param {{exponent: 1 | -1, value: number}[]} chain
26
+ * @return {number}
27
+ */
28
+ function chainValue(chain) {
29
+ let value = 1;
30
+ for (const f of chain) {
31
+ value = f.exponent === 1 ? value * f.value : value / f.value;
32
+ }
33
+ return value;
34
+ }
35
+
36
+ /**
37
+ * Whether the opaque factors are a single Sum of only Num/Dim terms.
38
+ * @param {ProductFactor[]} opaque
39
+ * @return {boolean}
40
+ */
41
+ function isScalarSum(opaque) {
42
+ return (
43
+ opaque.length === 1 &&
44
+ opaque[0].exponent === 1 &&
45
+ opaque[0].node.type === 'Sum' &&
46
+ opaque[0].node.terms.every(
47
+ (t) => t.node.type === 'Num' || t.node.type === 'Dim'
48
+ )
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Whether a folded quotient prints exactly. A quotient distributed over a
54
+ * scalar Sum is judged term by term, so `(3px + 6em) / 3` still folds.
55
+ * @param {number} value
56
+ * @param {ProductFactor[]} opaque
57
+ * @param {boolean} distributable
58
+ * @param {number | false} precision
59
+ * @return {boolean}
60
+ */
61
+ function foldsExactly(value, opaque, distributable, precision) {
62
+ if (!Number.isFinite(value) || isExact(value, precision)) {
63
+ return true;
64
+ }
65
+ if (!distributable) {
66
+ return false;
67
+ }
68
+ const sum = /** @type {import('../node.js').Sum} */ (opaque[0].node);
69
+ return sum.terms.every((t) =>
70
+ isExact(
71
+ value *
72
+ /** @type {import('../node.js').Num | import('../node.js').Dim} */ (
73
+ t.node
74
+ ).value,
75
+ precision
76
+ )
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Emit an inexact quotient as `numerator * dims * opaque / denominator`, with
82
+ * integer parts reduced by their gcd. A lone Dim absorbs the numerator.
83
+ * @param {number} numerator
84
+ * @param {number} denominator
85
+ * @param {{value: number, unit: string, rawUnit?: string} | null} loneDim
86
+ * @param {{exponent: 1 | -1, value: number, unit: string, rawUnit?: string}[]} dims
87
+ * @param {ProductFactor[]} opaque
88
+ * @param {number | false} precision
89
+ * @return {Node | null} null when a part would be rounded on output
90
+ */
91
+ function rationalProduct(
92
+ numerator,
93
+ denominator,
94
+ loneDim,
95
+ dims,
96
+ opaque,
97
+ precision
98
+ ) {
99
+ // Drop float noise (`1.9999999999999993 * 100`) so the parts print as
100
+ // written and the gcd below can reduce them.
101
+ let n = Number(
102
+ (loneDim === null ? numerator : numerator * loneDim.value).toPrecision(15)
103
+ );
104
+ let d = Number(denominator.toPrecision(15));
105
+ if (!isExact(n, precision) || !isExact(d, precision)) {
106
+ return null;
107
+ }
108
+ if (Number.isSafeInteger(n) && Number.isSafeInteger(d)) {
109
+ const g = gcd(Math.abs(n), Math.abs(d));
110
+ n /= g;
111
+ d /= g;
112
+ }
113
+ if (d < 0) {
114
+ n = -n;
115
+ d = -d;
116
+ }
117
+ /** @type {ProductFactor[]} */
118
+ const factors = [];
119
+ if (loneDim !== null) {
120
+ factors.push({
121
+ exponent: 1,
122
+ node: dim(n, loneDim.unit, loneDim.rawUnit),
123
+ });
124
+ } else {
125
+ if (n !== 1) {
126
+ factors.push({ exponent: 1, node: num(n) });
127
+ }
128
+ for (const dm of dims) {
129
+ factors.push({
130
+ exponent: dm.exponent,
131
+ node: dim(dm.value, dm.unit, dm.rawUnit),
132
+ });
133
+ }
134
+ }
135
+ factors.push(...opaque);
136
+ if (d !== 1) {
137
+ factors.push({ exponent: -1, node: num(d) });
138
+ }
139
+ return mkProduct(factors);
140
+ }
141
+
142
+ /**
143
+ * Fold a run of factors that contains no substitution barrier.
144
+ * @param {ProductFactor[]} items Simplified factors, flattened by the caller.
145
+ * @param {number} start
146
+ * @param {number} end
13
147
  * @param {SimplifyFn} simplify
148
+ * @param {number | false} precision
14
149
  * @return {Node}
15
150
  */
16
- function simplifyProduct(product, simplify) {
151
+ function foldFactors(items, start, end, simplify, precision) {
17
152
  let coeff = 1;
153
+ // Num factors by exponent, so an inexact quotient can be re-emitted as a
154
+ // reduced `numerator / denominator`.
155
+ let numerator = 1;
156
+ let denominator = 1;
18
157
  /** @type {{exponent: 1 | -1, value: number, unit: string, rawUnit?: string}[]} */
19
158
  const dims = [];
20
159
  /** @type {ProductFactor[]} */
@@ -28,27 +167,13 @@ function simplifyProduct(product, simplify) {
28
167
  * @return {void}
29
168
  */
30
169
  function processFactor(exponent, n) {
31
- if (n.type === 'Product') {
32
- for (const inner of n.factors) {
33
- processFactor(
34
- /** @type {1 | -1} */ (exponent * inner.exponent),
35
- inner.node
36
- );
37
- }
38
- return;
39
- }
40
- // Canonical negation form (see node.js's `negate`); flatten through it
41
- // like a nested Product so cancellation below can see what's inside.
42
- if (n.type === 'Sum' && n.terms.length === 1) {
43
- processFactor(exponent, num(-1));
44
- processFactor(exponent, n.terms[0].node);
45
- return;
46
- }
47
170
  if (n.type === 'Num') {
48
171
  if (exponent === 1) {
49
172
  coeff *= n.value;
173
+ numerator *= n.value;
50
174
  } else {
51
175
  coeff /= n.value; // §10.9.1: 1/0 → ±Infinity, 0/0 → NaN per IEEE-754
176
+ denominator *= n.value;
52
177
  }
53
178
  scalarChain.push({ exponent, value: n.value });
54
179
  return;
@@ -61,31 +186,50 @@ function simplifyProduct(product, simplify) {
61
186
  opaque.push({ exponent, node: n });
62
187
  }
63
188
 
64
- for (const f of product.factors) {
65
- processFactor(f.exponent, simplify(f.node));
189
+ for (let i = start; i < end; i++) {
190
+ processFactor(items[i].exponent, items[i].node);
66
191
  }
67
192
 
68
193
  // §10.2 typed division. Higher-power cancellation (`px^2 / px`) is left
69
194
  // unreduced — consumers don't rely on it and the spec doesn't require it.
70
- const cancelled = tryCancelPair(dims);
195
+ const cancelled = tryCancelPair(dims, precision);
71
196
  if (cancelled !== null) {
72
- coeff *= cancelled.factor;
197
+ coeff *= cancelled.numerator / cancelled.denominator;
198
+ numerator *= cancelled.numerator;
199
+ denominator *= cancelled.denominator;
73
200
  }
201
+ const divides = denominator !== 1 || cancelled !== null;
74
202
  const remainingDims = cancelled ? cancelled.remaining : dims;
203
+ const loneDim =
204
+ remainingDims.length === 1 &&
205
+ remainingDims[0].exponent === 1 &&
206
+ opaque.length === 0
207
+ ? remainingDims[0]
208
+ : null;
209
+ const value = loneDim === null ? coeff : chainValue(scalarChain);
210
+ const distributable = remainingDims.length === 0 && isScalarSum(opaque);
211
+
212
+ // An inexact quotient stays symbolic when its numerator and denominator
213
+ // print exactly; otherwise it is folded and rounded at serialization.
214
+ if (divides && !foldsExactly(value, opaque, distributable, precision)) {
215
+ const rational = rationalProduct(
216
+ numerator,
217
+ denominator,
218
+ loneDim,
219
+ remainingDims,
220
+ opaque,
221
+ precision
222
+ );
223
+ if (rational !== null) {
224
+ return rational;
225
+ }
226
+ }
75
227
 
76
228
  // §10.10 distributive multiplication: `0.5 * (100vw - 10px)` → `50vw - 5px`.
77
229
  // Only distribute when every Sum term is Num/Dim — partial distribution
78
230
  // over opaque terms matches neither the legacy implementation nor csstools.
79
- if (
80
- remainingDims.length === 0 &&
81
- opaque.length === 1 &&
82
- opaque[0].exponent === 1 &&
83
- opaque[0].node.type === 'Sum' &&
84
- opaque[0].node.terms.every(
85
- (t) => t.node.type === 'Num' || t.node.type === 'Dim'
86
- )
87
- ) {
88
- const sum = opaque[0].node;
231
+ if (distributable) {
232
+ const sum = /** @type {import('../node.js').Sum} */ (opaque[0].node);
89
233
  const distributed = sum.terms.map((t) => ({
90
234
  sign: t.sign,
91
235
  node: simplify(
@@ -98,21 +242,8 @@ function simplifyProduct(product, simplify) {
98
242
  return mkSum(distributed);
99
243
  }
100
244
 
101
- if (
102
- remainingDims.length === 1 &&
103
- remainingDims[0].exponent === 1 &&
104
- opaque.length === 0
105
- ) {
106
- const d = remainingDims[0];
107
- let value = 1;
108
- for (const f of scalarChain) {
109
- if (f.exponent === 1) {
110
- value = value * f.value;
111
- } else {
112
- value = value / f.value;
113
- }
114
- }
115
- return dim(value, d.unit, d.rawUnit);
245
+ if (loneDim !== null) {
246
+ return dim(value, loneDim.unit, loneDim.rawUnit);
116
247
  }
117
248
 
118
249
  if (remainingDims.length === 0 && opaque.length === 0) {
@@ -135,4 +266,88 @@ function simplifyProduct(product, simplify) {
135
266
  return mkProduct(factors);
136
267
  }
137
268
 
269
+ /**
270
+ * A substitution function (`var()`, `env()`, `attr()`, ...) is replaced by
271
+ * tokens, not by a value, so factors cannot move or cancel across it. Any
272
+ * function the parser does not recognise (`anchor()`, `foo()`, ...) is also an
273
+ * OpaqueCall and is treated the same, conservatively. A grouped Product is a
274
+ * parenthesized substitution and is equally opaque.
275
+ * @param {Node} node
276
+ * @return {boolean}
277
+ */
278
+ function isBarrier(node) {
279
+ return (
280
+ node.type === 'OpaqueCall' ||
281
+ (node.type === 'Product' && node.grouped === true)
282
+ );
283
+ }
284
+
285
+ /**
286
+ * Flatten a simplified factor through nested ungrouped Products and the
287
+ * canonical negation form, so barriers are visible at the top level.
288
+ * @param {ProductFactor[]} out
289
+ * @param {1 | -1} exponent
290
+ * @param {Node} node
291
+ * @return {boolean} Whether a barrier was pushed.
292
+ */
293
+ function flattenFactor(out, exponent, node) {
294
+ if (node.type === 'Product' && !node.grouped) {
295
+ let barrier = false;
296
+ for (const inner of node.factors) {
297
+ if (
298
+ flattenFactor(
299
+ out,
300
+ /** @type {1 | -1} */ (exponent * inner.exponent),
301
+ inner.node
302
+ )
303
+ )
304
+ barrier = true;
305
+ }
306
+ return barrier;
307
+ }
308
+ if (node.type === 'Sum' && node.terms.length === 1) {
309
+ out.push({ exponent, node: num(-1) });
310
+ return flattenFactor(out, exponent, node.terms[0].node);
311
+ }
312
+ out.push({ exponent, node });
313
+ return isBarrier(node);
314
+ }
315
+
316
+ /**
317
+ * @param {Product} product
318
+ * @param {SimplifyFn} simplify
319
+ * @param {number | false} [precision] A quotient that is not exact at this
320
+ * precision stays symbolic (`100% / 3`) instead of being rounded.
321
+ * @return {Node}
322
+ */
323
+ function simplifyProduct(product, simplify, precision = false) {
324
+ /** @type {ProductFactor[]} */
325
+ const items = [];
326
+ let hasBarrier = false;
327
+ for (const f of product.factors) {
328
+ if (flattenFactor(items, f.exponent, simplify(f.node))) hasBarrier = true;
329
+ }
330
+ if (!hasBarrier)
331
+ return foldFactors(items, 0, items.length, simplify, precision);
332
+
333
+ // Fold each run between barriers on its own; never move or cancel a factor
334
+ // across a barrier, and keep each barrier's own operator.
335
+ /** @type {ProductFactor[]} */
336
+ const factors = [];
337
+ let start = 0;
338
+ for (let i = 0; i <= items.length; i++) {
339
+ if (i < items.length && !isBarrier(items[i].node)) continue;
340
+ if (i - start === 1) factors.push(items[start]);
341
+ else if (i - start > 1)
342
+ factors.push({
343
+ exponent: 1,
344
+ node: foldFactors(items, start, i, simplify, precision),
345
+ });
346
+ if (i < items.length) factors.push(items[i]);
347
+ start = i + 1;
348
+ }
349
+ const result = mkProduct(factors);
350
+ return product.grouped === true ? mkGroup(result) : result;
351
+ }
352
+
138
353
  export { simplifyProduct };
@@ -26,9 +26,10 @@ function denoise(total, scale) {
26
26
  /**
27
27
  * @param {Sum} sum
28
28
  * @param {SimplifyFn} simplify
29
+ * @param {number | false} [precision]
29
30
  * @return {Node}
30
31
  */
31
- function simplifySum(sum, simplify) {
32
+ function simplifySum(sum, simplify, precision = false) {
32
33
  // §10.10 two-phase dim handling: phase 1 buckets by exact unit (`1em + 1em`
33
34
  // → `2em`); phase 2 merges convertible same-base buckets into the first-
34
35
  // encountered unit. `100vh - 5rem - 10rem - 100px` → `-15rem` in phase 1,
@@ -106,7 +107,10 @@ function simplifySum(sum, simplify) {
106
107
  const terms = hasNum
107
108
  ? [{ sign: /** @type {1} */ (1), node: num(denoise(numTotal, numScale)) }]
108
109
  : [];
109
- for (const bucket of mergeConvertibleBuckets([...byUnit.values()])) {
110
+ for (const bucket of mergeConvertibleBuckets(
111
+ [...byUnit.values()],
112
+ precision
113
+ )) {
110
114
  terms.push({
111
115
  sign: 1,
112
116
  node: dim(
@@ -6,7 +6,8 @@ import { simplifySum } from './simplify/sum.js';
6
6
  import { simplifyProduct } from './simplify/product.js';
7
7
  import { simplifyCall } from './simplify/call.js';
8
8
  import { simplifyComponents } from './opaque.js';
9
- import { opaqueCall } from './node.js';
9
+ import { groupSubstitution, opaqueCall } from './node.js';
10
+ import { isCalculationFunction } from './functions.js';
10
11
  import { assertDepth } from './limits.js';
11
12
 
12
13
  /**
@@ -17,17 +18,33 @@ import { assertDepth } from './limits.js';
17
18
  * @typedef {(node: Node) => Node} SimplifyFn
18
19
  */
19
20
 
21
+ /**
22
+ * A nested calc() is a parenthesized group: with unresolved tokens inside,
23
+ * its operands must not merge into the surrounding sum or product.
24
+ * @param {Node} value
25
+ * @param {(value: Node) => Node} child
26
+ * @return {Node}
27
+ */
28
+ function operand(value, child) {
29
+ const result = child(value);
30
+ return value.type === 'Call' && isCalculationFunction(value.name)
31
+ ? groupSubstitution(result)
32
+ : result;
33
+ }
34
+
20
35
  /**
21
36
  * Simplify is an independent, composable AST transformation. It may
22
37
  * synthesize canonical nodes while preserving the Node -> Node contract.
23
38
  * @param {Node} node
39
+ * @param {number | false} [precision] When set, divisions and unit
40
+ * conversions that are not exact at this precision stay symbolic.
24
41
  * @param {number} [depth]
25
42
  * @return {Node}
26
43
  */
27
- function simplify(node, depth = 0) {
44
+ function simplify(node, precision = false, depth = 0) {
28
45
  assertDepth(depth);
29
46
  /** @param {Node} value */
30
- const child = (value) => simplify(value, depth + 1);
47
+ const child = (value) => simplify(value, precision, depth + 1);
31
48
  switch (node.type) {
32
49
  case 'Num':
33
50
  case 'Dim':
@@ -42,9 +59,9 @@ function simplify(node, depth = 0) {
42
59
  node.rawName
43
60
  );
44
61
  case 'Sum':
45
- return simplifySum(node, child);
62
+ return simplifySum(node, (value) => operand(value, child), precision);
46
63
  case 'Product':
47
- return simplifyProduct(node, child);
64
+ return simplifyProduct(node, (value) => operand(value, child), precision);
48
65
  }
49
66
  }
50
67
 
package/src/reduce.js CHANGED
@@ -61,11 +61,12 @@ function reduceCalc(value, opts) {
61
61
 
62
62
  /** @type {ResolvedReduceCalcOptions} */
63
63
  const options = {
64
- precision: 5,
65
64
  warnWhenCannotResolve: false,
66
65
  unwrapSingleNegativeNumber: false,
67
66
  unwrapSingleValue: false,
68
67
  ...opts,
68
+ // An explicit `precision: undefined` keeps the default.
69
+ precision: opts?.precision ?? 5,
69
70
  };
70
71
  /** @type {import('@csstools/css-tokenizer').CSSToken[]} */
71
72
  let tokens;
@@ -42,6 +42,7 @@ export type ProductFactor = {
42
42
  export type Product = {
43
43
  type: 'Product';
44
44
  factors: ProductFactor[];
45
+ grouped?: boolean;
45
46
  };
46
47
  export type Node = Num | Dim | Ident | Call | OpaqueCall | Sum | Product;
47
48
  /**
@@ -54,7 +55,7 @@ export type Node = Num | Dim | Ident | Call | OpaqueCall | Sum | Product;
54
55
  * @typedef {{sign: 1 | -1, node: Node}} SumTerm Sign is always +1 when node is Num or Dim.
55
56
  * @typedef {{type: 'Sum', terms: SumTerm[], grouped?: boolean}} Sum
56
57
  * @typedef {{exponent: 1 | -1, node: Node}} ProductFactor exponent +1 = numerator, -1 = denominator.
57
- * @typedef {{type: 'Product', factors: ProductFactor[]}} Product
58
+ * @typedef {{type: 'Product', factors: ProductFactor[], grouped?: boolean}} Product
58
59
  * @typedef {Num | Dim | Ident | Call | OpaqueCall | Sum | Product} Node
59
60
  */
60
61
  /**
@@ -99,10 +100,24 @@ declare function mkSum(rawTerms: SumTerm[]): Node;
99
100
  * @return {Node}
100
101
  */
101
102
  declare function mkProduct(rawFactors: ProductFactor[]): Node;
103
+ /**
104
+ * Mark a node as a source parenthesized group. A Sum or Product keeps its
105
+ * structure; any other node becomes a single-factor grouped Product.
106
+ * @param {Node} node
107
+ * @return {Node}
108
+ */
109
+ declare function mkGroup(node: Node): Node;
110
+ /**
111
+ * Group a parenthesized expression when its tokens must stay bound: a sum, or
112
+ * a product around a substitution function.
113
+ * @param {Node} node
114
+ * @return {Node}
115
+ */
116
+ declare function groupSubstitution(node: Node): Node;
102
117
  /**
103
118
  * Negate any node, preserving canonical form.
104
119
  * @param {Node} node
105
120
  * @return {Node}
106
121
  */
107
122
  declare function negate(node: Node): Node;
108
- export { num, dim, ident, call, opaqueCall, mkSum, mkProduct, negate };
123
+ export { num, dim, ident, call, opaqueCall, mkSum, mkProduct, mkGroup, groupSubstitution, negate, };
@@ -0,0 +1,12 @@
1
+ export type CSSToken = import('@csstools/css-tokenizer').CSSToken;
2
+ export type Node = import('../node.js').Node;
3
+ export type OpaqueComponent = import('../node.js').OpaqueComponent;
4
+ export type ParseInput = import('./tokens.js').ParseInput;
5
+ export type Cursor = import('./tokens.js').Cursor;
6
+ export type FunctionToken = import('./tokens.js').FunctionToken;
7
+ export type ParseRange = (input: ParseInput, start: number, end: number) => Node;
8
+ /** @param {ParseInput} input @param {Cursor} cursor @param {FunctionToken} token @param {string} name @param {string} rawName @param {ParseRange} parseRange @return {Node} */
9
+ declare function parseOpaqueCall(input: ParseInput, cursor: Cursor, token: FunctionToken, name: string, rawName: string, parseRange: ParseRange): Node;
10
+ /** @param {ParseInput} input @param {Cursor} cursor @param {FunctionToken} token @param {string} name @param {string} rawName @param {ParseRange} parseRange @return {Node} */
11
+ declare function parseVar(input: ParseInput, cursor: Cursor, token: FunctionToken, name: string, rawName: string, parseRange: ParseRange): Node;
12
+ export { parseOpaqueCall, parseVar };