postcss-calc 11.1.2 → 11.2.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.
Files changed (41) hide show
  1. package/README.md +40 -18
  2. package/package.json +16 -12
  3. package/src/index.js +8 -5
  4. package/src/lib/analyze.js +228 -0
  5. package/src/lib/block-index.js +118 -0
  6. package/src/lib/calculation-type.js +25 -0
  7. package/src/lib/compile.js +80 -0
  8. package/src/lib/functions.js +364 -0
  9. package/src/lib/limits.js +49 -0
  10. package/src/lib/node.js +63 -21
  11. package/src/lib/opaque.js +12 -32
  12. package/src/lib/parser.js +383 -328
  13. package/src/lib/print.js +75 -0
  14. package/src/lib/regex.js +4 -0
  15. package/src/lib/scan.js +61 -0
  16. package/src/lib/serialize.js +574 -215
  17. package/src/lib/simplify/call.js +5 -98
  18. package/src/lib/simplify/round.js +4 -4
  19. package/src/lib/simplify/sum.js +14 -5
  20. package/src/lib/simplify.js +20 -5
  21. package/src/reduce.js +46 -115
  22. package/types/index.d.ts +4 -0
  23. package/types/lib/analyze.d.ts +18 -0
  24. package/types/lib/block-index.d.ts +35 -0
  25. package/types/lib/calculation-type.d.ts +18 -0
  26. package/types/lib/compile.d.ts +37 -0
  27. package/types/lib/functions.d.ts +40 -0
  28. package/types/lib/limits.d.ts +8 -0
  29. package/types/lib/node.d.ts +19 -3
  30. package/types/lib/opaque.d.ts +8 -14
  31. package/types/lib/parser.d.ts +42 -48
  32. package/types/lib/print.d.ts +16 -0
  33. package/types/lib/regex.d.ts +2 -0
  34. package/types/lib/scan.d.ts +35 -0
  35. package/types/lib/serialize.d.ts +19 -2
  36. package/types/lib/simplify/call.d.ts +4 -18
  37. package/types/lib/simplify/round.d.ts +3 -1
  38. package/types/lib/simplify.d.ts +5 -2
  39. package/types/reduce.d.ts +47 -6
  40. package/src/lib/tokenizer.js +0 -10
  41. package/types/lib/tokenizer.d.ts +0 -3
@@ -0,0 +1,364 @@
1
+ import { simplifyMinMax } from './simplify/min-max.js';
2
+ import { simplifyClamp } from './simplify/clamp.js';
3
+ import { simplifyAbs } from './simplify/abs.js';
4
+ import { simplifySign } from './simplify/sign.js';
5
+ import { simplifyModRem } from './simplify/mod-rem.js';
6
+ import { ROUND_STRATEGIES, simplifyRound } from './simplify/round.js';
7
+ import { simplifyTrig } from './simplify/trig.js';
8
+ import { simplifyInverseTrig } from './simplify/inverse-trig.js';
9
+ import { simplifyAtan2 } from './simplify/atan2.js';
10
+ import { simplifyPow } from './simplify/pow.js';
11
+ import { simplifySqrt } from './simplify/sqrt.js';
12
+ import { simplifyExp } from './simplify/exp.js';
13
+ import { simplifyLog } from './simplify/log.js';
14
+ import { simplifyHypot } from './simplify/hypot.js';
15
+
16
+ /** @typedef {import('./node.js').Node} Node */
17
+ /** @typedef {{kind: 'number'} | {kind: 'dimension', base: string | null} | {kind: 'unknown'} | {kind: 'failure'}} CalculationType */
18
+ /** @typedef {(name: string, args: Node[]) => Node} MathSimplifier */
19
+ /** @typedef {(args: CalculationType[], nodes: Node[]) => CalculationType} TypeAnalyzer */
20
+ /** @typedef {{analyze: TypeAnalyzer, simplify?: MathSimplifier, isKeyword?: (node: Node, index: number) => boolean, calculation?: boolean}} MathFunction */
21
+
22
+ /** @type {CalculationType} */ const numberType = { kind: 'number' };
23
+ /** @type {CalculationType} */ const unknownType = { kind: 'unknown' };
24
+ /** @type {CalculationType} */ const failureType = { kind: 'failure' };
25
+
26
+ /** @param {CalculationType} type @return {boolean} */
27
+ function isFailure(type) {
28
+ return type.kind === 'failure';
29
+ }
30
+
31
+ /** @param {CalculationType} a @param {CalculationType} b @return {CalculationType} */
32
+ function addTypes(a, b) {
33
+ if (isFailure(a) || isFailure(b)) return failureType;
34
+ if (a.kind === 'unknown' || b.kind === 'unknown') return unknownType;
35
+ if (a.kind === 'number' && b.kind === 'number') return numberType;
36
+ if (a.kind === 'dimension' && b.kind === 'dimension') {
37
+ if (a.base === null || b.base === null) return unknownType;
38
+ return a.base === b.base ? a : failureType;
39
+ }
40
+ return failureType;
41
+ }
42
+
43
+ /**
44
+ * Check an all-number function without assuming anything about an unresolved
45
+ * operand. A concrete dimension can never become a number, so it is still a
46
+ * definite error when another argument is opaque.
47
+ * @param {CalculationType[]} args
48
+ * @param {number} min
49
+ * @param {number} max
50
+ * @return {CalculationType}
51
+ */
52
+ function numberArguments(args, min, max) {
53
+ if (args.length < min || args.length > max) return failureType;
54
+ let hasUnknown = false;
55
+ for (const arg of args) {
56
+ if (arg.kind === 'dimension') return failureType;
57
+ hasUnknown = hasUnknown || arg.kind === 'unknown';
58
+ }
59
+ return hasUnknown ? unknownType : numberType;
60
+ }
61
+
62
+ /**
63
+ * Check values that must share a calculation type. Unknown operands remain
64
+ * unknown: they might resolve to the concrete type required by their peers.
65
+ * @param {CalculationType[]} args
66
+ * @param {number} min
67
+ * @param {number} max
68
+ * @return {CalculationType}
69
+ */
70
+ function matchingArguments(args, min, max) {
71
+ if (args.length < min || args.length > max) return failureType;
72
+ let result = args[0];
73
+ for (let index = 1; index < args.length; index++) {
74
+ result = addTypes(result, args[index]);
75
+ if (isFailure(result)) return failureType;
76
+ }
77
+ return result;
78
+ }
79
+
80
+ /** @param {CalculationType[]} args @return {CalculationType} */
81
+ function analyzeTrig(args) {
82
+ return args.length === 1 &&
83
+ (args[0].kind === 'unknown' ||
84
+ args[0].kind === 'number' ||
85
+ (args[0].kind === 'dimension' && args[0].base === 'angle'))
86
+ ? numberType
87
+ : failureType;
88
+ }
89
+
90
+ /** @param {CalculationType[]} args @return {CalculationType} */
91
+ function analyzeInverseTrig(args) {
92
+ if (args.length !== 1 || args[0].kind === 'dimension') {
93
+ return failureType;
94
+ }
95
+ return args[0].kind === 'unknown'
96
+ ? unknownType
97
+ : { kind: 'dimension', base: 'angle' };
98
+ }
99
+
100
+ /** @param {CalculationType[]} args @return {CalculationType} */
101
+ function analyzeIdentity(args) {
102
+ return args.length === 1 ? args[0] : failureType;
103
+ }
104
+
105
+ /** @param {CalculationType[]} args @return {CalculationType} */
106
+ function analyzeSign(args) {
107
+ return args.length === 1 ? numberType : failureType;
108
+ }
109
+
110
+ /** @param {CalculationType[]} args @param {Node[]} nodes @return {CalculationType} */
111
+ function analyzeRound(args, nodes) {
112
+ const strategy = nodes[0];
113
+ const hasStrategy =
114
+ strategy?.type === 'Ident' &&
115
+ ROUND_STRATEGIES.has(strategy.name.toLowerCase());
116
+ const start = hasStrategy ? 1 : 0;
117
+ const valueCount = args.length - start;
118
+ if (valueCount === 1) {
119
+ const value = args[start];
120
+ if (value.kind === 'dimension') return failureType;
121
+ if (value.kind === 'unknown') return unknownType;
122
+ return numberType;
123
+ }
124
+ if (valueCount !== 2) return failureType;
125
+ const type = addTypes(args[start], args[start + 1]);
126
+ return isFailure(type) ? failureType : type;
127
+ }
128
+
129
+ /** @param {CalculationType[]} args @return {CalculationType} */
130
+ function analyzeAtan2(args) {
131
+ const type = matchingArguments(args, 2, 2);
132
+ return isFailure(type) || type.kind === 'unknown'
133
+ ? type
134
+ : { kind: 'dimension', base: 'angle' };
135
+ }
136
+
137
+ /** @param {CalculationType[]} args @return {CalculationType} */
138
+ function analyzeCalc(args) {
139
+ return args.length === 1 ? args[0] : failureType;
140
+ }
141
+
142
+ /** @param {Node} node @return {boolean} */
143
+ function isRoundStrategy(node) {
144
+ return node.type === 'Ident' && ROUND_STRATEGIES.has(node.name.toLowerCase());
145
+ }
146
+
147
+ /** @param {Node} node @param {number} index @return {boolean} */
148
+ function isClampKeyword(node, index) {
149
+ return (
150
+ (index === 0 || index === 2) &&
151
+ node.type === 'Ident' &&
152
+ node.name.toLowerCase() === 'none'
153
+ );
154
+ }
155
+
156
+ /** @param {CalculationType[]} args @param {Node[]} nodes @return {CalculationType} */
157
+ function analyzeClamp(args, nodes) {
158
+ if (args.length !== 3) return failureType;
159
+ let valueCount = 0;
160
+ /** @type {CalculationType} */ let result = failureType;
161
+ for (let index = 0; index < args.length; index++) {
162
+ if (isClampKeyword(nodes[index], index)) continue;
163
+ result = valueCount === 0 ? args[index] : addTypes(result, args[index]);
164
+ valueCount++;
165
+ }
166
+ if (valueCount < 1 || isFailure(result)) return failureType;
167
+ return result;
168
+ }
169
+
170
+ const mathFunctions = new Map(
171
+ /** @type {[string, MathFunction][]} */ ([
172
+ ['calc', { analyze: analyzeCalc, calculation: true }],
173
+ ['-webkit-calc', { analyze: analyzeCalc, calculation: true }],
174
+ ['-moz-calc', { analyze: analyzeCalc, calculation: true }],
175
+ [
176
+ 'min',
177
+ {
178
+ analyze: (args) => matchingArguments(args, 1, Infinity),
179
+ simplify: simplifyMinMax,
180
+ },
181
+ ],
182
+ [
183
+ 'max',
184
+ {
185
+ analyze: (args) => matchingArguments(args, 1, Infinity),
186
+ simplify: simplifyMinMax,
187
+ },
188
+ ],
189
+ [
190
+ 'clamp',
191
+ {
192
+ analyze: analyzeClamp,
193
+ simplify: (_name, args) => simplifyClamp(args),
194
+ isKeyword: isClampKeyword,
195
+ },
196
+ ],
197
+ [
198
+ 'abs',
199
+ {
200
+ analyze: analyzeIdentity,
201
+ simplify: (_name, args) => simplifyAbs(args),
202
+ },
203
+ ],
204
+ [
205
+ 'sign',
206
+ {
207
+ analyze: analyzeSign,
208
+ simplify: (_name, args) => simplifySign(args),
209
+ },
210
+ ],
211
+ [
212
+ 'mod',
213
+ {
214
+ analyze: (args) => matchingArguments(args, 2, 2),
215
+ simplify: (_name, args) => simplifyModRem('mod', args),
216
+ },
217
+ ],
218
+ [
219
+ 'rem',
220
+ {
221
+ analyze: (args) => matchingArguments(args, 2, 2),
222
+ simplify: (_name, args) => simplifyModRem('rem', args),
223
+ },
224
+ ],
225
+ [
226
+ 'round',
227
+ {
228
+ analyze: analyzeRound,
229
+ simplify: (_name, args) => simplifyRound(args),
230
+ isKeyword: (node, index) => index === 0 && isRoundStrategy(node),
231
+ },
232
+ ],
233
+ [
234
+ 'sin',
235
+ {
236
+ analyze: analyzeTrig,
237
+ simplify: (_name, args) => simplifyTrig('sin', args),
238
+ },
239
+ ],
240
+ [
241
+ 'cos',
242
+ {
243
+ analyze: analyzeTrig,
244
+ simplify: (_name, args) => simplifyTrig('cos', args),
245
+ },
246
+ ],
247
+ [
248
+ 'tan',
249
+ {
250
+ analyze: analyzeTrig,
251
+ simplify: (_name, args) => simplifyTrig('tan', args),
252
+ },
253
+ ],
254
+ [
255
+ 'asin',
256
+ {
257
+ analyze: analyzeInverseTrig,
258
+ simplify: (_name, args) => simplifyInverseTrig('asin', args),
259
+ },
260
+ ],
261
+ [
262
+ 'acos',
263
+ {
264
+ analyze: analyzeInverseTrig,
265
+ simplify: (_name, args) => simplifyInverseTrig('acos', args),
266
+ },
267
+ ],
268
+ [
269
+ 'atan',
270
+ {
271
+ analyze: analyzeInverseTrig,
272
+ simplify: (_name, args) => simplifyInverseTrig('atan', args),
273
+ },
274
+ ],
275
+ [
276
+ 'atan2',
277
+ {
278
+ analyze: analyzeAtan2,
279
+ simplify: (_name, args) => simplifyAtan2(args),
280
+ },
281
+ ],
282
+ [
283
+ 'pow',
284
+ {
285
+ analyze: (args) => numberArguments(args, 2, 2),
286
+ simplify: (_name, args) => simplifyPow(args),
287
+ },
288
+ ],
289
+ [
290
+ 'sqrt',
291
+ {
292
+ analyze: (args) => numberArguments(args, 1, 1),
293
+ simplify: (_name, args) => simplifySqrt(args),
294
+ },
295
+ ],
296
+ [
297
+ 'hypot',
298
+ {
299
+ analyze: (args) => matchingArguments(args, 1, Infinity),
300
+ simplify: (_name, args) => simplifyHypot(args),
301
+ },
302
+ ],
303
+ [
304
+ 'log',
305
+ {
306
+ analyze: (args) => numberArguments(args, 1, 2),
307
+ simplify: (_name, args) => simplifyLog(args),
308
+ },
309
+ ],
310
+ [
311
+ 'exp',
312
+ {
313
+ analyze: (args) => numberArguments(args, 1, 1),
314
+ simplify: (_name, args) => simplifyExp(args),
315
+ },
316
+ ],
317
+ ])
318
+ );
319
+
320
+ /**
321
+ * @param {string} name
322
+ * @return {{normalizedName: string, definition: MathFunction} | undefined}
323
+ */
324
+ function lookupMathFunction(name) {
325
+ const normalizedName = name.toLowerCase();
326
+ const definition = mathFunctions.get(normalizedName);
327
+ return definition === undefined ? undefined : { normalizedName, definition };
328
+ }
329
+
330
+ const mathFunctionNames = [...mathFunctions.keys()];
331
+ const QUICK_MATH_TEST = new RegExp(
332
+ `(?:${mathFunctionNames.join('|')})\\(`,
333
+ 'i'
334
+ );
335
+
336
+ /** @param {string} name @return {boolean} */
337
+ function isCalculationFunction(name) {
338
+ return mathFunctions.get(name.toLowerCase())?.calculation === true;
339
+ }
340
+
341
+ /** @param {string} name @return {boolean} */
342
+ function isSupportedMathFunction(name) {
343
+ const normalizedName = name.toLowerCase();
344
+ const definition = mathFunctions.get(normalizedName);
345
+ return definition !== undefined && definition.calculation !== true;
346
+ }
347
+
348
+ /** @param {string} value @return {boolean} */
349
+ function hasPotentialMathFunction(value) {
350
+ return (
351
+ value.includes('(') && (QUICK_MATH_TEST.test(value) || value.includes('\\'))
352
+ );
353
+ }
354
+
355
+ export {
356
+ addTypes,
357
+ mathFunctions,
358
+ lookupMathFunction,
359
+ QUICK_MATH_TEST,
360
+ isFailure,
361
+ isCalculationFunction,
362
+ isSupportedMathFunction,
363
+ hasPotentialMathFunction,
364
+ };
@@ -0,0 +1,49 @@
1
+ /** @typedef {import('./node.js').Node} Node */
2
+
3
+ const MAX_CALCULATION_DEPTH = 1024;
4
+
5
+ /** @param {number} depth @return {void} */
6
+ function assertDepth(depth) {
7
+ if (depth > MAX_CALCULATION_DEPTH) {
8
+ throw new Error(
9
+ `Calculation nesting exceeds the limit of ${MAX_CALCULATION_DEPTH}`
10
+ );
11
+ }
12
+ }
13
+
14
+ /** @param {unknown} part @param {number} depth @return {void} */
15
+ function checkOpaquePartDepth(part, depth) {
16
+ assertDepth(depth);
17
+ if (typeof part === 'string') return;
18
+ if (Array.isArray(part)) {
19
+ for (const child of part) checkOpaquePartDepth(child, depth + 1);
20
+ return;
21
+ }
22
+ checkCalculationDepth(/** @type {Node} */ (part), depth + 1);
23
+ }
24
+
25
+ /** @param {Node} node @param {number} [depth] @return {void} */
26
+ function checkCalculationDepth(node, depth = 0) {
27
+ assertDepth(depth);
28
+ switch (node.type) {
29
+ case 'Sum':
30
+ for (const term of node.terms) {
31
+ checkCalculationDepth(term.node, depth + 1);
32
+ }
33
+ return;
34
+ case 'Product':
35
+ for (const factor of node.factors) {
36
+ checkCalculationDepth(factor.node, depth + 1);
37
+ }
38
+ return;
39
+ case 'Call':
40
+ for (const child of node.args) checkCalculationDepth(child, depth + 1);
41
+ return;
42
+ case 'OpaqueCall':
43
+ for (const part of node.components) {
44
+ checkOpaquePartDepth(part, depth + 1);
45
+ }
46
+ }
47
+ }
48
+
49
+ export { MAX_CALCULATION_DEPTH, assertDepth, checkCalculationDepth };
package/src/lib/node.js CHANGED
@@ -11,7 +11,11 @@
11
11
  // - No Product directly contains another Product (flattened).
12
12
  // - A Sum/Product with one positive element collapses to that element.
13
13
  // - A Sum/Product with no elements collapses to Num(0) / Num(1).
14
- // - Zero-valued Nums are dropped from sums (they contribute nothing).
14
+ // - Positive zero-valued Nums are dropped from all-number sums. They are
15
+ // retained in mixed or unresolved sums because they constrain the other
16
+ // terms to <number>. Negative zero is retained until calculation
17
+ // evaluation has finished, because it is an IEEE-754 value with observable
18
+ // math-function behavior.
15
19
  // Zero-valued Dims are kept — the unit carries type info.
16
20
 
17
21
  /**
@@ -19,11 +23,13 @@
19
23
  * @typedef {{type: 'Dim', value: number, unit: string, rawUnit?: string}} Dim
20
24
  * @typedef {{type: 'Ident', name: string, rawName?: string}} Ident
21
25
  * @typedef {{type: 'Call', name: string, args: Node[], rawName?: string}} Call
26
+ * @typedef {string | Node | OpaqueComponent[]} OpaqueComponent
27
+ * @typedef {{type: 'OpaqueCall', name: string, components: OpaqueComponent[], rawName?: string}} OpaqueCall
22
28
  * @typedef {{sign: 1 | -1, node: Node}} SumTerm Sign is always +1 when node is Num or Dim.
23
29
  * @typedef {{type: 'Sum', terms: SumTerm[], grouped?: boolean}} Sum
24
30
  * @typedef {{exponent: 1 | -1, node: Node}} ProductFactor exponent +1 = numerator, -1 = denominator.
25
31
  * @typedef {{type: 'Product', factors: ProductFactor[]}} Product
26
- * @typedef {Num | Dim | Ident | Call | Sum | Product} Node
32
+ * @typedef {Num | Dim | Ident | Call | OpaqueCall | Sum | Product} Node
27
33
  */
28
34
 
29
35
  /**
@@ -69,6 +75,18 @@ function call(name, args, rawName) {
69
75
  : { type: 'Call', name, args, rawName };
70
76
  }
71
77
 
78
+ /**
79
+ * @param {string} name
80
+ * @param {OpaqueComponent[]} components
81
+ * @param {string} [rawName]
82
+ * @return {OpaqueCall}
83
+ */
84
+ function opaqueCall(name, components, rawName) {
85
+ return rawName === undefined
86
+ ? { type: 'OpaqueCall', name, components }
87
+ : { type: 'OpaqueCall', name, components, rawName };
88
+ }
89
+
72
90
  /**
73
91
  * @param {SumTerm[]} rawTerms
74
92
  * @return {Node}
@@ -76,13 +94,32 @@ function call(name, args, rawName) {
76
94
  function mkSum(rawTerms) {
77
95
  /** @type {SumTerm[]} */
78
96
  const flat = [];
97
+ let hasNegativeZero = false;
79
98
  for (const t of rawTerms) {
80
- pushSumTerm(flat, t);
99
+ if (pushSumTerm(flat, t)) hasNegativeZero = true;
81
100
  }
82
- if (flat.length === 0) {
101
+ const zeroIsTypeAnchor = flat.some((term) => term.node.type !== 'Num');
102
+ // `+0 + -0` evaluates to +0. Keep positive zero terms when the sum also
103
+ // contains -0 so simplification can perform that IEEE-754 operation before
104
+ // the canonical zero-elision below.
105
+ let length = 0;
106
+ for (let i = 0; i < flat.length; i++) {
107
+ const term = flat[i];
108
+ if (
109
+ !hasNegativeZero &&
110
+ !zeroIsTypeAnchor &&
111
+ term.node.type === 'Num' &&
112
+ term.node.value === 0
113
+ ) {
114
+ continue;
115
+ }
116
+ flat[length++] = term;
117
+ }
118
+ flat.length = length;
119
+ if (length === 0) {
83
120
  return num(0);
84
121
  }
85
- if (flat.length === 1 && flat[0].sign === 1) {
122
+ if (length === 1 && flat[0].sign === 1) {
86
123
  return flat[0].node;
87
124
  }
88
125
  return { type: 'Sum', terms: flat };
@@ -91,19 +128,24 @@ function mkSum(rawTerms) {
91
128
  /**
92
129
  * @param {SumTerm[]} out
93
130
  * @param {SumTerm} term
94
- * @return {void}
131
+ * @return {boolean} Whether the appended terms contain negative zero.
95
132
  */
96
133
  function pushSumTerm(out, term) {
97
134
  let { sign, node } = term;
98
135
 
99
136
  if (node.type === 'Sum' && !node.grouped) {
137
+ let hasNegativeZero = false;
100
138
  for (const inner of node.terms) {
101
- pushSumTerm(out, {
102
- sign: /** @type {1 | -1} */ (sign * inner.sign),
103
- node: inner.node,
104
- });
139
+ if (
140
+ pushSumTerm(out, {
141
+ sign: /** @type {1 | -1} */ (sign * inner.sign),
142
+ node: inner.node,
143
+ })
144
+ ) {
145
+ hasNegativeZero = true;
146
+ }
105
147
  }
106
- return;
148
+ return hasNegativeZero;
107
149
  }
108
150
 
109
151
  // sign=-1 around a Num/Dim leaf collapses into the value's sign — the
@@ -113,12 +155,8 @@ function pushSumTerm(out, term) {
113
155
  sign = 1;
114
156
  }
115
157
 
116
- // Drop zero-valued Nums. Dims with value 0 stay — the unit carries type.
117
- if (node.type === 'Num' && node.value === 0) {
118
- return;
119
- }
120
-
121
158
  out.push({ sign, node });
159
+ return node.type === 'Num' && Object.is(node.value, -0);
122
160
  }
123
161
 
124
162
  /**
@@ -176,19 +214,23 @@ function negate(node) {
176
214
  return dim(-node.value, node.unit, node.rawUnit);
177
215
  }
178
216
  if (node.type === 'Sum') {
217
+ // A grouped sum may contain opaque terms whose meaning depends on the
218
+ // surrounding context. Keep the group intact so `-(a + b)` cannot turn
219
+ // into `-a - b` while it is still unresolved.
220
+ if (node.grouped) {
221
+ return mkSum([{ sign: -1, node }]);
222
+ }
179
223
  const result = mkSum(
180
224
  node.terms.map((t) => ({
181
225
  sign: /** @type {1 | -1} */ (-t.sign),
182
226
  node: t.node,
183
227
  }))
184
228
  );
185
- return node.grouped && result.type === 'Sum'
186
- ? { ...result, grouped: true }
187
- : result;
229
+ return result;
188
230
  }
189
- // Opaque (Ident, Call, Product): wrap as a single negative-sign term —
231
+ // Opaque (Ident, Call, OpaqueCall, Product): wrap as a single negative-sign term —
190
232
  // the only case where sign=-1 remains on a SumTerm.
191
233
  return mkSum([{ sign: -1, node }]);
192
234
  }
193
235
 
194
- export { num, dim, ident, call, mkSum, mkProduct, negate };
236
+ export { num, dim, ident, call, opaqueCall, mkSum, mkProduct, negate };
package/src/lib/opaque.js CHANGED
@@ -1,40 +1,20 @@
1
- // Private metadata for opaque function contents (including var() fallbacks).
2
- // Keeping it in a WeakMap means the public calculation AST remains unchanged.
3
1
  /** @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) => {
2
+ /** @typedef {import('./node.js').OpaqueComponent} OpaqueComponent */
3
+
4
+ /** @param {OpaqueComponent[]} components @param {(node: Node) => Node} simplify @return {OpaqueComponent[]} */
5
+ function simplifyComponents(components, simplify) {
6
+ return components.map((part) => {
19
7
  if (typeof part === 'string') return part;
20
8
  if (Array.isArray(part)) return simplifyComponents(part, simplify);
21
9
  return simplify(part);
22
10
  });
23
11
  }
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);
12
+ /** @param {OpaqueComponent[]} components @param {string[]} buffer @param {(node: Node, buffer: string[]) => void} serialize @return {void} */
13
+ function serializeComponents(components, buffer, serialize) {
14
+ for (const part of components) {
15
+ if (typeof part === 'string') buffer.push(part);
16
+ else if (Array.isArray(part)) serializeComponents(part, buffer, serialize);
17
+ else serialize(part, buffer);
32
18
  }
33
- return result;
34
19
  }
35
- export {
36
- getComponents,
37
- setComponents,
38
- simplifyComponents,
39
- serializeComponents,
40
- };
20
+ export { simplifyComponents, serializeComponents };