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
package/src/lib/serialize.js
CHANGED
|
@@ -2,10 +2,22 @@
|
|
|
2
2
|
// Outer calc() is added when the top-level result contains an arithmetic
|
|
3
3
|
// operator, or when a finite scalar needs context-sensitive CSS semantics.
|
|
4
4
|
|
|
5
|
-
import { serializeComponents } from './opaque.js';
|
|
6
5
|
import { checkCalculationDepth } from './limits.js';
|
|
7
6
|
import { isCalculationFunction } from './functions.js';
|
|
8
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
isScalar,
|
|
9
|
+
serializeNumber,
|
|
10
|
+
roundedScalarValue,
|
|
11
|
+
isDegenerate,
|
|
12
|
+
degenerateKeyword,
|
|
13
|
+
} from './serialize/precision.js';
|
|
14
|
+
import {
|
|
15
|
+
emitCall,
|
|
16
|
+
emitLeadingNeg,
|
|
17
|
+
emitOpaqueCall,
|
|
18
|
+
emitMathResult,
|
|
19
|
+
emitNode,
|
|
20
|
+
} from './serialize/expression.js';
|
|
9
21
|
|
|
10
22
|
/**
|
|
11
23
|
* @typedef {import('./node.js').Node} Node
|
|
@@ -19,127 +31,6 @@ import { num } from './node.js';
|
|
|
19
31
|
* @property {boolean} [unwrapSingleValue] Serialize fully resolved finite scalar results without calculation syntax.
|
|
20
32
|
*/
|
|
21
33
|
|
|
22
|
-
// The AST is canonical: sums and products are flat, so these precedence
|
|
23
|
-
// levels cover every binary expression
|
|
24
|
-
const SUM_PRECEDENCE = 1;
|
|
25
|
-
const PRODUCT_PRECEDENCE = 2;
|
|
26
|
-
const ATOMIC_PRECEDENCE = 3;
|
|
27
|
-
// Negation (-1 * ...) binds more tightly than a sum but has the same atomic boundary
|
|
28
|
-
// for deciding whether the operand needs parentheses.
|
|
29
|
-
const UNARY_PRECEDENCE = ATOMIC_PRECEDENCE;
|
|
30
|
-
const NOISE_FLOOR = 1e-12;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Divide a decimal digit string by 10^k, rounding half away from zero, and
|
|
34
|
-
* return the resulting integer digit string. `digits` has no leading zeros.
|
|
35
|
-
* @param {string} digits
|
|
36
|
-
* @param {number} k
|
|
37
|
-
* @return {string}
|
|
38
|
-
*/
|
|
39
|
-
function divideByPowerOfTen(digits, k) {
|
|
40
|
-
// 0x30/0x35/0x39 are the char codes of '0'/'5'/'9'.
|
|
41
|
-
if (digits.length <= k) {
|
|
42
|
-
return digits.length === k && digits.charCodeAt(0) >= 0x35 ? '1' : '0';
|
|
43
|
-
}
|
|
44
|
-
const cut = digits.length - k;
|
|
45
|
-
if (digits.charCodeAt(cut) < 0x35) return digits.slice(0, cut);
|
|
46
|
-
// Round up and propagate the carry through trailing nines.
|
|
47
|
-
let index = cut - 1;
|
|
48
|
-
while (index >= 0 && digits.charCodeAt(index) === 0x39) index--;
|
|
49
|
-
if (index < 0) return `1${'0'.repeat(cut)}`;
|
|
50
|
-
return `${digits.slice(0, index)}${String.fromCharCode(
|
|
51
|
-
digits.charCodeAt(index) + 1
|
|
52
|
-
)}${'0'.repeat(cut - index - 1)}`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Round the shortest decimal representation of a non-negative double to `p`
|
|
57
|
-
* fractional digits, half away from zero.
|
|
58
|
-
*
|
|
59
|
-
* `Number(text + 'e' + p)` reads the exact intended decimal (so `1.005` at
|
|
60
|
-
* precision 2 becomes `1.01`), but it is only exact while the shifted value
|
|
61
|
-
* fits in `Number.MAX_SAFE_INTEGER`; beyond that the intermediate double
|
|
62
|
-
* rounds and can move the rounding boundary (e.g. `312834450754803.44` at
|
|
63
|
-
* precision 1 or 6 drifted to `312834450754803.5`). Round the decimal digits
|
|
64
|
-
* directly instead.
|
|
65
|
-
*
|
|
66
|
-
* @param {number} abs
|
|
67
|
-
* @param {number} p
|
|
68
|
-
* @return {number}
|
|
69
|
-
*/
|
|
70
|
-
function roundDecimal(abs, p) {
|
|
71
|
-
const text = String(abs);
|
|
72
|
-
const eIdx = text.indexOf('e');
|
|
73
|
-
const mantissa = eIdx === -1 ? text : text.slice(0, eIdx);
|
|
74
|
-
let exponent = eIdx === -1 ? 0 : Number(text.slice(eIdx + 1));
|
|
75
|
-
const dot = mantissa.indexOf('.');
|
|
76
|
-
let digits = mantissa;
|
|
77
|
-
if (dot !== -1) {
|
|
78
|
-
digits = mantissa.slice(0, dot) + mantissa.slice(dot + 1);
|
|
79
|
-
exponent -= mantissa.length - dot - 1;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// value = digits * 10^exponent, so the shortest decimal has -exponent
|
|
83
|
-
// fractional digits when it is smaller than 1.
|
|
84
|
-
if (exponent >= -p) return abs;
|
|
85
|
-
|
|
86
|
-
let start = 0;
|
|
87
|
-
while (start < digits.length - 1 && digits.charCodeAt(start) === 0x30)
|
|
88
|
-
start++;
|
|
89
|
-
const rounded = divideByPowerOfTen(digits.slice(start), -(exponent + p));
|
|
90
|
-
return Number(`${rounded}e-${p}`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* @param {number} v
|
|
95
|
-
* @param {number | false} prec
|
|
96
|
-
* @return {number}
|
|
97
|
-
*/
|
|
98
|
-
function round(v, prec) {
|
|
99
|
-
if (prec === false || !Number.isFinite(v)) return v;
|
|
100
|
-
if (Object.is(v, -0) || v === 0) return v;
|
|
101
|
-
const abs = Math.abs(v);
|
|
102
|
-
// Numbers >= MAX_SAFE_INTEGER (2^53 - 1) cannot represent fractional values, and
|
|
103
|
-
// integers already have 0 fractional places. Bypassing them avoids float drift.
|
|
104
|
-
if (abs >= Number.MAX_SAFE_INTEGER || Number.isInteger(v)) return v;
|
|
105
|
-
|
|
106
|
-
// Clamp precision to [0, 100] integer to prevent NaN from fractional precisions
|
|
107
|
-
// or exponent overflows into Infinity/NaN (e.g. exponent + prec > 308).
|
|
108
|
-
const p = Math.min(100, Math.max(0, Math.trunc(prec)));
|
|
109
|
-
const sign = v < 0 ? -1 : 1;
|
|
110
|
-
// Fast path: rounding to integer with "round half away from zero".
|
|
111
|
-
const rounded =
|
|
112
|
-
p === 0 ? sign * Math.round(abs) : sign * roundDecimal(abs, p);
|
|
113
|
-
|
|
114
|
-
// Preserve non-zero values smaller than precision (e.g. 1/1000000) from collapsing
|
|
115
|
-
// to zero, while still snapping true floating-point dust (< 1e-12) to zero.
|
|
116
|
-
if (rounded === 0 && abs > NOISE_FLOOR) {
|
|
117
|
-
return Number(v.toPrecision(Math.max(p, 1)));
|
|
118
|
-
}
|
|
119
|
-
return rounded;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// §10.13 / §10.7.2: Infinity/NaN serialize as canonical keywords.
|
|
123
|
-
/** @param {number} v @return {boolean} */
|
|
124
|
-
function isDegenerate(v) {
|
|
125
|
-
return !Number.isFinite(v) || Number.isNaN(v);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** @param {number} v @return {string} */
|
|
129
|
-
function degenerateKeyword(v) {
|
|
130
|
-
if (Number.isNaN(v)) return 'NaN';
|
|
131
|
-
return v > 0 ? 'infinity' : '-infinity';
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** @param {number} v @return {string} */
|
|
135
|
-
function serializeNumber(v) {
|
|
136
|
-
if (Object.is(v, -0)) return '0';
|
|
137
|
-
const text = String(v);
|
|
138
|
-
if (text.startsWith('0.')) return text.slice(1);
|
|
139
|
-
if (text.startsWith('-0.')) return `-${text.slice(2)}`;
|
|
140
|
-
return text;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
34
|
/** @param {SerializeOptions} opts @return {'standard' | 'unwrap-all'} */
|
|
144
35
|
function normalizeScalarPolicy(opts) {
|
|
145
36
|
return opts.unwrapSingleValue || opts.unwrapSingleNegativeNumber
|
|
@@ -147,429 +38,6 @@ function normalizeScalarPolicy(opts) {
|
|
|
147
38
|
: 'standard';
|
|
148
39
|
}
|
|
149
40
|
|
|
150
|
-
/**
|
|
151
|
-
* @param {import('./node.js').Num | import('./node.js').Dim} node
|
|
152
|
-
* @param {number | false} precision
|
|
153
|
-
* @param {number} [value]
|
|
154
|
-
* @return {number}
|
|
155
|
-
*/
|
|
156
|
-
function roundedScalarValue(node, precision, value) {
|
|
157
|
-
return round(value ?? node.value, precision);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* @param {import('./node.js').Num | import('./node.js').Dim} node
|
|
162
|
-
* @param {string[]} buffer
|
|
163
|
-
* @param {number} value
|
|
164
|
-
* @return {void}
|
|
165
|
-
*/
|
|
166
|
-
function emitRoundedScalar(node, buffer, value) {
|
|
167
|
-
buffer.push(serializeNumber(value));
|
|
168
|
-
if (node.type === 'Dim') {
|
|
169
|
-
buffer.push(node.rawUnit ?? node.unit);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* @param {import('./node.js').Num | import('./node.js').Dim} node
|
|
175
|
-
* @param {ReturnType<typeof makeContext>} session
|
|
176
|
-
* @param {number} [value]
|
|
177
|
-
* @return {number}
|
|
178
|
-
*/
|
|
179
|
-
function emitFiniteScalar(node, session, value) {
|
|
180
|
-
const rounded = roundedScalarValue(node, session.precision, value);
|
|
181
|
-
emitRoundedScalar(node, session.buffer, rounded);
|
|
182
|
-
return rounded;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* @param {import('./node.js').Num | import('./node.js').Dim} node
|
|
187
|
-
* @param {ReturnType<typeof makeContext>} session
|
|
188
|
-
* @param {number} [value]
|
|
189
|
-
* @return {void}
|
|
190
|
-
*/
|
|
191
|
-
function emitScalar(node, session, value) {
|
|
192
|
-
const buffer = session.buffer;
|
|
193
|
-
const effective = value ?? node.value;
|
|
194
|
-
if (Object.is(effective, -0)) emitSignedZero(buffer, node);
|
|
195
|
-
else if (isDegenerate(effective)) {
|
|
196
|
-
if (node.type === 'Dim') {
|
|
197
|
-
buffer.push(
|
|
198
|
-
'calc(',
|
|
199
|
-
degenerateKeyword(effective),
|
|
200
|
-
' * 1',
|
|
201
|
-
node.rawUnit ?? node.unit,
|
|
202
|
-
')'
|
|
203
|
-
);
|
|
204
|
-
} else {
|
|
205
|
-
buffer.push(degenerateKeyword(effective));
|
|
206
|
-
}
|
|
207
|
-
} else emitFiniteScalar(node, session, effective);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* @param {string[]} buffer
|
|
212
|
-
* @param {import('./node.js').Num | import('./node.js').Dim} node
|
|
213
|
-
* @return {void}
|
|
214
|
-
*/
|
|
215
|
-
function emitSignedZero(buffer, node) {
|
|
216
|
-
const unit = node.type === 'Dim' ? (node.rawUnit ?? node.unit) : '';
|
|
217
|
-
buffer.push('calc(-1 * 0', unit, ')');
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** @param {Node} node @return {node is import('./node.js').Num | import('./node.js').Dim} */
|
|
221
|
-
function isScalar(node) {
|
|
222
|
-
return node.type === 'Num' || node.type === 'Dim';
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** @param {Node} node @return {node is import('./node.js').Num | import('./node.js').Dim} */
|
|
226
|
-
function isSignedZero(node) {
|
|
227
|
-
return isScalar(node) ? Object.is(node.value, -0) : false;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** @param {Node} node @return {number} */
|
|
231
|
-
function precedence(node) {
|
|
232
|
-
if (node.type === 'Sum') return SUM_PRECEDENCE;
|
|
233
|
-
if (node.type === 'Product') return PRODUCT_PRECEDENCE;
|
|
234
|
-
return ATOMIC_PRECEDENCE;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* @param {Node} node
|
|
239
|
-
* @param {number} parentPrecedence
|
|
240
|
-
* @param {boolean} groupedRequired
|
|
241
|
-
* @return {boolean}
|
|
242
|
-
*/
|
|
243
|
-
function needsParentheses(node, parentPrecedence, groupedRequired) {
|
|
244
|
-
return (
|
|
245
|
-
precedence(node) < parentPrecedence ||
|
|
246
|
-
(node.type === 'Sum' && node.grouped === true && groupedRequired === true)
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* @param {Node} node
|
|
252
|
-
* @param {ReturnType<typeof makeContext>} session
|
|
253
|
-
* @param {number} [parentPrecedence]
|
|
254
|
-
* @param {boolean} [groupedRequired]
|
|
255
|
-
* @return {void}
|
|
256
|
-
*/
|
|
257
|
-
function emitNode(
|
|
258
|
-
node,
|
|
259
|
-
session,
|
|
260
|
-
parentPrecedence = 0,
|
|
261
|
-
groupedRequired = false
|
|
262
|
-
) {
|
|
263
|
-
const parenthesized = needsParentheses(
|
|
264
|
-
node,
|
|
265
|
-
parentPrecedence,
|
|
266
|
-
groupedRequired
|
|
267
|
-
);
|
|
268
|
-
if (parenthesized) session.buffer.push('(');
|
|
269
|
-
emitNodeBody(node, session);
|
|
270
|
-
if (parenthesized) session.buffer.push(')');
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* @param {Node} node
|
|
275
|
-
* @param {ReturnType<typeof makeContext>} session
|
|
276
|
-
* @return {void}
|
|
277
|
-
*/
|
|
278
|
-
function emitNodeBody(node, session) {
|
|
279
|
-
const buffer = session.buffer;
|
|
280
|
-
switch (node.type) {
|
|
281
|
-
case 'Num':
|
|
282
|
-
case 'Dim':
|
|
283
|
-
emitScalar(node, session);
|
|
284
|
-
return;
|
|
285
|
-
case 'Ident':
|
|
286
|
-
buffer.push(node.rawName ?? node.name);
|
|
287
|
-
return;
|
|
288
|
-
case 'Call':
|
|
289
|
-
emitCall(node, session);
|
|
290
|
-
return;
|
|
291
|
-
case 'OpaqueCall':
|
|
292
|
-
emitOpaqueCall(node, session);
|
|
293
|
-
return;
|
|
294
|
-
case 'Sum':
|
|
295
|
-
emitSum(node, session);
|
|
296
|
-
return;
|
|
297
|
-
case 'Product':
|
|
298
|
-
emitProduct(node, session);
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* @param {import('./node.js').Call} node
|
|
305
|
-
* @param {ReturnType<typeof makeContext>} session
|
|
306
|
-
* @param {string} [callNameOverride]
|
|
307
|
-
* @return {void}
|
|
308
|
-
*/
|
|
309
|
-
function emitCall(node, session, callNameOverride) {
|
|
310
|
-
const buffer = session.buffer;
|
|
311
|
-
buffer.push(callNameOverride ?? node.rawName ?? node.name, '(');
|
|
312
|
-
for (let i = 0; i < node.args.length; i++) {
|
|
313
|
-
if (i > 0) buffer.push(', ');
|
|
314
|
-
emitNode(node.args[i], session);
|
|
315
|
-
}
|
|
316
|
-
buffer.push(')');
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* @param {import('./node.js').OpaqueCall} node
|
|
321
|
-
* @param {ReturnType<typeof makeContext>} session
|
|
322
|
-
* @param {string} [callNameOverride]
|
|
323
|
-
* @return {void}
|
|
324
|
-
*/
|
|
325
|
-
function emitOpaqueCall(node, session, callNameOverride) {
|
|
326
|
-
const buffer = session.buffer;
|
|
327
|
-
buffer.push(callNameOverride ?? node.rawName ?? node.name, '(');
|
|
328
|
-
serializeComponents(node.components, buffer, (child, childBuffer) => {
|
|
329
|
-
emitNestedMathResult(child, session, childBuffer);
|
|
330
|
-
});
|
|
331
|
-
buffer.push(')');
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Whether a scalar node is strictly negative after precision rounding
|
|
336
|
-
* (excluding signed zero and sub-precision values that round to zero).
|
|
337
|
-
* @param {Node} node
|
|
338
|
-
* @param {number | false} precision
|
|
339
|
-
* @return {node is import('./node.js').Num | import('./node.js').Dim}
|
|
340
|
-
*/
|
|
341
|
-
function isEffectivelyNegative(node, precision) {
|
|
342
|
-
return (
|
|
343
|
-
isScalar(node) &&
|
|
344
|
-
!Object.is(node.value, -0) &&
|
|
345
|
-
Number.isFinite(node.value) &&
|
|
346
|
-
round(node.value, precision) < 0
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* @param {import('./node.js').SumTerm} term
|
|
352
|
-
* @param {1 | -1} multiplier
|
|
353
|
-
* @param {number | false} precision
|
|
354
|
-
* @return {1 | -1}
|
|
355
|
-
*/
|
|
356
|
-
function termSign(term, multiplier, precision) {
|
|
357
|
-
let sign = /** @type {1 | -1} */ (term.sign * multiplier);
|
|
358
|
-
if (isEffectivelyNegative(term.node, precision)) {
|
|
359
|
-
sign = /** @type {1 | -1} */ (-sign);
|
|
360
|
-
}
|
|
361
|
-
return sign;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* @param {import('./node.js').SumTerm} term
|
|
366
|
-
* @param {ReturnType<typeof makeContext>} session
|
|
367
|
-
* @param {1 | -1} sign
|
|
368
|
-
* @return {void}
|
|
369
|
-
*/
|
|
370
|
-
function emitSumTerm(term, session, sign) {
|
|
371
|
-
if (sign === 1) {
|
|
372
|
-
emitNode(term.node, session, SUM_PRECEDENCE, true);
|
|
373
|
-
} else {
|
|
374
|
-
emitLeadingNeg(term.node, session);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* @param {import('./node.js').SumTerm[]} terms
|
|
380
|
-
* @param {ReturnType<typeof makeContext>} session
|
|
381
|
-
* @param {1 | -1} [multiplier]
|
|
382
|
-
* @return {void}
|
|
383
|
-
*/
|
|
384
|
-
function emitSumTerms(terms, session, multiplier = 1) {
|
|
385
|
-
const buffer = session.buffer;
|
|
386
|
-
for (let i = 0; i < terms.length; i++) {
|
|
387
|
-
const term = terms[i];
|
|
388
|
-
const termNode = term.node;
|
|
389
|
-
if (isScalar(termNode)) {
|
|
390
|
-
const effectiveVal = term.sign * multiplier * termNode.value;
|
|
391
|
-
if (Object.is(effectiveVal, -0)) {
|
|
392
|
-
if (i > 0) buffer.push(' + ');
|
|
393
|
-
emitSignedZero(buffer, termNode);
|
|
394
|
-
} else if (isDegenerate(effectiveVal)) {
|
|
395
|
-
const sign = /** @type {1 | -1} */ (term.sign * multiplier);
|
|
396
|
-
if (i === 0) {
|
|
397
|
-
if (sign === -1) buffer.push('-');
|
|
398
|
-
emitScalar(termNode, session);
|
|
399
|
-
} else {
|
|
400
|
-
buffer.push(sign === 1 ? ' + ' : ' - ');
|
|
401
|
-
emitScalar(termNode, session);
|
|
402
|
-
}
|
|
403
|
-
} else {
|
|
404
|
-
const rounded = round(effectiveVal, session.precision);
|
|
405
|
-
if (rounded < 0) {
|
|
406
|
-
if (i === 0) buffer.push('-');
|
|
407
|
-
else buffer.push(' - ');
|
|
408
|
-
emitRoundedScalar(termNode, buffer, -rounded);
|
|
409
|
-
} else {
|
|
410
|
-
if (i > 0) buffer.push(' + ');
|
|
411
|
-
emitRoundedScalar(termNode, buffer, rounded);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
const sign = /** @type {1 | -1} */ (term.sign * multiplier);
|
|
417
|
-
if (i === 0) {
|
|
418
|
-
emitSumTerm(term, session, sign);
|
|
419
|
-
} else {
|
|
420
|
-
buffer.push(sign === 1 ? ' + ' : ' - ');
|
|
421
|
-
emitNode(termNode, session, SUM_PRECEDENCE, true);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/** @param {Sum} sum @param {ReturnType<typeof makeContext>} session @return {void} */
|
|
427
|
-
function emitSum(sum, session) {
|
|
428
|
-
emitSumTerms(sum.terms, session);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* @param {Node} node
|
|
433
|
-
* @param {ReturnType<typeof makeContext>} session
|
|
434
|
-
* @return {void}
|
|
435
|
-
*/
|
|
436
|
-
function emitLeadingNeg(node, session) {
|
|
437
|
-
if (node.type === 'Product') {
|
|
438
|
-
if (
|
|
439
|
-
node.factors.length > 0 &&
|
|
440
|
-
node.factors[0].exponent === 1 &&
|
|
441
|
-
node.factors[0].node.type === 'Num'
|
|
442
|
-
) {
|
|
443
|
-
const head = node.factors[0].node;
|
|
444
|
-
emitProductFactors(node.factors, session, 1, -head.value, head);
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
emitProductFactors(node.factors, session, 0, -1, num(-1));
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
session.buffer.push('-1 * ');
|
|
451
|
-
emitNode(node, session, UNARY_PRECEDENCE, false);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* @param {ProductFactor[]} factors
|
|
456
|
-
* @param {ReturnType<typeof makeContext>} session
|
|
457
|
-
* @param {number} [start]
|
|
458
|
-
* @param {number} [coefficientValue]
|
|
459
|
-
* @param {import('./node.js').Num} [coefficientNode]
|
|
460
|
-
* @return {void}
|
|
461
|
-
*/
|
|
462
|
-
function emitProductFactors(
|
|
463
|
-
factors,
|
|
464
|
-
session,
|
|
465
|
-
start = 0,
|
|
466
|
-
coefficientValue,
|
|
467
|
-
coefficientNode
|
|
468
|
-
) {
|
|
469
|
-
const buffer = session.buffer;
|
|
470
|
-
let first = true;
|
|
471
|
-
if (coefficientValue !== undefined && coefficientValue !== 1) {
|
|
472
|
-
emitScalar(
|
|
473
|
-
/** @type {import('./node.js').Num} */ (coefficientNode),
|
|
474
|
-
session,
|
|
475
|
-
coefficientValue
|
|
476
|
-
);
|
|
477
|
-
first = false;
|
|
478
|
-
}
|
|
479
|
-
for (let i = start; i < factors.length; i++) {
|
|
480
|
-
const factor = factors[i];
|
|
481
|
-
const factorNode = factor.node;
|
|
482
|
-
if (first) {
|
|
483
|
-
if (factor.exponent === -1) buffer.push('1 / ');
|
|
484
|
-
if (isScalar(factorNode)) emitScalar(factorNode, session);
|
|
485
|
-
else emitNode(factorNode, session, PRODUCT_PRECEDENCE);
|
|
486
|
-
first = false;
|
|
487
|
-
} else {
|
|
488
|
-
buffer.push(factor.exponent === 1 ? ' * ' : ' / ');
|
|
489
|
-
if (isScalar(factorNode)) emitScalar(factorNode, session);
|
|
490
|
-
else emitNode(factorNode, session, PRODUCT_PRECEDENCE);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/** @param {Product} product @param {ReturnType<typeof makeContext>} session @return {void} */
|
|
496
|
-
function emitProduct(product, session) {
|
|
497
|
-
emitProductFactors(product.factors, session);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/** @param {Node} node @param {ReturnType<typeof makeContext>} session @return {void} */
|
|
501
|
-
function emitRootExpr(node, session) {
|
|
502
|
-
if (
|
|
503
|
-
node.type === 'Sum' &&
|
|
504
|
-
node.grouped &&
|
|
505
|
-
node.terms.length > 1 &&
|
|
506
|
-
termSign(node.terms[0], 1, session.precision) === -1
|
|
507
|
-
) {
|
|
508
|
-
session.buffer.push('-1 * (');
|
|
509
|
-
emitSumTerms(node.terms, session, -1);
|
|
510
|
-
session.buffer.push(')');
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
if (node.type === 'Sum' && node.terms.length === 1) {
|
|
514
|
-
emitLeadingNeg(node.terms[0].node, session);
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
emitNode(node, session);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* @param {Node} node
|
|
522
|
-
* @param {ReturnType<typeof makeContext>} session
|
|
523
|
-
* @param {string} wrapper
|
|
524
|
-
* @return {void}
|
|
525
|
-
*/
|
|
526
|
-
function emitMathResult(node, session, wrapper) {
|
|
527
|
-
if (isScalar(node)) {
|
|
528
|
-
const scalarValue = roundedScalarValue(node, session.precision);
|
|
529
|
-
const buffer = session.buffer;
|
|
530
|
-
if (isDegenerate(scalarValue)) {
|
|
531
|
-
buffer.push(wrapper, '(', degenerateKeyword(scalarValue));
|
|
532
|
-
if (node.type === 'Dim') buffer.push(' * 1', node.rawUnit ?? node.unit);
|
|
533
|
-
buffer.push(')');
|
|
534
|
-
} else if (session.scalarPolicy === 'standard') {
|
|
535
|
-
buffer.push(wrapper, '(');
|
|
536
|
-
emitRoundedScalar(node, buffer, scalarValue);
|
|
537
|
-
buffer.push(')');
|
|
538
|
-
} else {
|
|
539
|
-
emitRoundedScalar(node, buffer, scalarValue);
|
|
540
|
-
}
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
if (
|
|
544
|
-
node.type === 'Sum' &&
|
|
545
|
-
node.grouped &&
|
|
546
|
-
node.terms.length > 1 &&
|
|
547
|
-
termSign(node.terms[0], 1, session.precision) === -1
|
|
548
|
-
) {
|
|
549
|
-
session.buffer.push(wrapper, '(-1 * (');
|
|
550
|
-
emitSumTerms(node.terms, session, -1);
|
|
551
|
-
session.buffer.push('))');
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
if (
|
|
555
|
-
node.type === 'Ident' ||
|
|
556
|
-
node.type === 'Call' ||
|
|
557
|
-
node.type === 'OpaqueCall'
|
|
558
|
-
) {
|
|
559
|
-
emitNode(node, session);
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
if (node.type === 'Sum' && node.terms.length === 1) {
|
|
563
|
-
session.buffer.push(wrapper, '(');
|
|
564
|
-
emitLeadingNeg(node.terms[0].node, session);
|
|
565
|
-
session.buffer.push(')');
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
session.buffer.push(wrapper, '(');
|
|
569
|
-
emitNode(node, session);
|
|
570
|
-
session.buffer.push(')');
|
|
571
|
-
}
|
|
572
|
-
|
|
573
41
|
/**
|
|
574
42
|
* Serialize a scalar result without allocating a render context or buffer.
|
|
575
43
|
* @param {import('./node.js').Num | import('./node.js').Dim} node
|
|
@@ -588,20 +56,6 @@ function serializeScalarResult(node, precision, scalarPolicy, wrapper) {
|
|
|
588
56
|
return scalarPolicy === 'standard' ? `${wrapper}(${scalar})` : scalar;
|
|
589
57
|
}
|
|
590
58
|
|
|
591
|
-
/**
|
|
592
|
-
* @param {Node} node
|
|
593
|
-
* @param {ReturnType<typeof makeContext>} session
|
|
594
|
-
* @param {string[]} [buffer]
|
|
595
|
-
* @return {void}
|
|
596
|
-
*/
|
|
597
|
-
function emitNestedMathResult(node, session, buffer = session.buffer) {
|
|
598
|
-
if (isSignedZero(node)) {
|
|
599
|
-
emitSignedZero(buffer, node);
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
emitMathResult(node, session, 'calc');
|
|
603
|
-
}
|
|
604
|
-
|
|
605
59
|
/**
|
|
606
60
|
* @param {SerializeOptions} opts
|
|
607
61
|
* @return {{buffer: string[], precision: number | false, scalarPolicy: 'standard' | 'unwrap-all'}}
|
|
@@ -667,10 +121,19 @@ function planSerializeResult(result, opts) {
|
|
|
667
121
|
return { kind: 'math', node: result.tree, session, wrapper };
|
|
668
122
|
}
|
|
669
123
|
|
|
124
|
+
/** @param {Node} node @param {ReturnType<typeof makeContext>} session @return {void} */
|
|
125
|
+
function emitRootExpr(node, session) {
|
|
126
|
+
if (node.type === 'Sum' && node.terms.length === 1) {
|
|
127
|
+
emitLeadingNeg(node.terms[0].node, session);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
emitNode(node, session);
|
|
131
|
+
}
|
|
132
|
+
|
|
670
133
|
/**
|
|
671
134
|
* @param {ReturnType<typeof planSerialize> | ReturnType<typeof planSerializeResult>} renderSpec
|
|
672
135
|
* @return {string}
|
|
673
|
-
|
|
136
|
+
*/
|
|
674
137
|
function emitOutput(renderSpec) {
|
|
675
138
|
if (renderSpec.kind === 'original') return renderSpec.text;
|
|
676
139
|
const { session } = renderSpec;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { convert } from '../convertUnits.js';
|
|
8
|
+
import { isExact } from './exact.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* @typedef {object} UnitBucket
|
|
@@ -18,9 +19,12 @@ import { convert } from '../convertUnits.js';
|
|
|
18
19
|
/** Mutates `buckets` in place — totals of survivor buckets accumulate the
|
|
19
20
|
* converted values of merged neighbors. Caller must not reuse the input.
|
|
20
21
|
* @param {UnitBucket[]} buckets
|
|
22
|
+
* @param {number | false} [precision] A conversion that is not exact at this
|
|
23
|
+
* precision is not merged. When only the reverse direction is exact, the
|
|
24
|
+
* survivor switches to the other bucket's unit.
|
|
21
25
|
* @return {UnitBucket[]}
|
|
22
26
|
*/
|
|
23
|
-
function mergeConvertibleBuckets(buckets) {
|
|
27
|
+
function mergeConvertibleBuckets(buckets, precision = false) {
|
|
24
28
|
/** @type {Map<import('../convertUnits.js').BaseType, UnitBucket>} */ const representative =
|
|
25
29
|
new Map();
|
|
26
30
|
/** @type {UnitBucket[]} */ const out = [];
|
|
@@ -36,12 +40,25 @@ function mergeConvertibleBuckets(buckets) {
|
|
|
36
40
|
continue;
|
|
37
41
|
}
|
|
38
42
|
const converted = convert(b.total, b.unit, first.unit);
|
|
39
|
-
if (converted
|
|
40
|
-
|
|
43
|
+
if (converted !== null && isExact(converted, precision)) {
|
|
44
|
+
first.total += converted;
|
|
45
|
+
first.scale = Math.max(first.scale, Math.abs(converted));
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const reversed = convert(first.total, first.unit, b.unit);
|
|
49
|
+
const reversedScale = convert(first.scale, first.unit, b.unit);
|
|
50
|
+
if (
|
|
51
|
+
reversed !== null &&
|
|
52
|
+
reversedScale !== null &&
|
|
53
|
+
isExact(reversed, precision)
|
|
54
|
+
) {
|
|
55
|
+
first.unit = b.unit;
|
|
56
|
+
first.rawUnit = b.rawUnit;
|
|
57
|
+
first.total = reversed + b.total;
|
|
58
|
+
first.scale = Math.max(reversedScale, Math.abs(b.total));
|
|
41
59
|
continue;
|
|
42
60
|
}
|
|
43
|
-
|
|
44
|
-
first.scale = Math.max(first.scale, Math.abs(converted));
|
|
61
|
+
out.push(b);
|
|
45
62
|
}
|
|
46
63
|
return out;
|
|
47
64
|
}
|