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.
- package/LICENSE +0 -0
- package/README.md +47 -35
- package/package.json +15 -15
- package/src/index.js +2 -1
- package/src/lib/analyze.js +0 -6
- package/src/lib/compile.js +1 -1
- package/src/lib/function-analyzers.js +119 -0
- package/src/lib/functions.js +25 -175
- package/src/lib/node.js +61 -4
- package/src/lib/parser/opaque.js +152 -0
- package/src/lib/parser/tokens.js +242 -0
- package/src/lib/parser.js +32 -377
- package/src/lib/serialize/expression.js +329 -0
- package/src/lib/serialize/precedence.js +47 -0
- package/src/lib/serialize/precision.js +244 -0
- package/src/lib/serialize.js +24 -561
- 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-barrier.js +55 -0
- package/src/lib/simplify/product-quotient.js +141 -0
- package/src/lib/simplify/product.js +92 -48
- package/src/lib/simplify/sum.js +6 -2
- package/src/lib/simplify.js +22 -5
- package/src/lib/types.js +94 -0
- package/src/reduce.js +2 -1
- package/types/lib/function-analyzers.d.ts +27 -0
- package/types/lib/functions.d.ts +3 -28
- 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 +55 -0
- package/types/lib/serialize/precedence.d.ts +18 -0
- package/types/lib/serialize/precision.d.ts +102 -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-barrier.d.ts +26 -0
- package/types/lib/simplify/product-quotient.d.ts +48 -0
- 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/types/lib/types.d.ts +48 -0
- 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
|
+
};
|