postcss-calc 11.2.1 → 11.2.3

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 (49) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +47 -35
  3. package/package.json +15 -15
  4. package/src/index.js +2 -1
  5. package/src/lib/analyze.js +0 -6
  6. package/src/lib/compile.js +1 -1
  7. package/src/lib/function-analyzers.js +119 -0
  8. package/src/lib/functions.js +25 -175
  9. package/src/lib/node.js +61 -4
  10. package/src/lib/parser/opaque.js +152 -0
  11. package/src/lib/parser/tokens.js +242 -0
  12. package/src/lib/parser.js +32 -377
  13. package/src/lib/serialize/expression.js +329 -0
  14. package/src/lib/serialize/precedence.js +47 -0
  15. package/src/lib/serialize/precision.js +244 -0
  16. package/src/lib/serialize.js +24 -561
  17. package/src/lib/simplify/bucket.js +22 -5
  18. package/src/lib/simplify/cancel.js +35 -11
  19. package/src/lib/simplify/clamp.js +2 -2
  20. package/src/lib/simplify/exact.js +18 -0
  21. package/src/lib/simplify/fold.js +15 -1
  22. package/src/lib/simplify/min-max.js +2 -2
  23. package/src/lib/simplify/product-barrier.js +55 -0
  24. package/src/lib/simplify/product-quotient.js +141 -0
  25. package/src/lib/simplify/product.js +92 -48
  26. package/src/lib/simplify/sum.js +6 -2
  27. package/src/lib/simplify.js +22 -5
  28. package/src/lib/types.js +94 -0
  29. package/src/reduce.js +2 -1
  30. package/types/lib/function-analyzers.d.ts +27 -0
  31. package/types/lib/functions.d.ts +3 -28
  32. package/types/lib/node.d.ts +17 -2
  33. package/types/lib/parser/opaque.d.ts +12 -0
  34. package/types/lib/parser/tokens.d.ts +83 -0
  35. package/types/lib/parser.d.ts +4 -44
  36. package/types/lib/serialize/expression.d.ts +55 -0
  37. package/types/lib/serialize/precedence.d.ts +18 -0
  38. package/types/lib/serialize/precision.d.ts +102 -0
  39. package/types/lib/simplify/bucket.d.ts +4 -1
  40. package/types/lib/simplify/cancel.d.ts +11 -7
  41. package/types/lib/simplify/exact.d.ts +9 -0
  42. package/types/lib/simplify/fold.d.ts +14 -1
  43. package/types/lib/simplify/product-barrier.d.ts +26 -0
  44. package/types/lib/simplify/product-quotient.d.ts +48 -0
  45. package/types/lib/simplify/product.d.ts +3 -7
  46. package/types/lib/simplify/sum.d.ts +2 -1
  47. package/types/lib/simplify.d.ts +3 -8
  48. package/types/lib/types.d.ts +48 -0
  49. package/types/lib/calculation-type.d.ts +0 -18
@@ -2,10 +2,22 @@
2
2
  // Outer calc() is added when the top-level result contains an arithmetic
3
3
  // operator, or when a finite scalar needs context-sensitive CSS semantics.
4
4
 
5
- import { serializeComponents } from './opaque.js';
6
5
  import { checkCalculationDepth } from './limits.js';
7
6
  import { isCalculationFunction } from './functions.js';
8
- import { num } from './node.js';
7
+ import {
8
+ isScalar,
9
+ serializeNumber,
10
+ roundedScalarValue,
11
+ isDegenerate,
12
+ degenerateKeyword,
13
+ } from './serialize/precision.js';
14
+ import {
15
+ emitCall,
16
+ emitLeadingNeg,
17
+ emitOpaqueCall,
18
+ emitMathResult,
19
+ emitNode,
20
+ } from './serialize/expression.js';
9
21
 
10
22
  /**
11
23
  * @typedef {import('./node.js').Node} Node
@@ -19,127 +31,6 @@ import { num } from './node.js';
19
31
  * @property {boolean} [unwrapSingleValue] Serialize fully resolved finite scalar results without calculation syntax.
20
32
  */
21
33
 
22
- // The AST is canonical: sums and products are flat, so these precedence
23
- // levels cover every binary expression
24
- const SUM_PRECEDENCE = 1;
25
- const PRODUCT_PRECEDENCE = 2;
26
- const ATOMIC_PRECEDENCE = 3;
27
- // Negation (-1 * ...) binds more tightly than a sum but has the same atomic boundary
28
- // for deciding whether the operand needs parentheses.
29
- const UNARY_PRECEDENCE = ATOMIC_PRECEDENCE;
30
- const NOISE_FLOOR = 1e-12;
31
-
32
- /**
33
- * Divide a decimal digit string by 10^k, rounding half away from zero, and
34
- * return the resulting integer digit string. `digits` has no leading zeros.
35
- * @param {string} digits
36
- * @param {number} k
37
- * @return {string}
38
- */
39
- function divideByPowerOfTen(digits, k) {
40
- // 0x30/0x35/0x39 are the char codes of '0'/'5'/'9'.
41
- if (digits.length <= k) {
42
- return digits.length === k && digits.charCodeAt(0) >= 0x35 ? '1' : '0';
43
- }
44
- const cut = digits.length - k;
45
- if (digits.charCodeAt(cut) < 0x35) return digits.slice(0, cut);
46
- // Round up and propagate the carry through trailing nines.
47
- let index = cut - 1;
48
- while (index >= 0 && digits.charCodeAt(index) === 0x39) index--;
49
- if (index < 0) return `1${'0'.repeat(cut)}`;
50
- return `${digits.slice(0, index)}${String.fromCharCode(
51
- digits.charCodeAt(index) + 1
52
- )}${'0'.repeat(cut - index - 1)}`;
53
- }
54
-
55
- /**
56
- * Round the shortest decimal representation of a non-negative double to `p`
57
- * fractional digits, half away from zero.
58
- *
59
- * `Number(text + 'e' + p)` reads the exact intended decimal (so `1.005` at
60
- * precision 2 becomes `1.01`), but it is only exact while the shifted value
61
- * fits in `Number.MAX_SAFE_INTEGER`; beyond that the intermediate double
62
- * rounds and can move the rounding boundary (e.g. `312834450754803.44` at
63
- * precision 1 or 6 drifted to `312834450754803.5`). Round the decimal digits
64
- * directly instead.
65
- *
66
- * @param {number} abs
67
- * @param {number} p
68
- * @return {number}
69
- */
70
- function roundDecimal(abs, p) {
71
- const text = String(abs);
72
- const eIdx = text.indexOf('e');
73
- const mantissa = eIdx === -1 ? text : text.slice(0, eIdx);
74
- let exponent = eIdx === -1 ? 0 : Number(text.slice(eIdx + 1));
75
- const dot = mantissa.indexOf('.');
76
- let digits = mantissa;
77
- if (dot !== -1) {
78
- digits = mantissa.slice(0, dot) + mantissa.slice(dot + 1);
79
- exponent -= mantissa.length - dot - 1;
80
- }
81
-
82
- // value = digits * 10^exponent, so the shortest decimal has -exponent
83
- // fractional digits when it is smaller than 1.
84
- if (exponent >= -p) return abs;
85
-
86
- let start = 0;
87
- while (start < digits.length - 1 && digits.charCodeAt(start) === 0x30)
88
- start++;
89
- const rounded = divideByPowerOfTen(digits.slice(start), -(exponent + p));
90
- return Number(`${rounded}e-${p}`);
91
- }
92
-
93
- /**
94
- * @param {number} v
95
- * @param {number | false} prec
96
- * @return {number}
97
- */
98
- function round(v, prec) {
99
- if (prec === false || !Number.isFinite(v)) return v;
100
- if (Object.is(v, -0) || v === 0) return v;
101
- const abs = Math.abs(v);
102
- // Numbers >= MAX_SAFE_INTEGER (2^53 - 1) cannot represent fractional values, and
103
- // integers already have 0 fractional places. Bypassing them avoids float drift.
104
- if (abs >= Number.MAX_SAFE_INTEGER || Number.isInteger(v)) return v;
105
-
106
- // Clamp precision to [0, 100] integer to prevent NaN from fractional precisions
107
- // or exponent overflows into Infinity/NaN (e.g. exponent + prec > 308).
108
- const p = Math.min(100, Math.max(0, Math.trunc(prec)));
109
- const sign = v < 0 ? -1 : 1;
110
- // Fast path: rounding to integer with "round half away from zero".
111
- const rounded =
112
- p === 0 ? sign * Math.round(abs) : sign * roundDecimal(abs, p);
113
-
114
- // Preserve non-zero values smaller than precision (e.g. 1/1000000) from collapsing
115
- // to zero, while still snapping true floating-point dust (< 1e-12) to zero.
116
- if (rounded === 0 && abs > NOISE_FLOOR) {
117
- return Number(v.toPrecision(Math.max(p, 1)));
118
- }
119
- return rounded;
120
- }
121
-
122
- // §10.13 / §10.7.2: Infinity/NaN serialize as canonical keywords.
123
- /** @param {number} v @return {boolean} */
124
- function isDegenerate(v) {
125
- return !Number.isFinite(v) || Number.isNaN(v);
126
- }
127
-
128
- /** @param {number} v @return {string} */
129
- function degenerateKeyword(v) {
130
- if (Number.isNaN(v)) return 'NaN';
131
- return v > 0 ? 'infinity' : '-infinity';
132
- }
133
-
134
- /** @param {number} v @return {string} */
135
- function serializeNumber(v) {
136
- if (Object.is(v, -0)) return '0';
137
- const text = String(v);
138
- if (text.startsWith('0.')) return text.slice(1);
139
- if (text.startsWith('-0.')) return `-${text.slice(2)}`;
140
- return text;
141
- }
142
-
143
34
  /** @param {SerializeOptions} opts @return {'standard' | 'unwrap-all'} */
144
35
  function normalizeScalarPolicy(opts) {
145
36
  return opts.unwrapSingleValue || opts.unwrapSingleNegativeNumber
@@ -147,429 +38,6 @@ function normalizeScalarPolicy(opts) {
147
38
  : 'standard';
148
39
  }
149
40
 
150
- /**
151
- * @param {import('./node.js').Num | import('./node.js').Dim} node
152
- * @param {number | false} precision
153
- * @param {number} [value]
154
- * @return {number}
155
- */
156
- function roundedScalarValue(node, precision, value) {
157
- return round(value ?? node.value, precision);
158
- }
159
-
160
- /**
161
- * @param {import('./node.js').Num | import('./node.js').Dim} node
162
- * @param {string[]} buffer
163
- * @param {number} value
164
- * @return {void}
165
- */
166
- function emitRoundedScalar(node, buffer, value) {
167
- buffer.push(serializeNumber(value));
168
- if (node.type === 'Dim') {
169
- buffer.push(node.rawUnit ?? node.unit);
170
- }
171
- }
172
-
173
- /**
174
- * @param {import('./node.js').Num | import('./node.js').Dim} node
175
- * @param {ReturnType<typeof makeContext>} session
176
- * @param {number} [value]
177
- * @return {number}
178
- */
179
- function emitFiniteScalar(node, session, value) {
180
- const rounded = roundedScalarValue(node, session.precision, value);
181
- emitRoundedScalar(node, session.buffer, rounded);
182
- return rounded;
183
- }
184
-
185
- /**
186
- * @param {import('./node.js').Num | import('./node.js').Dim} node
187
- * @param {ReturnType<typeof makeContext>} session
188
- * @param {number} [value]
189
- * @return {void}
190
- */
191
- function emitScalar(node, session, value) {
192
- const buffer = session.buffer;
193
- const effective = value ?? node.value;
194
- if (Object.is(effective, -0)) emitSignedZero(buffer, node);
195
- else if (isDegenerate(effective)) {
196
- if (node.type === 'Dim') {
197
- buffer.push(
198
- 'calc(',
199
- degenerateKeyword(effective),
200
- ' * 1',
201
- node.rawUnit ?? node.unit,
202
- ')'
203
- );
204
- } else {
205
- buffer.push(degenerateKeyword(effective));
206
- }
207
- } else emitFiniteScalar(node, session, effective);
208
- }
209
-
210
- /**
211
- * @param {string[]} buffer
212
- * @param {import('./node.js').Num | import('./node.js').Dim} node
213
- * @return {void}
214
- */
215
- function emitSignedZero(buffer, node) {
216
- const unit = node.type === 'Dim' ? (node.rawUnit ?? node.unit) : '';
217
- buffer.push('calc(-1 * 0', unit, ')');
218
- }
219
-
220
- /** @param {Node} node @return {node is import('./node.js').Num | import('./node.js').Dim} */
221
- function isScalar(node) {
222
- return node.type === 'Num' || node.type === 'Dim';
223
- }
224
-
225
- /** @param {Node} node @return {node is import('./node.js').Num | import('./node.js').Dim} */
226
- function isSignedZero(node) {
227
- return isScalar(node) ? Object.is(node.value, -0) : false;
228
- }
229
-
230
- /** @param {Node} node @return {number} */
231
- function precedence(node) {
232
- if (node.type === 'Sum') return SUM_PRECEDENCE;
233
- if (node.type === 'Product') return PRODUCT_PRECEDENCE;
234
- return ATOMIC_PRECEDENCE;
235
- }
236
-
237
- /**
238
- * @param {Node} node
239
- * @param {number} parentPrecedence
240
- * @param {boolean} groupedRequired
241
- * @return {boolean}
242
- */
243
- function needsParentheses(node, parentPrecedence, groupedRequired) {
244
- return (
245
- precedence(node) < parentPrecedence ||
246
- (node.type === 'Sum' && node.grouped === true && groupedRequired === true)
247
- );
248
- }
249
-
250
- /**
251
- * @param {Node} node
252
- * @param {ReturnType<typeof makeContext>} session
253
- * @param {number} [parentPrecedence]
254
- * @param {boolean} [groupedRequired]
255
- * @return {void}
256
- */
257
- function emitNode(
258
- node,
259
- session,
260
- parentPrecedence = 0,
261
- groupedRequired = false
262
- ) {
263
- const parenthesized = needsParentheses(
264
- node,
265
- parentPrecedence,
266
- groupedRequired
267
- );
268
- if (parenthesized) session.buffer.push('(');
269
- emitNodeBody(node, session);
270
- if (parenthesized) session.buffer.push(')');
271
- }
272
-
273
- /**
274
- * @param {Node} node
275
- * @param {ReturnType<typeof makeContext>} session
276
- * @return {void}
277
- */
278
- function emitNodeBody(node, session) {
279
- const buffer = session.buffer;
280
- switch (node.type) {
281
- case 'Num':
282
- case 'Dim':
283
- emitScalar(node, session);
284
- return;
285
- case 'Ident':
286
- buffer.push(node.rawName ?? node.name);
287
- return;
288
- case 'Call':
289
- emitCall(node, session);
290
- return;
291
- case 'OpaqueCall':
292
- emitOpaqueCall(node, session);
293
- return;
294
- case 'Sum':
295
- emitSum(node, session);
296
- return;
297
- case 'Product':
298
- emitProduct(node, session);
299
- return;
300
- }
301
- }
302
-
303
- /**
304
- * @param {import('./node.js').Call} node
305
- * @param {ReturnType<typeof makeContext>} session
306
- * @param {string} [callNameOverride]
307
- * @return {void}
308
- */
309
- function emitCall(node, session, callNameOverride) {
310
- const buffer = session.buffer;
311
- buffer.push(callNameOverride ?? node.rawName ?? node.name, '(');
312
- for (let i = 0; i < node.args.length; i++) {
313
- if (i > 0) buffer.push(', ');
314
- emitNode(node.args[i], session);
315
- }
316
- buffer.push(')');
317
- }
318
-
319
- /**
320
- * @param {import('./node.js').OpaqueCall} node
321
- * @param {ReturnType<typeof makeContext>} session
322
- * @param {string} [callNameOverride]
323
- * @return {void}
324
- */
325
- function emitOpaqueCall(node, session, callNameOverride) {
326
- const buffer = session.buffer;
327
- buffer.push(callNameOverride ?? node.rawName ?? node.name, '(');
328
- serializeComponents(node.components, buffer, (child, childBuffer) => {
329
- emitNestedMathResult(child, session, childBuffer);
330
- });
331
- buffer.push(')');
332
- }
333
-
334
- /**
335
- * Whether a scalar node is strictly negative after precision rounding
336
- * (excluding signed zero and sub-precision values that round to zero).
337
- * @param {Node} node
338
- * @param {number | false} precision
339
- * @return {node is import('./node.js').Num | import('./node.js').Dim}
340
- */
341
- function isEffectivelyNegative(node, precision) {
342
- return (
343
- isScalar(node) &&
344
- !Object.is(node.value, -0) &&
345
- Number.isFinite(node.value) &&
346
- round(node.value, precision) < 0
347
- );
348
- }
349
-
350
- /**
351
- * @param {import('./node.js').SumTerm} term
352
- * @param {1 | -1} multiplier
353
- * @param {number | false} precision
354
- * @return {1 | -1}
355
- */
356
- function termSign(term, multiplier, precision) {
357
- let sign = /** @type {1 | -1} */ (term.sign * multiplier);
358
- if (isEffectivelyNegative(term.node, precision)) {
359
- sign = /** @type {1 | -1} */ (-sign);
360
- }
361
- return sign;
362
- }
363
-
364
- /**
365
- * @param {import('./node.js').SumTerm} term
366
- * @param {ReturnType<typeof makeContext>} session
367
- * @param {1 | -1} sign
368
- * @return {void}
369
- */
370
- function emitSumTerm(term, session, sign) {
371
- if (sign === 1) {
372
- emitNode(term.node, session, SUM_PRECEDENCE, true);
373
- } else {
374
- emitLeadingNeg(term.node, session);
375
- }
376
- }
377
-
378
- /**
379
- * @param {import('./node.js').SumTerm[]} terms
380
- * @param {ReturnType<typeof makeContext>} session
381
- * @param {1 | -1} [multiplier]
382
- * @return {void}
383
- */
384
- function emitSumTerms(terms, session, multiplier = 1) {
385
- const buffer = session.buffer;
386
- for (let i = 0; i < terms.length; i++) {
387
- const term = terms[i];
388
- const termNode = term.node;
389
- if (isScalar(termNode)) {
390
- const effectiveVal = term.sign * multiplier * termNode.value;
391
- if (Object.is(effectiveVal, -0)) {
392
- if (i > 0) buffer.push(' + ');
393
- emitSignedZero(buffer, termNode);
394
- } else if (isDegenerate(effectiveVal)) {
395
- const sign = /** @type {1 | -1} */ (term.sign * multiplier);
396
- if (i === 0) {
397
- if (sign === -1) buffer.push('-');
398
- emitScalar(termNode, session);
399
- } else {
400
- buffer.push(sign === 1 ? ' + ' : ' - ');
401
- emitScalar(termNode, session);
402
- }
403
- } else {
404
- const rounded = round(effectiveVal, session.precision);
405
- if (rounded < 0) {
406
- if (i === 0) buffer.push('-');
407
- else buffer.push(' - ');
408
- emitRoundedScalar(termNode, buffer, -rounded);
409
- } else {
410
- if (i > 0) buffer.push(' + ');
411
- emitRoundedScalar(termNode, buffer, rounded);
412
- }
413
- }
414
- continue;
415
- }
416
- const sign = /** @type {1 | -1} */ (term.sign * multiplier);
417
- if (i === 0) {
418
- emitSumTerm(term, session, sign);
419
- } else {
420
- buffer.push(sign === 1 ? ' + ' : ' - ');
421
- emitNode(termNode, session, SUM_PRECEDENCE, true);
422
- }
423
- }
424
- }
425
-
426
- /** @param {Sum} sum @param {ReturnType<typeof makeContext>} session @return {void} */
427
- function emitSum(sum, session) {
428
- emitSumTerms(sum.terms, session);
429
- }
430
-
431
- /**
432
- * @param {Node} node
433
- * @param {ReturnType<typeof makeContext>} session
434
- * @return {void}
435
- */
436
- function emitLeadingNeg(node, session) {
437
- if (node.type === 'Product') {
438
- if (
439
- node.factors.length > 0 &&
440
- node.factors[0].exponent === 1 &&
441
- node.factors[0].node.type === 'Num'
442
- ) {
443
- const head = node.factors[0].node;
444
- emitProductFactors(node.factors, session, 1, -head.value, head);
445
- return;
446
- }
447
- emitProductFactors(node.factors, session, 0, -1, num(-1));
448
- return;
449
- }
450
- session.buffer.push('-1 * ');
451
- emitNode(node, session, UNARY_PRECEDENCE, false);
452
- }
453
-
454
- /**
455
- * @param {ProductFactor[]} factors
456
- * @param {ReturnType<typeof makeContext>} session
457
- * @param {number} [start]
458
- * @param {number} [coefficientValue]
459
- * @param {import('./node.js').Num} [coefficientNode]
460
- * @return {void}
461
- */
462
- function emitProductFactors(
463
- factors,
464
- session,
465
- start = 0,
466
- coefficientValue,
467
- coefficientNode
468
- ) {
469
- const buffer = session.buffer;
470
- let first = true;
471
- if (coefficientValue !== undefined && coefficientValue !== 1) {
472
- emitScalar(
473
- /** @type {import('./node.js').Num} */ (coefficientNode),
474
- session,
475
- coefficientValue
476
- );
477
- first = false;
478
- }
479
- for (let i = start; i < factors.length; i++) {
480
- const factor = factors[i];
481
- const factorNode = factor.node;
482
- if (first) {
483
- if (factor.exponent === -1) buffer.push('1 / ');
484
- if (isScalar(factorNode)) emitScalar(factorNode, session);
485
- else emitNode(factorNode, session, PRODUCT_PRECEDENCE);
486
- first = false;
487
- } else {
488
- buffer.push(factor.exponent === 1 ? ' * ' : ' / ');
489
- if (isScalar(factorNode)) emitScalar(factorNode, session);
490
- else emitNode(factorNode, session, PRODUCT_PRECEDENCE);
491
- }
492
- }
493
- }
494
-
495
- /** @param {Product} product @param {ReturnType<typeof makeContext>} session @return {void} */
496
- function emitProduct(product, session) {
497
- emitProductFactors(product.factors, session);
498
- }
499
-
500
- /** @param {Node} node @param {ReturnType<typeof makeContext>} session @return {void} */
501
- function emitRootExpr(node, session) {
502
- if (
503
- node.type === 'Sum' &&
504
- node.grouped &&
505
- node.terms.length > 1 &&
506
- termSign(node.terms[0], 1, session.precision) === -1
507
- ) {
508
- session.buffer.push('-1 * (');
509
- emitSumTerms(node.terms, session, -1);
510
- session.buffer.push(')');
511
- return;
512
- }
513
- if (node.type === 'Sum' && node.terms.length === 1) {
514
- emitLeadingNeg(node.terms[0].node, session);
515
- return;
516
- }
517
- emitNode(node, session);
518
- }
519
-
520
- /**
521
- * @param {Node} node
522
- * @param {ReturnType<typeof makeContext>} session
523
- * @param {string} wrapper
524
- * @return {void}
525
- */
526
- function emitMathResult(node, session, wrapper) {
527
- if (isScalar(node)) {
528
- const scalarValue = roundedScalarValue(node, session.precision);
529
- const buffer = session.buffer;
530
- if (isDegenerate(scalarValue)) {
531
- buffer.push(wrapper, '(', degenerateKeyword(scalarValue));
532
- if (node.type === 'Dim') buffer.push(' * 1', node.rawUnit ?? node.unit);
533
- buffer.push(')');
534
- } else if (session.scalarPolicy === 'standard') {
535
- buffer.push(wrapper, '(');
536
- emitRoundedScalar(node, buffer, scalarValue);
537
- buffer.push(')');
538
- } else {
539
- emitRoundedScalar(node, buffer, scalarValue);
540
- }
541
- return;
542
- }
543
- if (
544
- node.type === 'Sum' &&
545
- node.grouped &&
546
- node.terms.length > 1 &&
547
- termSign(node.terms[0], 1, session.precision) === -1
548
- ) {
549
- session.buffer.push(wrapper, '(-1 * (');
550
- emitSumTerms(node.terms, session, -1);
551
- session.buffer.push('))');
552
- return;
553
- }
554
- if (
555
- node.type === 'Ident' ||
556
- node.type === 'Call' ||
557
- node.type === 'OpaqueCall'
558
- ) {
559
- emitNode(node, session);
560
- return;
561
- }
562
- if (node.type === 'Sum' && node.terms.length === 1) {
563
- session.buffer.push(wrapper, '(');
564
- emitLeadingNeg(node.terms[0].node, session);
565
- session.buffer.push(')');
566
- return;
567
- }
568
- session.buffer.push(wrapper, '(');
569
- emitNode(node, session);
570
- session.buffer.push(')');
571
- }
572
-
573
41
  /**
574
42
  * Serialize a scalar result without allocating a render context or buffer.
575
43
  * @param {import('./node.js').Num | import('./node.js').Dim} node
@@ -588,20 +56,6 @@ function serializeScalarResult(node, precision, scalarPolicy, wrapper) {
588
56
  return scalarPolicy === 'standard' ? `${wrapper}(${scalar})` : scalar;
589
57
  }
590
58
 
591
- /**
592
- * @param {Node} node
593
- * @param {ReturnType<typeof makeContext>} session
594
- * @param {string[]} [buffer]
595
- * @return {void}
596
- */
597
- function emitNestedMathResult(node, session, buffer = session.buffer) {
598
- if (isSignedZero(node)) {
599
- emitSignedZero(buffer, node);
600
- return;
601
- }
602
- emitMathResult(node, session, 'calc');
603
- }
604
-
605
59
  /**
606
60
  * @param {SerializeOptions} opts
607
61
  * @return {{buffer: string[], precision: number | false, scalarPolicy: 'standard' | 'unwrap-all'}}
@@ -667,10 +121,19 @@ function planSerializeResult(result, opts) {
667
121
  return { kind: 'math', node: result.tree, session, wrapper };
668
122
  }
669
123
 
124
+ /** @param {Node} node @param {ReturnType<typeof makeContext>} session @return {void} */
125
+ function emitRootExpr(node, session) {
126
+ if (node.type === 'Sum' && node.terms.length === 1) {
127
+ emitLeadingNeg(node.terms[0].node, session);
128
+ return;
129
+ }
130
+ emitNode(node, session);
131
+ }
132
+
670
133
  /**
671
134
  * @param {ReturnType<typeof planSerialize> | ReturnType<typeof planSerializeResult>} renderSpec
672
135
  * @return {string}
673
- * */
136
+ */
674
137
  function emitOutput(renderSpec) {
675
138
  if (renderSpec.kind === 'original') return renderSpec.text;
676
139
  const { session } = renderSpec;
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { convert } from '../convertUnits.js';
8
+ import { isExact } from './exact.js';
8
9
 
9
10
  /**
10
11
  * @typedef {object} UnitBucket
@@ -18,9 +19,12 @@ import { convert } from '../convertUnits.js';
18
19
  /** Mutates `buckets` in place — totals of survivor buckets accumulate the
19
20
  * converted values of merged neighbors. Caller must not reuse the input.
20
21
  * @param {UnitBucket[]} buckets
22
+ * @param {number | false} [precision] A conversion that is not exact at this
23
+ * precision is not merged. When only the reverse direction is exact, the
24
+ * survivor switches to the other bucket's unit.
21
25
  * @return {UnitBucket[]}
22
26
  */
23
- function mergeConvertibleBuckets(buckets) {
27
+ function mergeConvertibleBuckets(buckets, precision = false) {
24
28
  /** @type {Map<import('../convertUnits.js').BaseType, UnitBucket>} */ const representative =
25
29
  new Map();
26
30
  /** @type {UnitBucket[]} */ const out = [];
@@ -36,12 +40,25 @@ function mergeConvertibleBuckets(buckets) {
36
40
  continue;
37
41
  }
38
42
  const converted = convert(b.total, b.unit, first.unit);
39
- if (converted === null) {
40
- out.push(b);
43
+ if (converted !== null && isExact(converted, precision)) {
44
+ first.total += converted;
45
+ first.scale = Math.max(first.scale, Math.abs(converted));
46
+ continue;
47
+ }
48
+ const reversed = convert(first.total, first.unit, b.unit);
49
+ const reversedScale = convert(first.scale, first.unit, b.unit);
50
+ if (
51
+ reversed !== null &&
52
+ reversedScale !== null &&
53
+ isExact(reversed, precision)
54
+ ) {
55
+ first.unit = b.unit;
56
+ first.rawUnit = b.rawUnit;
57
+ first.total = reversed + b.total;
58
+ first.scale = Math.max(reversedScale, Math.abs(b.total));
41
59
  continue;
42
60
  }
43
- first.total += converted;
44
- first.scale = Math.max(first.scale, Math.abs(converted));
61
+ out.push(b);
45
62
  }
46
63
  return out;
47
64
  }