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
@@ -0,0 +1,329 @@
1
+ // Spec: https://www.w3.org/TR/css-values-4/#serialize-a-calculation-tree
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.
5
+
6
+ import { num } from '../node.js';
7
+ import { serializeComponents } from '../opaque.js';
8
+ import {
9
+ degenerateKeyword,
10
+ emitBareSignedZero,
11
+ emitRoundedScalar,
12
+ emitScalar,
13
+ emitSignedZero,
14
+ isDegenerate,
15
+ isScalar,
16
+ isSignedZero,
17
+ round,
18
+ roundedScalarValue,
19
+ } from './precision.js';
20
+ import {
21
+ needsParentheses,
22
+ PRODUCT_PRECEDENCE,
23
+ SUM_PRECEDENCE,
24
+ UNARY_PRECEDENCE,
25
+ } from './precedence.js';
26
+
27
+ /**
28
+ * @typedef {import('../node.js').Node} Node
29
+ * @typedef {import('../node.js').Sum} Sum
30
+ * @typedef {import('../node.js').SumTerm} SumTerm
31
+ * @typedef {import('../node.js').ProductFactor} ProductFactor
32
+ * @typedef {import('./precision.js').SerializeSession} SerializeSession
33
+ */
34
+
35
+ /**
36
+ * @param {Node} node
37
+ * @param {SerializeSession} session
38
+ * @param {number} [parentPrecedence]
39
+ * @param {boolean} [groupedRequired]
40
+ * @return {void}
41
+ */
42
+ function emitNode(
43
+ node,
44
+ session,
45
+ parentPrecedence = 0,
46
+ groupedRequired = false
47
+ ) {
48
+ const parenthesized = needsParentheses(
49
+ node,
50
+ parentPrecedence,
51
+ groupedRequired
52
+ );
53
+ if (parenthesized) session.buffer.push('(');
54
+ emitNodeBody(node, session);
55
+ if (parenthesized) session.buffer.push(')');
56
+ }
57
+
58
+ /**
59
+ * @param {Node} node
60
+ * @param {SerializeSession} session
61
+ * @return {void}
62
+ */
63
+ function emitNodeBody(node, session) {
64
+ const buffer = session.buffer;
65
+ switch (node.type) {
66
+ case 'Num':
67
+ case 'Dim':
68
+ emitScalar(node, session);
69
+ return;
70
+ case 'Ident':
71
+ buffer.push(node.rawName ?? node.name);
72
+ return;
73
+ case 'Call':
74
+ emitCall(node, session);
75
+ return;
76
+ case 'OpaqueCall':
77
+ emitOpaqueCall(node, session);
78
+ return;
79
+ case 'Sum':
80
+ emitSum(node, session);
81
+ return;
82
+ case 'Product':
83
+ emitProductFactors(node.factors, session);
84
+ return;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * @param {import('../node.js').Num | import('../node.js').Dim} termNode
90
+ * @param {1 | -1} sign
91
+ * @param {boolean} leading
92
+ * @param {SerializeSession} session
93
+ * @return {void}
94
+ */
95
+ function emitScalarSumTerm(termNode, sign, leading, session) {
96
+ const buffer = session.buffer;
97
+ const effectiveVal = sign * termNode.value;
98
+ if (Object.is(effectiveVal, -0)) {
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
+ }
107
+ return;
108
+ }
109
+ if (isDegenerate(effectiveVal)) {
110
+ if (!leading) buffer.push(sign === 1 ? ' + ' : ' - ');
111
+ else if (sign === -1) buffer.push('-');
112
+ emitScalar(termNode, session);
113
+ return;
114
+ }
115
+ const rounded = round(effectiveVal, session.precision);
116
+ if (rounded < 0) {
117
+ buffer.push(leading ? '-' : ' - ');
118
+ emitRoundedScalar(termNode, buffer, -rounded);
119
+ } else {
120
+ if (!leading) buffer.push(' + ');
121
+ emitRoundedScalar(termNode, buffer, rounded);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * @param {SumTerm} term
127
+ * @param {boolean} leading
128
+ * @param {SerializeSession} session
129
+ * @return {void}
130
+ */
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);
140
+ }
141
+ }
142
+
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
+ */
153
+ function emitSum(sum, 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
+ }
165
+ }
166
+
167
+ /**
168
+ * @param {Node} node
169
+ * @param {SerializeSession} session
170
+ * @return {void}
171
+ */
172
+ function emitLeadingNeg(node, session) {
173
+ if (node.type === 'Product' && !node.grouped) {
174
+ if (
175
+ node.factors.length > 0 &&
176
+ node.factors[0].exponent === 1 &&
177
+ node.factors[0].node.type === 'Num'
178
+ ) {
179
+ const head = node.factors[0].node;
180
+ emitProductFactors(node.factors, session, 1, -head.value, head);
181
+ return;
182
+ }
183
+ emitProductFactors(node.factors, session, 0, -1, num(-1));
184
+ return;
185
+ }
186
+ session.buffer.push('-1 * ');
187
+ emitNode(node, session, UNARY_PRECEDENCE, false);
188
+ }
189
+
190
+ /**
191
+ * @param {ProductFactor[]} factors
192
+ * @param {SerializeSession} session
193
+ * @param {number} [start]
194
+ * @param {number} [coefficientValue]
195
+ * @param {import('../node.js').Num} [coefficientNode]
196
+ * @return {void}
197
+ */
198
+ function emitProductFactors(
199
+ factors,
200
+ session,
201
+ start = 0,
202
+ coefficientValue,
203
+ coefficientNode
204
+ ) {
205
+ const buffer = session.buffer;
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;
210
+ if (coefficientValue !== undefined && coefficientValue !== 1) {
211
+ const coefficient = /** @type {import('../node.js').Num} */ (
212
+ coefficientNode
213
+ );
214
+ emitScalar(coefficient, session, coefficientValue, true);
215
+ leading = false;
216
+ }
217
+ for (let i = start; i < factors.length; i++) {
218
+ const factor = factors[i];
219
+ const factorNode = factor.node;
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);
225
+ } else {
226
+ emitNode(factorNode, session, PRODUCT_PRECEDENCE);
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);
247
+ }
248
+ buffer.push(')');
249
+ }
250
+
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(')');
264
+ }
265
+
266
+ /**
267
+ * @param {Node} node
268
+ * @param {SerializeSession} session
269
+ * @param {string} wrapper
270
+ * @return {void}
271
+ */
272
+ function emitMathResult(node, session, wrapper) {
273
+ if (isScalar(node)) {
274
+ const scalarValue = roundedScalarValue(node, session.precision);
275
+ const buffer = session.buffer;
276
+ if (isDegenerate(scalarValue)) {
277
+ buffer.push(wrapper, '(', degenerateKeyword(scalarValue));
278
+ if (node.type === 'Dim') buffer.push(' * 1', node.rawUnit ?? node.unit);
279
+ buffer.push(')');
280
+ } else if (session.scalarPolicy === 'standard') {
281
+ buffer.push(wrapper, '(');
282
+ emitRoundedScalar(node, buffer, scalarValue);
283
+ buffer.push(')');
284
+ } else {
285
+ emitRoundedScalar(node, buffer, scalarValue);
286
+ }
287
+ return;
288
+ }
289
+ if (
290
+ node.type === 'Ident' ||
291
+ node.type === 'Call' ||
292
+ node.type === 'OpaqueCall'
293
+ ) {
294
+ emitNode(node, session);
295
+ return;
296
+ }
297
+ if (node.type === 'Sum' && node.terms.length === 1) {
298
+ session.buffer.push(wrapper, '(');
299
+ emitLeadingNeg(node.terms[0].node, session);
300
+ session.buffer.push(')');
301
+ return;
302
+ }
303
+ session.buffer.push(wrapper, '(');
304
+ emitNode(node, session);
305
+ session.buffer.push(')');
306
+ }
307
+
308
+ /**
309
+ * @param {Node} node
310
+ * @param {SerializeSession} session
311
+ * @param {string[]} [buffer]
312
+ * @return {void}
313
+ */
314
+ function emitNestedMathResult(node, session, buffer = session.buffer) {
315
+ if (isSignedZero(node)) {
316
+ emitSignedZero(buffer, node);
317
+ return;
318
+ }
319
+ emitMathResult(node, session, 'calc');
320
+ }
321
+
322
+ export {
323
+ emitNode,
324
+ emitLeadingNeg,
325
+ emitCall,
326
+ emitOpaqueCall,
327
+ emitMathResult,
328
+ emitNestedMathResult,
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
+ };
@@ -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
+ * @param {boolean} [bare] emit negative zero without its calc() wrapper
172
+ * @return {void}
173
+ */
174
+ function emitScalar(node, session, value, bare = false) {
175
+ const buffer = session.buffer;
176
+ const effective = value ?? node.value;
177
+ if (Object.is(effective, -0)) {
178
+ if (bare) emitBareSignedZero(buffer, node);
179
+ else emitSignedZero(buffer, node);
180
+ } else if (isDegenerate(effective)) {
181
+ if (node.type === 'Dim') {
182
+ buffer.push(
183
+ 'calc(',
184
+ degenerateKeyword(effective),
185
+ ' * 1',
186
+ node.rawUnit ?? node.unit,
187
+ ')'
188
+ );
189
+ } else {
190
+ buffer.push(degenerateKeyword(effective));
191
+ }
192
+ } else emitFiniteScalar(node, session, effective);
193
+ }
194
+
195
+ /**
196
+ * @param {string[]} buffer
197
+ * @param {import('../node.js').Num | import('../node.js').Dim} node
198
+ * @return {void}
199
+ */
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) {
214
+ const unit = node.type === 'Dim' ? (node.rawUnit ?? node.unit) : '';
215
+ buffer.push('-1 * 0', unit);
216
+ }
217
+
218
+ /** @param {Node} node @return {node is import('../node.js').Num | import('../node.js').Dim} */
219
+ function isScalar(node) {
220
+ return node.type === 'Num' || node.type === 'Dim';
221
+ }
222
+
223
+ /** @param {Node} node @return {node is import('../node.js').Num | import('../node.js').Dim} */
224
+ function isSignedZero(node) {
225
+ return isScalar(node) ? Object.is(node.value, -0) : false;
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
+ emitBareSignedZero,
242
+ isScalar,
243
+ isSignedZero,
244
+ };