postcss-calc 11.2.2 → 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.
@@ -1,62 +1,37 @@
1
1
  // Spec: https://www.w3.org/TR/css-values-4/#serialize-a-calculation-tree
2
- // Canonical AST expression emission: operator precedence, parenthesization,
3
- // sum terms, product factors, and function calls.
2
+ // Canonical AST expression emission: node dispatch, sums, products, function
3
+ // calls, and math results. These emitters recurse into each other, so they
4
+ // share one module; precision and precedence rules live in sibling modules.
4
5
 
5
- import { serializeComponents } from '../opaque.js';
6
6
  import { num } from '../node.js';
7
+ import { serializeComponents } from '../opaque.js';
7
8
  import {
8
- round,
9
- isDegenerate,
10
9
  degenerateKeyword,
11
- roundedScalarValue,
10
+ emitBareSignedZero,
12
11
  emitRoundedScalar,
13
12
  emitScalar,
14
13
  emitSignedZero,
14
+ isDegenerate,
15
15
  isScalar,
16
16
  isSignedZero,
17
- isEffectivelyNegative,
17
+ round,
18
+ roundedScalarValue,
18
19
  } from './precision.js';
20
+ import {
21
+ needsParentheses,
22
+ PRODUCT_PRECEDENCE,
23
+ SUM_PRECEDENCE,
24
+ UNARY_PRECEDENCE,
25
+ } from './precedence.js';
19
26
 
20
27
  /**
21
28
  * @typedef {import('../node.js').Node} Node
22
29
  * @typedef {import('../node.js').Sum} Sum
23
- * @typedef {import('../node.js').Product} Product
30
+ * @typedef {import('../node.js').SumTerm} SumTerm
24
31
  * @typedef {import('../node.js').ProductFactor} ProductFactor
25
32
  * @typedef {import('./precision.js').SerializeSession} SerializeSession
26
33
  */
27
34
 
28
- // The AST is canonical: sums and products are flat, except grouped nodes that
29
- // keep their parentheses, so these precedence levels cover every binary expression
30
- const SUM_PRECEDENCE = 1;
31
- const PRODUCT_PRECEDENCE = 2;
32
- const ATOMIC_PRECEDENCE = 3;
33
- // Negation (-1 * ...) binds more tightly than a sum but has the same atomic boundary
34
- // for deciding whether the operand needs parentheses.
35
- const UNARY_PRECEDENCE = ATOMIC_PRECEDENCE;
36
-
37
- /** @param {Node} node @return {number} */
38
- function precedence(node) {
39
- if (node.type === 'Sum') return SUM_PRECEDENCE;
40
- if (node.type === 'Product') return PRODUCT_PRECEDENCE;
41
- return ATOMIC_PRECEDENCE;
42
- }
43
-
44
- /**
45
- * @param {Node} node
46
- * @param {number} parentPrecedence
47
- * @param {boolean} groupedRequired
48
- * @return {boolean}
49
- */
50
- function needsParentheses(node, parentPrecedence, groupedRequired) {
51
- return (
52
- precedence(node) < parentPrecedence ||
53
- (node.type === 'Sum' &&
54
- node.grouped === true &&
55
- groupedRequired === true) ||
56
- (node.type === 'Product' && node.grouped === true && parentPrecedence > 0)
57
- );
58
- }
59
-
60
35
  /**
61
36
  * @param {Node} node
62
37
  * @param {SerializeSession} session
@@ -105,129 +80,88 @@ function emitNodeBody(node, session) {
105
80
  emitSum(node, session);
106
81
  return;
107
82
  case 'Product':
108
- emitProduct(node, session);
83
+ emitProductFactors(node.factors, session);
109
84
  return;
110
85
  }
111
86
  }
112
87
 
113
- /**
114
- * @param {import('../node.js').Call} node
115
- * @param {SerializeSession} session
116
- * @param {string} [callNameOverride]
117
- * @return {void}
118
- */
119
- function emitCall(node, session, callNameOverride) {
120
- const buffer = session.buffer;
121
- buffer.push(callNameOverride ?? node.rawName ?? node.name, '(');
122
- for (let i = 0; i < node.args.length; i++) {
123
- if (i > 0) buffer.push(', ');
124
- emitNode(node.args[i], session);
125
- }
126
- buffer.push(')');
127
- }
128
-
129
- /**
130
- * @param {import('../node.js').OpaqueCall} node
131
- * @param {SerializeSession} session
132
- * @param {string} [callNameOverride]
133
- * @return {void}
134
- */
135
- function emitOpaqueCall(node, session, callNameOverride) {
136
- const buffer = session.buffer;
137
- buffer.push(callNameOverride ?? node.rawName ?? node.name, '(');
138
- serializeComponents(node.components, buffer, (child, childBuffer) => {
139
- emitNestedMathResult(child, session, childBuffer);
140
- });
141
- buffer.push(')');
142
- }
143
-
144
- /**
145
- * @param {import('../node.js').SumTerm} term
146
- * @param {1 | -1} multiplier
147
- * @param {number | false} precision
148
- * @return {1 | -1}
149
- */
150
- function termSign(term, multiplier, precision) {
151
- let sign = /** @type {1 | -1} */ (term.sign * multiplier);
152
- if (isEffectivelyNegative(term.node, precision)) {
153
- sign = /** @type {1 | -1} */ (-sign);
154
- }
155
- return sign;
156
- }
157
-
158
- /**
159
- * @param {import('../node.js').SumTerm} term
160
- * @param {SerializeSession} session
161
- * @param {1 | -1} sign
162
- * @return {void}
163
- */
164
- function emitSumTerm(term, session, sign) {
165
- if (sign === 1) {
166
- emitNode(term.node, session, SUM_PRECEDENCE, true);
167
- } else {
168
- emitLeadingNeg(term.node, session);
169
- }
170
- }
171
-
172
88
  /**
173
89
  * @param {import('../node.js').Num | import('../node.js').Dim} termNode
174
90
  * @param {1 | -1} sign
175
- * @param {number} i
91
+ * @param {boolean} leading
176
92
  * @param {SerializeSession} session
177
93
  * @return {void}
178
94
  */
179
- function emitScalarSumTerm(termNode, sign, i, session) {
95
+ function emitScalarSumTerm(termNode, sign, leading, session) {
180
96
  const buffer = session.buffer;
181
97
  const effectiveVal = sign * termNode.value;
182
98
  if (Object.is(effectiveVal, -0)) {
183
- if (i > 0) buffer.push(' + ');
184
- emitSignedZero(buffer, termNode);
99
+ // `x - 0` reparses as x + -(0), so subtraction round-trips negative zero.
100
+ // A leading term starts a <calc-product>, so `-1 * 0` needs no calc().
101
+ if (leading) {
102
+ emitBareSignedZero(buffer, termNode);
103
+ } else {
104
+ buffer.push(' - ');
105
+ emitRoundedScalar(termNode, buffer, 0);
106
+ }
185
107
  return;
186
108
  }
187
109
  if (isDegenerate(effectiveVal)) {
188
- if (i === 0 && sign === -1) buffer.push('-');
189
- else if (i > 0) buffer.push(sign === 1 ? ' + ' : ' - ');
110
+ if (!leading) buffer.push(sign === 1 ? ' + ' : ' - ');
111
+ else if (sign === -1) buffer.push('-');
190
112
  emitScalar(termNode, session);
191
113
  return;
192
114
  }
193
115
  const rounded = round(effectiveVal, session.precision);
194
116
  if (rounded < 0) {
195
- buffer.push(i === 0 ? '-' : ' - ');
117
+ buffer.push(leading ? '-' : ' - ');
196
118
  emitRoundedScalar(termNode, buffer, -rounded);
197
119
  } else {
198
- if (i > 0) buffer.push(' + ');
120
+ if (!leading) buffer.push(' + ');
199
121
  emitRoundedScalar(termNode, buffer, rounded);
200
122
  }
201
123
  }
202
124
 
203
125
  /**
204
- * @param {import('../node.js').SumTerm[]} terms
126
+ * @param {SumTerm} term
127
+ * @param {boolean} leading
205
128
  * @param {SerializeSession} session
206
- * @param {1 | -1} [multiplier]
207
129
  * @return {void}
208
130
  */
209
- function emitSumTerms(terms, session, multiplier = 1) {
210
- const buffer = session.buffer;
211
- for (let i = 0; i < terms.length; i++) {
212
- const term = terms[i];
213
- const termNode = term.node;
214
- const sign = /** @type {1 | -1} */ (term.sign * multiplier);
215
- if (isScalar(termNode)) {
216
- emitScalarSumTerm(termNode, sign, i, session);
217
- continue;
218
- }
219
- if (i === 0) {
220
- emitSumTerm(term, session, sign);
221
- } else {
222
- buffer.push(sign === 1 ? ' + ' : ' - ');
223
- emitNode(termNode, session, SUM_PRECEDENCE, true);
224
- }
131
+ function emitSumTerm(term, leading, session) {
132
+ const termNode = term.node;
133
+ if (isScalar(termNode)) {
134
+ emitScalarSumTerm(termNode, term.sign, leading, session);
135
+ } else if (leading && term.sign === -1) {
136
+ emitLeadingNeg(termNode, session);
137
+ } else {
138
+ if (!leading) session.buffer.push(term.sign === 1 ? ' + ' : ' - ');
139
+ emitNode(termNode, session, SUM_PRECEDENCE, true);
225
140
  }
226
141
  }
227
142
 
228
- /** @param {Sum} sum @param {SerializeSession} session @return {void} */
143
+ /** @param {SumTerm} term @return {boolean} */
144
+ function isNegativeZeroTerm(term) {
145
+ return isScalar(term.node) && Object.is(term.sign * term.node.value, -0);
146
+ }
147
+
148
+ /**
149
+ * @param {Sum} sum
150
+ * @param {SerializeSession} session
151
+ * @return {void}
152
+ */
229
153
  function emitSum(sum, session) {
230
- emitSumTerms(sum.terms, session);
154
+ const terms = sum.terms;
155
+ // x + -0 is exactly x for every x, so leading negative zeros can move
156
+ // behind the first other term and use the shorter subtraction form. When
157
+ // every term is negative zero, keep the order so reducing again is stable.
158
+ let lead = 0;
159
+ while (lead < terms.length && isNegativeZeroTerm(terms[lead])) lead++;
160
+ if (lead === terms.length) lead = 0;
161
+ emitSumTerm(terms[lead], true, session);
162
+ for (let i = 0; i < terms.length; i++) {
163
+ if (i !== lead) emitSumTerm(terms[i], false, session);
164
+ }
231
165
  }
232
166
 
233
167
  /**
@@ -269,34 +203,64 @@ function emitProductFactors(
269
203
  coefficientNode
270
204
  ) {
271
205
  const buffer = session.buffer;
272
- let first = true;
206
+ // Only the product's first multiplied scalar may print negative zero as a
207
+ // bare `-1 * 0`: reparsing folds the `-1` back into that same coefficient.
208
+ // After any operator, the `-1` would bind to the preceding operand instead.
209
+ let leading = true;
273
210
  if (coefficientValue !== undefined && coefficientValue !== 1) {
274
- emitScalar(
275
- /** @type {import('../node.js').Num} */ (coefficientNode),
276
- session,
277
- coefficientValue
211
+ const coefficient = /** @type {import('../node.js').Num} */ (
212
+ coefficientNode
278
213
  );
279
- first = false;
214
+ emitScalar(coefficient, session, coefficientValue, true);
215
+ leading = false;
280
216
  }
281
217
  for (let i = start; i < factors.length; i++) {
282
218
  const factor = factors[i];
283
219
  const factorNode = factor.node;
284
- if (first) {
285
- if (factor.exponent === -1) buffer.push('1 / ');
286
- if (isScalar(factorNode)) emitScalar(factorNode, session);
287
- else emitNode(factorNode, session, PRODUCT_PRECEDENCE);
288
- first = false;
220
+ const multiplied = factor.exponent === 1;
221
+ if (!leading) buffer.push(multiplied ? ' * ' : ' / ');
222
+ else if (!multiplied) buffer.push('1 / ');
223
+ if (isScalar(factorNode)) {
224
+ emitScalar(factorNode, session, undefined, leading && multiplied);
289
225
  } else {
290
- buffer.push(factor.exponent === 1 ? ' * ' : ' / ');
291
- if (isScalar(factorNode)) emitScalar(factorNode, session);
292
- else emitNode(factorNode, session, PRODUCT_PRECEDENCE);
226
+ emitNode(factorNode, session, PRODUCT_PRECEDENCE);
293
227
  }
228
+ leading = false;
229
+ }
230
+ }
231
+
232
+ /**
233
+ * @param {import('../node.js').Call} node
234
+ * @param {SerializeSession} session
235
+ * @param {string} [callNameOverride]
236
+ * @return {void}
237
+ */
238
+ function emitCall(node, session, callNameOverride) {
239
+ const buffer = session.buffer;
240
+ buffer.push(callNameOverride ?? node.rawName ?? node.name, '(');
241
+ for (let i = 0; i < node.args.length; i++) {
242
+ if (i > 0) buffer.push(', ');
243
+ const arg = node.args[i];
244
+ // Every math-function argument is a <calc-sum>, so it needs no calc()
245
+ if (isSignedZero(arg)) emitBareSignedZero(buffer, arg);
246
+ else emitNode(arg, session);
294
247
  }
248
+ buffer.push(')');
295
249
  }
296
250
 
297
- /** @param {Product} product @param {SerializeSession} session @return {void} */
298
- function emitProduct(product, session) {
299
- emitProductFactors(product.factors, session);
251
+ /**
252
+ * @param {import('../node.js').OpaqueCall} node
253
+ * @param {SerializeSession} session
254
+ * @param {string} [callNameOverride]
255
+ * @return {void}
256
+ */
257
+ function emitOpaqueCall(node, session, callNameOverride) {
258
+ const buffer = session.buffer;
259
+ buffer.push(callNameOverride ?? node.rawName ?? node.name, '(');
260
+ serializeComponents(node.components, buffer, (child, childBuffer) => {
261
+ emitNestedMathResult(child, session, childBuffer);
262
+ });
263
+ buffer.push(')');
300
264
  }
301
265
 
302
266
  /**
@@ -356,21 +320,10 @@ function emitNestedMathResult(node, session, buffer = session.buffer) {
356
320
  }
357
321
 
358
322
  export {
359
- SUM_PRECEDENCE,
360
- PRODUCT_PRECEDENCE,
361
- ATOMIC_PRECEDENCE,
362
- UNARY_PRECEDENCE,
363
- precedence,
364
- needsParentheses,
365
323
  emitNode,
324
+ emitLeadingNeg,
366
325
  emitCall,
367
326
  emitOpaqueCall,
368
- termSign,
369
- emitSumTerms,
370
- emitSum,
371
- emitLeadingNeg,
372
- emitProductFactors,
373
- emitProduct,
374
327
  emitMathResult,
375
328
  emitNestedMathResult,
376
329
  };
@@ -0,0 +1,47 @@
1
+ // Spec: https://www.w3.org/TR/css-values-4/#serialize-a-calculation-tree
2
+ // Operator precedence and parenthesization rules for canonical AST emission.
3
+
4
+ /**
5
+ * @typedef {import('../node.js').Node} Node
6
+ */
7
+
8
+ // The AST is canonical: sums and products are flat, except grouped nodes that
9
+ // keep their parentheses, so these precedence levels cover every binary expression
10
+ const SUM_PRECEDENCE = 1;
11
+ const PRODUCT_PRECEDENCE = 2;
12
+ const ATOMIC_PRECEDENCE = 3;
13
+ // Negation (-1 * ...) binds more tightly than a sum but has the same atomic boundary
14
+ // for deciding whether the operand needs parentheses.
15
+ const UNARY_PRECEDENCE = ATOMIC_PRECEDENCE;
16
+
17
+ /** @param {Node} node @return {number} */
18
+ function precedence(node) {
19
+ if (node.type === 'Sum') return SUM_PRECEDENCE;
20
+ if (node.type === 'Product') return PRODUCT_PRECEDENCE;
21
+ return ATOMIC_PRECEDENCE;
22
+ }
23
+
24
+ /**
25
+ * @param {Node} node
26
+ * @param {number} parentPrecedence
27
+ * @param {boolean} groupedRequired
28
+ * @return {boolean}
29
+ */
30
+ function needsParentheses(node, parentPrecedence, groupedRequired) {
31
+ return (
32
+ precedence(node) < parentPrecedence ||
33
+ (node.type === 'Sum' &&
34
+ node.grouped === true &&
35
+ groupedRequired === true) ||
36
+ (node.type === 'Product' && node.grouped === true && parentPrecedence > 0)
37
+ );
38
+ }
39
+
40
+ export {
41
+ SUM_PRECEDENCE,
42
+ PRODUCT_PRECEDENCE,
43
+ ATOMIC_PRECEDENCE,
44
+ UNARY_PRECEDENCE,
45
+ precedence,
46
+ needsParentheses,
47
+ };
@@ -168,13 +168,16 @@ function emitFiniteScalar(node, session, value) {
168
168
  * @param {import('../node.js').Num | import('../node.js').Dim} node
169
169
  * @param {SerializeSession} session
170
170
  * @param {number} [value]
171
+ * @param {boolean} [bare] emit negative zero without its calc() wrapper
171
172
  * @return {void}
172
173
  */
173
- function emitScalar(node, session, value) {
174
+ function emitScalar(node, session, value, bare = false) {
174
175
  const buffer = session.buffer;
175
176
  const effective = value ?? node.value;
176
- if (Object.is(effective, -0)) emitSignedZero(buffer, node);
177
- else if (isDegenerate(effective)) {
177
+ if (Object.is(effective, -0)) {
178
+ if (bare) emitBareSignedZero(buffer, node);
179
+ else emitSignedZero(buffer, node);
180
+ } else if (isDegenerate(effective)) {
178
181
  if (node.type === 'Dim') {
179
182
  buffer.push(
180
183
  'calc(',
@@ -195,8 +198,21 @@ function emitScalar(node, session, value) {
195
198
  * @return {void}
196
199
  */
197
200
  function emitSignedZero(buffer, node) {
201
+ buffer.push('calc(');
202
+ emitBareSignedZero(buffer, node);
203
+ buffer.push(')');
204
+ }
205
+
206
+ /**
207
+ * Negative zero as an unwrapped product, valid only where a `<calc-product>`
208
+ * may appear without changing how the surrounding expression parses.
209
+ * @param {string[]} buffer
210
+ * @param {import('../node.js').Num | import('../node.js').Dim} node
211
+ * @return {void}
212
+ */
213
+ function emitBareSignedZero(buffer, node) {
198
214
  const unit = node.type === 'Dim' ? (node.rawUnit ?? node.unit) : '';
199
- buffer.push('calc(-1 * 0', unit, ')');
215
+ buffer.push('-1 * 0', unit);
200
216
  }
201
217
 
202
218
  /** @param {Node} node @return {node is import('../node.js').Num | import('../node.js').Dim} */
@@ -209,22 +225,6 @@ function isSignedZero(node) {
209
225
  return isScalar(node) ? Object.is(node.value, -0) : false;
210
226
  }
211
227
 
212
- /**
213
- * Whether a scalar node is strictly negative after precision rounding
214
- * (excluding signed zero and sub-precision values that round to zero).
215
- * @param {Node} node
216
- * @param {number | false} precision
217
- * @return {node is import('../node.js').Num | import('../node.js').Dim}
218
- */
219
- function isEffectivelyNegative(node, precision) {
220
- return (
221
- isScalar(node) &&
222
- !Object.is(node.value, -0) &&
223
- Number.isFinite(node.value) &&
224
- round(node.value, precision) < 0
225
- );
226
- }
227
-
228
228
  export {
229
229
  NOISE_FLOOR,
230
230
  divideByPowerOfTen,
@@ -238,7 +238,7 @@ export {
238
238
  emitFiniteScalar,
239
239
  emitScalar,
240
240
  emitSignedZero,
241
+ emitBareSignedZero,
241
242
  isScalar,
242
243
  isSignedZero,
243
- isEffectivelyNegative,
244
244
  };
@@ -13,10 +13,10 @@ import {
13
13
  } from './serialize/precision.js';
14
14
  import {
15
15
  emitCall,
16
+ emitLeadingNeg,
16
17
  emitOpaqueCall,
17
18
  emitMathResult,
18
19
  emitNode,
19
- emitLeadingNeg,
20
20
  } from './serialize/expression.js';
21
21
 
22
22
  /**
@@ -0,0 +1,55 @@
1
+ import { num } from '../node.js';
2
+
3
+ /**
4
+ * @typedef {import('../node.js').Node} Node
5
+ * @typedef {import('../node.js').ProductFactor} ProductFactor
6
+ */
7
+
8
+ /**
9
+ * A substitution function (`var()`, `env()`, `attr()`, ...) is replaced by
10
+ * tokens, not by a value, so factors cannot move or cancel across it. Any
11
+ * function the parser does not recognise (`anchor()`, `foo()`, ...) is also an
12
+ * OpaqueCall and is treated the same, conservatively. A grouped Product is a
13
+ * parenthesized substitution and is equally opaque.
14
+ * @param {Node} node
15
+ * @return {boolean}
16
+ */
17
+ function isBarrier(node) {
18
+ return (
19
+ node.type === 'OpaqueCall' ||
20
+ (node.type === 'Product' && node.grouped === true)
21
+ );
22
+ }
23
+
24
+ /**
25
+ * Flatten a simplified factor through nested ungrouped Products and the
26
+ * canonical negation form, so barriers are visible at the top level.
27
+ * @param {ProductFactor[]} out
28
+ * @param {1 | -1} exponent
29
+ * @param {Node} node
30
+ * @return {boolean} Whether a barrier was pushed.
31
+ */
32
+ function flattenFactor(out, exponent, node) {
33
+ if (node.type === 'Product' && !node.grouped) {
34
+ let barrier = false;
35
+ for (const inner of node.factors) {
36
+ if (
37
+ flattenFactor(
38
+ out,
39
+ /** @type {1 | -1} */ (exponent * inner.exponent),
40
+ inner.node
41
+ )
42
+ )
43
+ barrier = true;
44
+ }
45
+ return barrier;
46
+ }
47
+ if (node.type === 'Sum' && node.terms.length === 1) {
48
+ out.push({ exponent, node: num(-1) });
49
+ return flattenFactor(out, exponent, node.terms[0].node);
50
+ }
51
+ out.push({ exponent, node });
52
+ return isBarrier(node);
53
+ }
54
+
55
+ export { flattenFactor, isBarrier };
@@ -0,0 +1,141 @@
1
+ import { mkProduct, num, dim } from '../node.js';
2
+ import { isExact } from './exact.js';
3
+
4
+ /**
5
+ * @typedef {import('../node.js').Node} Node
6
+ * @typedef {import('../node.js').ProductFactor} ProductFactor
7
+ */
8
+
9
+ /**
10
+ * @param {number} a non-negative
11
+ * @param {number} b non-negative
12
+ * @return {number}
13
+ */
14
+ function gcd(a, b) {
15
+ let x = a;
16
+ let y = b;
17
+ while (y !== 0) {
18
+ [x, y] = [y, x % y];
19
+ }
20
+ return x || 1;
21
+ }
22
+
23
+ /**
24
+ * @param {{exponent: 1 | -1, value: number}[]} chain
25
+ * @return {number}
26
+ */
27
+ function chainValue(chain) {
28
+ let value = 1;
29
+ for (const f of chain) {
30
+ value = f.exponent === 1 ? value * f.value : value / f.value;
31
+ }
32
+ return value;
33
+ }
34
+
35
+ /**
36
+ * Whether the opaque factors are a single Sum of only Num/Dim terms.
37
+ * @param {ProductFactor[]} opaque
38
+ * @return {boolean}
39
+ */
40
+ function isScalarSum(opaque) {
41
+ return (
42
+ opaque.length === 1 &&
43
+ opaque[0].exponent === 1 &&
44
+ opaque[0].node.type === 'Sum' &&
45
+ opaque[0].node.terms.every(
46
+ (t) => t.node.type === 'Num' || t.node.type === 'Dim'
47
+ )
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Whether a folded quotient prints exactly. A quotient distributed over a
53
+ * scalar Sum is judged term by term, so `(3px + 6em) / 3` still folds.
54
+ * @param {number} value
55
+ * @param {ProductFactor[]} opaque
56
+ * @param {boolean} distributable
57
+ * @param {number | false} precision
58
+ * @return {boolean}
59
+ */
60
+ function foldsExactly(value, opaque, distributable, precision) {
61
+ if (!Number.isFinite(value) || isExact(value, precision)) {
62
+ return true;
63
+ }
64
+ if (!distributable) {
65
+ return false;
66
+ }
67
+ const sum = /** @type {import('../node.js').Sum} */ (opaque[0].node);
68
+ return sum.terms.every((t) =>
69
+ isExact(
70
+ value *
71
+ /** @type {import('../node.js').Num | import('../node.js').Dim} */ (
72
+ t.node
73
+ ).value,
74
+ precision
75
+ )
76
+ );
77
+ }
78
+
79
+ /**
80
+ * Emit an inexact quotient as `numerator * dims * opaque / denominator`, with
81
+ * integer parts reduced by their gcd. A lone Dim absorbs the numerator.
82
+ * @param {number} numerator
83
+ * @param {number} denominator
84
+ * @param {{value: number, unit: string, rawUnit?: string} | null} loneDim
85
+ * @param {{exponent: 1 | -1, value: number, unit: string, rawUnit?: string}[]} dims
86
+ * @param {ProductFactor[]} opaque
87
+ * @param {number | false} precision
88
+ * @return {Node | null} null when a part would be rounded on output
89
+ */
90
+ function rationalProduct(
91
+ numerator,
92
+ denominator,
93
+ loneDim,
94
+ dims,
95
+ opaque,
96
+ precision
97
+ ) {
98
+ // Drop float noise (`1.9999999999999993 * 100`) so the parts print as
99
+ // written and the gcd below can reduce them.
100
+ let n = Number(
101
+ (loneDim === null ? numerator : numerator * loneDim.value).toPrecision(15)
102
+ );
103
+ let d = Number(denominator.toPrecision(15));
104
+ if (!isExact(n, precision) || !isExact(d, precision)) {
105
+ return null;
106
+ }
107
+ if (Number.isSafeInteger(n) && Number.isSafeInteger(d)) {
108
+ const g = gcd(Math.abs(n), Math.abs(d));
109
+ n /= g;
110
+ d /= g;
111
+ }
112
+ if (d < 0) {
113
+ n = -n;
114
+ d = -d;
115
+ }
116
+ /** @type {ProductFactor[]} */
117
+ const factors = [];
118
+ if (loneDim !== null) {
119
+ factors.push({
120
+ exponent: 1,
121
+ node: dim(n, loneDim.unit, loneDim.rawUnit),
122
+ });
123
+ } else {
124
+ if (n !== 1) {
125
+ factors.push({ exponent: 1, node: num(n) });
126
+ }
127
+ for (const dm of dims) {
128
+ factors.push({
129
+ exponent: dm.exponent,
130
+ node: dim(dm.value, dm.unit, dm.rawUnit),
131
+ });
132
+ }
133
+ }
134
+ factors.push(...opaque);
135
+ if (d !== 1) {
136
+ factors.push({ exponent: -1, node: num(d) });
137
+ }
138
+ return mkProduct(factors);
139
+ }
140
+
141
+ export { chainValue, foldsExactly, isScalarSum, rationalProduct };