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.
- package/LICENSE +0 -0
- package/README.md +44 -16
- package/package.json +13 -13
- package/src/index.js +2 -1
- package/src/lib/analyze.js +81 -43
- package/src/lib/compile.js +1 -1
- package/src/lib/functions.js +40 -10
- package/src/lib/node.js +62 -5
- package/src/lib/parser/opaque.js +151 -0
- package/src/lib/parser/tokens.js +242 -0
- package/src/lib/parser.js +27 -382
- package/src/lib/serialize/expression.js +376 -0
- package/src/lib/serialize/precision.js +244 -0
- package/src/lib/serialize.js +24 -517
- package/src/lib/simplify/bucket.js +22 -5
- package/src/lib/simplify/cancel.js +35 -11
- package/src/lib/simplify/clamp.js +2 -2
- package/src/lib/simplify/exact.js +18 -0
- package/src/lib/simplify/fold.js +15 -1
- package/src/lib/simplify/min-max.js +2 -2
- package/src/lib/simplify/product.js +263 -48
- package/src/lib/simplify/sum.js +6 -2
- package/src/lib/simplify.js +22 -5
- package/src/reduce.js +2 -1
- package/types/lib/analyze.d.ts +27 -0
- package/types/lib/functions.d.ts +19 -1
- package/types/lib/node.d.ts +17 -2
- package/types/lib/parser/opaque.d.ts +12 -0
- package/types/lib/parser/tokens.d.ts +83 -0
- package/types/lib/parser.d.ts +4 -44
- package/types/lib/serialize/expression.d.ts +95 -0
- package/types/lib/serialize/precision.d.ts +101 -0
- package/types/lib/simplify/bucket.d.ts +4 -1
- package/types/lib/simplify/cancel.d.ts +11 -7
- package/types/lib/simplify/exact.d.ts +9 -0
- package/types/lib/simplify/fold.d.ts +14 -1
- package/types/lib/simplify/product.d.ts +3 -7
- package/types/lib/simplify/sum.d.ts +2 -1
- package/types/lib/simplify.d.ts +3 -8
- 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
|
+
};
|