postcss-calc 11.2.0 → 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.
Files changed (40) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +44 -16
  3. package/package.json +13 -13
  4. package/src/index.js +2 -1
  5. package/src/lib/analyze.js +81 -43
  6. package/src/lib/compile.js +1 -1
  7. package/src/lib/functions.js +40 -10
  8. package/src/lib/node.js +62 -5
  9. package/src/lib/parser/opaque.js +151 -0
  10. package/src/lib/parser/tokens.js +242 -0
  11. package/src/lib/parser.js +27 -382
  12. package/src/lib/serialize/expression.js +376 -0
  13. package/src/lib/serialize/precision.js +244 -0
  14. package/src/lib/serialize.js +24 -517
  15. package/src/lib/simplify/bucket.js +22 -5
  16. package/src/lib/simplify/cancel.js +35 -11
  17. package/src/lib/simplify/clamp.js +2 -2
  18. package/src/lib/simplify/exact.js +18 -0
  19. package/src/lib/simplify/fold.js +15 -1
  20. package/src/lib/simplify/min-max.js +2 -2
  21. package/src/lib/simplify/product.js +263 -48
  22. package/src/lib/simplify/sum.js +6 -2
  23. package/src/lib/simplify.js +22 -5
  24. package/src/reduce.js +2 -1
  25. package/types/lib/analyze.d.ts +27 -0
  26. package/types/lib/functions.d.ts +19 -1
  27. package/types/lib/node.d.ts +17 -2
  28. package/types/lib/parser/opaque.d.ts +12 -0
  29. package/types/lib/parser/tokens.d.ts +83 -0
  30. package/types/lib/parser.d.ts +4 -44
  31. package/types/lib/serialize/expression.d.ts +95 -0
  32. package/types/lib/serialize/precision.d.ts +101 -0
  33. package/types/lib/simplify/bucket.d.ts +4 -1
  34. package/types/lib/simplify/cancel.d.ts +11 -7
  35. package/types/lib/simplify/exact.d.ts +9 -0
  36. package/types/lib/simplify/fold.d.ts +14 -1
  37. package/types/lib/simplify/product.d.ts +3 -7
  38. package/types/lib/simplify/sum.d.ts +2 -1
  39. package/types/lib/simplify.d.ts +3 -8
  40. package/src/lib/calculation-type.js +0 -25
@@ -0,0 +1,376 @@
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.
4
+
5
+ import { serializeComponents } from '../opaque.js';
6
+ import { num } from '../node.js';
7
+ import {
8
+ round,
9
+ isDegenerate,
10
+ degenerateKeyword,
11
+ roundedScalarValue,
12
+ emitRoundedScalar,
13
+ emitScalar,
14
+ emitSignedZero,
15
+ isScalar,
16
+ isSignedZero,
17
+ isEffectivelyNegative,
18
+ } from './precision.js';
19
+
20
+ /**
21
+ * @typedef {import('../node.js').Node} Node
22
+ * @typedef {import('../node.js').Sum} Sum
23
+ * @typedef {import('../node.js').Product} Product
24
+ * @typedef {import('../node.js').ProductFactor} ProductFactor
25
+ * @typedef {import('./precision.js').SerializeSession} SerializeSession
26
+ */
27
+
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
+ /**
61
+ * @param {Node} node
62
+ * @param {SerializeSession} session
63
+ * @param {number} [parentPrecedence]
64
+ * @param {boolean} [groupedRequired]
65
+ * @return {void}
66
+ */
67
+ function emitNode(
68
+ node,
69
+ session,
70
+ parentPrecedence = 0,
71
+ groupedRequired = false
72
+ ) {
73
+ const parenthesized = needsParentheses(
74
+ node,
75
+ parentPrecedence,
76
+ groupedRequired
77
+ );
78
+ if (parenthesized) session.buffer.push('(');
79
+ emitNodeBody(node, session);
80
+ if (parenthesized) session.buffer.push(')');
81
+ }
82
+
83
+ /**
84
+ * @param {Node} node
85
+ * @param {SerializeSession} session
86
+ * @return {void}
87
+ */
88
+ function emitNodeBody(node, session) {
89
+ const buffer = session.buffer;
90
+ switch (node.type) {
91
+ case 'Num':
92
+ case 'Dim':
93
+ emitScalar(node, session);
94
+ return;
95
+ case 'Ident':
96
+ buffer.push(node.rawName ?? node.name);
97
+ return;
98
+ case 'Call':
99
+ emitCall(node, session);
100
+ return;
101
+ case 'OpaqueCall':
102
+ emitOpaqueCall(node, session);
103
+ return;
104
+ case 'Sum':
105
+ emitSum(node, session);
106
+ return;
107
+ case 'Product':
108
+ emitProduct(node, session);
109
+ return;
110
+ }
111
+ }
112
+
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
+ /**
173
+ * @param {import('../node.js').Num | import('../node.js').Dim} termNode
174
+ * @param {1 | -1} sign
175
+ * @param {number} i
176
+ * @param {SerializeSession} session
177
+ * @return {void}
178
+ */
179
+ function emitScalarSumTerm(termNode, sign, i, session) {
180
+ const buffer = session.buffer;
181
+ const effectiveVal = sign * termNode.value;
182
+ if (Object.is(effectiveVal, -0)) {
183
+ if (i > 0) buffer.push(' + ');
184
+ emitSignedZero(buffer, termNode);
185
+ return;
186
+ }
187
+ if (isDegenerate(effectiveVal)) {
188
+ if (i === 0 && sign === -1) buffer.push('-');
189
+ else if (i > 0) buffer.push(sign === 1 ? ' + ' : ' - ');
190
+ emitScalar(termNode, session);
191
+ return;
192
+ }
193
+ const rounded = round(effectiveVal, session.precision);
194
+ if (rounded < 0) {
195
+ buffer.push(i === 0 ? '-' : ' - ');
196
+ emitRoundedScalar(termNode, buffer, -rounded);
197
+ } else {
198
+ if (i > 0) buffer.push(' + ');
199
+ emitRoundedScalar(termNode, buffer, rounded);
200
+ }
201
+ }
202
+
203
+ /**
204
+ * @param {import('../node.js').SumTerm[]} terms
205
+ * @param {SerializeSession} session
206
+ * @param {1 | -1} [multiplier]
207
+ * @return {void}
208
+ */
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
+ }
225
+ }
226
+ }
227
+
228
+ /** @param {Sum} sum @param {SerializeSession} session @return {void} */
229
+ function emitSum(sum, session) {
230
+ emitSumTerms(sum.terms, session);
231
+ }
232
+
233
+ /**
234
+ * @param {Node} node
235
+ * @param {SerializeSession} session
236
+ * @return {void}
237
+ */
238
+ function emitLeadingNeg(node, session) {
239
+ if (node.type === 'Product' && !node.grouped) {
240
+ if (
241
+ node.factors.length > 0 &&
242
+ node.factors[0].exponent === 1 &&
243
+ node.factors[0].node.type === 'Num'
244
+ ) {
245
+ const head = node.factors[0].node;
246
+ emitProductFactors(node.factors, session, 1, -head.value, head);
247
+ return;
248
+ }
249
+ emitProductFactors(node.factors, session, 0, -1, num(-1));
250
+ return;
251
+ }
252
+ session.buffer.push('-1 * ');
253
+ emitNode(node, session, UNARY_PRECEDENCE, false);
254
+ }
255
+
256
+ /**
257
+ * @param {ProductFactor[]} factors
258
+ * @param {SerializeSession} session
259
+ * @param {number} [start]
260
+ * @param {number} [coefficientValue]
261
+ * @param {import('../node.js').Num} [coefficientNode]
262
+ * @return {void}
263
+ */
264
+ function emitProductFactors(
265
+ factors,
266
+ session,
267
+ start = 0,
268
+ coefficientValue,
269
+ coefficientNode
270
+ ) {
271
+ const buffer = session.buffer;
272
+ let first = true;
273
+ if (coefficientValue !== undefined && coefficientValue !== 1) {
274
+ emitScalar(
275
+ /** @type {import('../node.js').Num} */ (coefficientNode),
276
+ session,
277
+ coefficientValue
278
+ );
279
+ first = false;
280
+ }
281
+ for (let i = start; i < factors.length; i++) {
282
+ const factor = factors[i];
283
+ 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;
289
+ } else {
290
+ buffer.push(factor.exponent === 1 ? ' * ' : ' / ');
291
+ if (isScalar(factorNode)) emitScalar(factorNode, session);
292
+ else emitNode(factorNode, session, PRODUCT_PRECEDENCE);
293
+ }
294
+ }
295
+ }
296
+
297
+ /** @param {Product} product @param {SerializeSession} session @return {void} */
298
+ function emitProduct(product, session) {
299
+ emitProductFactors(product.factors, session);
300
+ }
301
+
302
+ /**
303
+ * @param {Node} node
304
+ * @param {SerializeSession} session
305
+ * @param {string} wrapper
306
+ * @return {void}
307
+ */
308
+ function emitMathResult(node, session, wrapper) {
309
+ if (isScalar(node)) {
310
+ const scalarValue = roundedScalarValue(node, session.precision);
311
+ const buffer = session.buffer;
312
+ if (isDegenerate(scalarValue)) {
313
+ buffer.push(wrapper, '(', degenerateKeyword(scalarValue));
314
+ if (node.type === 'Dim') buffer.push(' * 1', node.rawUnit ?? node.unit);
315
+ buffer.push(')');
316
+ } else if (session.scalarPolicy === 'standard') {
317
+ buffer.push(wrapper, '(');
318
+ emitRoundedScalar(node, buffer, scalarValue);
319
+ buffer.push(')');
320
+ } else {
321
+ emitRoundedScalar(node, buffer, scalarValue);
322
+ }
323
+ return;
324
+ }
325
+ if (
326
+ node.type === 'Ident' ||
327
+ node.type === 'Call' ||
328
+ node.type === 'OpaqueCall'
329
+ ) {
330
+ emitNode(node, session);
331
+ return;
332
+ }
333
+ if (node.type === 'Sum' && node.terms.length === 1) {
334
+ session.buffer.push(wrapper, '(');
335
+ emitLeadingNeg(node.terms[0].node, session);
336
+ session.buffer.push(')');
337
+ return;
338
+ }
339
+ session.buffer.push(wrapper, '(');
340
+ emitNode(node, session);
341
+ session.buffer.push(')');
342
+ }
343
+
344
+ /**
345
+ * @param {Node} node
346
+ * @param {SerializeSession} session
347
+ * @param {string[]} [buffer]
348
+ * @return {void}
349
+ */
350
+ function emitNestedMathResult(node, session, buffer = session.buffer) {
351
+ if (isSignedZero(node)) {
352
+ emitSignedZero(buffer, node);
353
+ return;
354
+ }
355
+ emitMathResult(node, session, 'calc');
356
+ }
357
+
358
+ export {
359
+ SUM_PRECEDENCE,
360
+ PRODUCT_PRECEDENCE,
361
+ ATOMIC_PRECEDENCE,
362
+ UNARY_PRECEDENCE,
363
+ precedence,
364
+ needsParentheses,
365
+ emitNode,
366
+ emitCall,
367
+ emitOpaqueCall,
368
+ termSign,
369
+ emitSumTerms,
370
+ emitSum,
371
+ emitLeadingNeg,
372
+ emitProductFactors,
373
+ emitProduct,
374
+ emitMathResult,
375
+ emitNestedMathResult,
376
+ };
@@ -0,0 +1,244 @@
1
+ // Spec: https://www.w3.org/TR/css-values-4/#serialize-a-calculation-tree
2
+ // Precision rounding, degenerate keyword serialization (NaN, Infinity), and
3
+ // scalar number/dimension formatting.
4
+
5
+ /**
6
+ * @typedef {import('../node.js').Node} Node
7
+ * @typedef {import('../node.js').Num} Num
8
+ * @typedef {import('../node.js').Dim} Dim
9
+ * @typedef {object} SerializeSession
10
+ * @property {string[]} buffer
11
+ * @property {number | false} precision
12
+ * @property {'standard' | 'unwrap-all'} scalarPolicy
13
+ */
14
+
15
+ const NOISE_FLOOR = 1e-12;
16
+
17
+ /**
18
+ * Divide a decimal digit string by 10^k, rounding half away from zero, and
19
+ * return the resulting integer digit string. `digits` has no leading zeros.
20
+ * @param {string} digits
21
+ * @param {number} k
22
+ * @return {string}
23
+ */
24
+ function divideByPowerOfTen(digits, k) {
25
+ // 0x30/0x35/0x39 are the char codes of '0'/'5'/'9'.
26
+ if (digits.length <= k) {
27
+ return digits.length === k && digits.charCodeAt(0) >= 0x35 ? '1' : '0';
28
+ }
29
+ const cut = digits.length - k;
30
+ if (digits.charCodeAt(cut) < 0x35) return digits.slice(0, cut);
31
+ // Round up and propagate the carry through trailing nines.
32
+ let index = cut - 1;
33
+ while (index >= 0 && digits.charCodeAt(index) === 0x39) index--;
34
+ if (index < 0) return `1${'0'.repeat(cut)}`;
35
+ return `${digits.slice(0, index)}${String.fromCharCode(
36
+ digits.charCodeAt(index) + 1
37
+ )}${'0'.repeat(cut - index - 1)}`;
38
+ }
39
+
40
+ /**
41
+ * Round the shortest decimal representation of a non-negative double to `p`
42
+ * fractional digits, half away from zero.
43
+ *
44
+ * `Number(text + 'e' + p)` reads the exact intended decimal (so `1.005` at
45
+ * precision 2 becomes `1.01`), but it is only exact while the shifted value
46
+ * fits in `Number.MAX_SAFE_INTEGER`; beyond that the intermediate double
47
+ * rounds and can move the rounding boundary (e.g. `312834450754803.44` at
48
+ * precision 1 or 6 drifted to `312834450754803.5`). Round the decimal digits
49
+ * directly instead.
50
+ *
51
+ * @param {number} abs
52
+ * @param {number} p
53
+ * @return {number}
54
+ */
55
+ function roundDecimal(abs, p) {
56
+ const text = String(abs);
57
+ const eIdx = text.indexOf('e');
58
+ const mantissa = eIdx === -1 ? text : text.slice(0, eIdx);
59
+ let exponent = eIdx === -1 ? 0 : Number(text.slice(eIdx + 1));
60
+ const dot = mantissa.indexOf('.');
61
+ let digits = mantissa;
62
+ if (dot !== -1) {
63
+ digits = mantissa.slice(0, dot) + mantissa.slice(dot + 1);
64
+ exponent -= mantissa.length - dot - 1;
65
+ }
66
+
67
+ // value = digits * 10^exponent, so the shortest decimal has -exponent
68
+ // fractional digits when it is smaller than 1.
69
+ if (exponent >= -p) return abs;
70
+
71
+ let start = 0;
72
+ while (start < digits.length - 1 && digits.charCodeAt(start) === 0x30)
73
+ start++;
74
+ const rounded = divideByPowerOfTen(digits.slice(start), -(exponent + p));
75
+ return Number(`${rounded}e-${p}`);
76
+ }
77
+
78
+ /**
79
+ * @param {number} v
80
+ * @param {number | false} prec
81
+ * @return {number}
82
+ */
83
+ function round(v, prec) {
84
+ if (prec === false || !Number.isFinite(v)) return v;
85
+ if (Object.is(v, -0) || v === 0) return v;
86
+ const abs = Math.abs(v);
87
+ // Numbers >= MAX_SAFE_INTEGER (2^53 - 1) cannot represent fractional values, and
88
+ // integers already have 0 fractional places. Bypassing them avoids float drift.
89
+ if (abs >= Number.MAX_SAFE_INTEGER || Number.isInteger(v)) return v;
90
+
91
+ // Clamp precision to [0, 100] integer to prevent NaN from fractional precisions
92
+ // or exponent overflows into Infinity/NaN (e.g. exponent + prec > 308).
93
+ const p = Math.min(100, Math.max(0, Math.trunc(prec)));
94
+ const sign = v < 0 ? -1 : 1;
95
+ // Values below 1 keep `p` significant digits (at least one) rather than `p`
96
+ // decimals, so small magnitudes lose precision evenly. The exponent comes
97
+ // from the decimal text, not log10, so powers of ten cannot land off by one.
98
+ if (abs < 1) {
99
+ const exponent = Number(abs.toExponential().split('e')[1]);
100
+ const rounded = roundDecimal(abs, Math.max(p, 1) - 1 - exponent);
101
+ // Snap true floating-point dust (<= 1e-12) to zero, unless the precision
102
+ // is high enough to represent it as a fractional value.
103
+ if (abs <= NOISE_FLOOR && roundDecimal(abs, p) === 0) {
104
+ return sign === -1 ? -0 : 0;
105
+ }
106
+ return sign * rounded;
107
+ }
108
+ return sign * roundDecimal(abs, p);
109
+ }
110
+
111
+ // §10.13 / §10.7.2: Infinity/NaN serialize as canonical keywords.
112
+ /** @param {number} v @return {boolean} */
113
+ function isDegenerate(v) {
114
+ return !Number.isFinite(v) || Number.isNaN(v);
115
+ }
116
+
117
+ /** @param {number} v @return {string} */
118
+ function degenerateKeyword(v) {
119
+ if (Number.isNaN(v)) return 'NaN';
120
+ return v > 0 ? 'infinity' : '-infinity';
121
+ }
122
+
123
+ /** @param {number} v @return {string} */
124
+ function serializeNumber(v) {
125
+ if (Object.is(v, -0)) return '0';
126
+ const text = String(v);
127
+ if (text.startsWith('0.')) return text.slice(1);
128
+ if (text.startsWith('-0.')) return `-${text.slice(2)}`;
129
+ return text;
130
+ }
131
+
132
+ /**
133
+ * @param {import('../node.js').Num | import('../node.js').Dim} node
134
+ * @param {number | false} precision
135
+ * @param {number} [value]
136
+ * @return {number}
137
+ */
138
+ function roundedScalarValue(node, precision, value) {
139
+ return round(value ?? node.value, precision);
140
+ }
141
+
142
+ /**
143
+ * @param {import('../node.js').Num | import('../node.js').Dim} node
144
+ * @param {string[]} buffer
145
+ * @param {number} value
146
+ * @return {void}
147
+ */
148
+ function emitRoundedScalar(node, buffer, value) {
149
+ buffer.push(serializeNumber(value));
150
+ if (node.type === 'Dim') {
151
+ buffer.push(node.rawUnit ?? node.unit);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * @param {import('../node.js').Num | import('../node.js').Dim} node
157
+ * @param {SerializeSession} session
158
+ * @param {number} [value]
159
+ * @return {number}
160
+ */
161
+ function emitFiniteScalar(node, session, value) {
162
+ const rounded = roundedScalarValue(node, session.precision, value);
163
+ emitRoundedScalar(node, session.buffer, rounded);
164
+ return rounded;
165
+ }
166
+
167
+ /**
168
+ * @param {import('../node.js').Num | import('../node.js').Dim} node
169
+ * @param {SerializeSession} session
170
+ * @param {number} [value]
171
+ * @return {void}
172
+ */
173
+ function emitScalar(node, session, value) {
174
+ const buffer = session.buffer;
175
+ const effective = value ?? node.value;
176
+ if (Object.is(effective, -0)) emitSignedZero(buffer, node);
177
+ else if (isDegenerate(effective)) {
178
+ if (node.type === 'Dim') {
179
+ buffer.push(
180
+ 'calc(',
181
+ degenerateKeyword(effective),
182
+ ' * 1',
183
+ node.rawUnit ?? node.unit,
184
+ ')'
185
+ );
186
+ } else {
187
+ buffer.push(degenerateKeyword(effective));
188
+ }
189
+ } else emitFiniteScalar(node, session, effective);
190
+ }
191
+
192
+ /**
193
+ * @param {string[]} buffer
194
+ * @param {import('../node.js').Num | import('../node.js').Dim} node
195
+ * @return {void}
196
+ */
197
+ function emitSignedZero(buffer, node) {
198
+ const unit = node.type === 'Dim' ? (node.rawUnit ?? node.unit) : '';
199
+ buffer.push('calc(-1 * 0', unit, ')');
200
+ }
201
+
202
+ /** @param {Node} node @return {node is import('../node.js').Num | import('../node.js').Dim} */
203
+ function isScalar(node) {
204
+ return node.type === 'Num' || node.type === 'Dim';
205
+ }
206
+
207
+ /** @param {Node} node @return {node is import('../node.js').Num | import('../node.js').Dim} */
208
+ function isSignedZero(node) {
209
+ return isScalar(node) ? Object.is(node.value, -0) : false;
210
+ }
211
+
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
+ export {
229
+ NOISE_FLOOR,
230
+ divideByPowerOfTen,
231
+ roundDecimal,
232
+ round,
233
+ isDegenerate,
234
+ degenerateKeyword,
235
+ serializeNumber,
236
+ roundedScalarValue,
237
+ emitRoundedScalar,
238
+ emitFiniteScalar,
239
+ emitScalar,
240
+ emitSignedZero,
241
+ isScalar,
242
+ isSignedZero,
243
+ isEffectivelyNegative,
244
+ };