postcss-calc 11.1.2 → 11.2.1
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/README.md +40 -18
- package/package.json +18 -14
- package/src/index.js +8 -5
- package/src/lib/analyze.js +272 -0
- package/src/lib/block-index.js +118 -0
- package/src/lib/compile.js +80 -0
- package/src/lib/functions.js +394 -0
- package/src/lib/limits.js +49 -0
- package/src/lib/node.js +63 -21
- package/src/lib/opaque.js +12 -32
- package/src/lib/parser.js +373 -328
- package/src/lib/print.js +75 -0
- package/src/lib/regex.js +4 -0
- package/src/lib/scan.js +61 -0
- package/src/lib/serialize.js +626 -223
- package/src/lib/simplify/call.js +5 -98
- package/src/lib/simplify/round.js +4 -4
- package/src/lib/simplify/sum.js +14 -5
- package/src/lib/simplify.js +20 -5
- package/src/reduce.js +46 -115
- package/types/index.d.ts +4 -0
- package/types/lib/analyze.d.ts +45 -0
- package/types/lib/block-index.d.ts +35 -0
- package/types/lib/calculation-type.d.ts +18 -0
- package/types/lib/compile.d.ts +37 -0
- package/types/lib/functions.d.ts +58 -0
- package/types/lib/limits.d.ts +8 -0
- package/types/lib/node.d.ts +19 -3
- package/types/lib/opaque.d.ts +8 -14
- package/types/lib/parser.d.ts +42 -48
- package/types/lib/print.d.ts +16 -0
- package/types/lib/regex.d.ts +2 -0
- package/types/lib/scan.d.ts +35 -0
- package/types/lib/serialize.d.ts +19 -2
- package/types/lib/simplify/call.d.ts +4 -18
- package/types/lib/simplify/round.d.ts +3 -1
- package/types/lib/simplify.d.ts +5 -2
- package/types/reduce.d.ts +47 -6
- package/src/lib/tokenizer.js +0 -10
- package/types/lib/tokenizer.d.ts +0 -3
package/src/lib/serialize.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// Spec: https://www.w3.org/TR/css-values-4/#serialize-a-calculation-tree
|
|
2
2
|
// Outer calc() is added when the top-level result contains an arithmetic
|
|
3
|
-
// operator, or when a finite scalar
|
|
3
|
+
// operator, or when a finite scalar needs context-sensitive CSS semantics.
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { serializeComponents } from './opaque.js';
|
|
6
|
+
import { checkCalculationDepth } from './limits.js';
|
|
7
|
+
import { isCalculationFunction } from './functions.js';
|
|
8
|
+
import { num } from './node.js';
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* @typedef {import('./node.js').Node} Node
|
|
@@ -13,324 +15,725 @@ import { getComponents, serializeComponents } from './opaque.js';
|
|
|
13
15
|
* @typedef {object} SerializeOptions
|
|
14
16
|
* @property {number | false} [precision] Decimal places for numbers. `false` disables rounding. Default 5.
|
|
15
17
|
* @property {string} [calcName] Wrapper name to use when `calc()` is needed. Default `'calc'`.
|
|
16
|
-
* @property {boolean} [unwrapSingleNegativeNumber]
|
|
18
|
+
* @property {boolean} [unwrapSingleNegativeNumber] Deprecated alias for `unwrapSingleValue`.
|
|
19
|
+
* @property {boolean} [unwrapSingleValue] Serialize fully resolved finite scalar results without calculation syntax.
|
|
17
20
|
*/
|
|
18
21
|
|
|
19
|
-
//
|
|
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;
|
|
20
30
|
const NOISE_FLOOR = 1e-12;
|
|
21
31
|
|
|
22
32
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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.
|
|
27
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
|
+
/**
|
|
28
94
|
* @param {number} v
|
|
29
95
|
* @param {number | false} prec
|
|
30
96
|
* @return {number}
|
|
31
97
|
*/
|
|
32
98
|
function round(v, prec) {
|
|
33
|
-
if (prec === false)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
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)));
|
|
41
118
|
}
|
|
42
119
|
return rounded;
|
|
43
120
|
}
|
|
44
121
|
|
|
45
122
|
// §10.13 / §10.7.2: Infinity/NaN serialize as canonical keywords.
|
|
46
|
-
/**
|
|
47
|
-
* @param {number} v
|
|
48
|
-
* @return {boolean}
|
|
49
|
-
*/
|
|
123
|
+
/** @param {number} v @return {boolean} */
|
|
50
124
|
function isDegenerate(v) {
|
|
51
125
|
return !Number.isFinite(v) || Number.isNaN(v);
|
|
52
126
|
}
|
|
53
127
|
|
|
54
|
-
/**
|
|
55
|
-
* @param {number} v
|
|
56
|
-
* @return {string}
|
|
57
|
-
*/
|
|
128
|
+
/** @param {number} v @return {string} */
|
|
58
129
|
function degenerateKeyword(v) {
|
|
59
|
-
if (Number.isNaN(v))
|
|
60
|
-
return 'NaN';
|
|
61
|
-
}
|
|
130
|
+
if (Number.isNaN(v)) return 'NaN';
|
|
62
131
|
return v > 0 ? 'infinity' : '-infinity';
|
|
63
132
|
}
|
|
64
133
|
|
|
65
|
-
/**
|
|
66
|
-
* Serialize a finite CSS number. CSS numbers may omit the zero before a
|
|
67
|
-
* fractional value between -1 and 1 (`.5`, `-.5`). Scientific notation is
|
|
68
|
-
* left untouched because it already has no leading zero to remove.
|
|
69
|
-
*
|
|
70
|
-
* @param {number} v
|
|
71
|
-
* @return {string}
|
|
72
|
-
*/
|
|
134
|
+
/** @param {number} v @return {string} */
|
|
73
135
|
function serializeNumber(v) {
|
|
136
|
+
if (Object.is(v, -0)) return '0';
|
|
74
137
|
const text = String(v);
|
|
75
|
-
if (text.startsWith('0.'))
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
if (text.startsWith('-0.')) {
|
|
79
|
-
return `-${text.slice(2)}`;
|
|
80
|
-
}
|
|
138
|
+
if (text.startsWith('0.')) return text.slice(1);
|
|
139
|
+
if (text.startsWith('-0.')) return `-${text.slice(2)}`;
|
|
81
140
|
return text;
|
|
82
141
|
}
|
|
83
142
|
|
|
143
|
+
/** @param {SerializeOptions} opts @return {'standard' | 'unwrap-all'} */
|
|
144
|
+
function normalizeScalarPolicy(opts) {
|
|
145
|
+
return opts.unwrapSingleValue || opts.unwrapSingleNegativeNumber
|
|
146
|
+
? 'unwrap-all'
|
|
147
|
+
: 'standard';
|
|
148
|
+
}
|
|
149
|
+
|
|
84
150
|
/**
|
|
85
|
-
* Round and serialize a finite scalar once so callers can use the same value
|
|
86
|
-
* to decide its syntactic context and render its text.
|
|
87
|
-
*
|
|
88
151
|
* @param {import('./node.js').Num | import('./node.js').Dim} node
|
|
89
|
-
* @param {number | false}
|
|
90
|
-
* @
|
|
152
|
+
* @param {number | false} precision
|
|
153
|
+
* @param {number} [value]
|
|
154
|
+
* @return {number}
|
|
91
155
|
*/
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
const text = `${serializeNumber(value)}${node.type === 'Dim' ? (node.rawUnit ?? node.unit) : ''}`;
|
|
95
|
-
return { value, text };
|
|
156
|
+
function roundedScalarValue(node, precision, value) {
|
|
157
|
+
return round(value ?? node.value, precision);
|
|
96
158
|
}
|
|
97
159
|
|
|
98
160
|
/**
|
|
99
|
-
* @param {
|
|
100
|
-
* @param {
|
|
101
|
-
* @
|
|
161
|
+
* @param {import('./node.js').Num | import('./node.js').Dim} node
|
|
162
|
+
* @param {string[]} buffer
|
|
163
|
+
* @param {number} value
|
|
164
|
+
* @return {void}
|
|
102
165
|
*/
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// §10.13: top-level Infinity/NaN wrap in calc(); dim degenerates carry
|
|
108
|
-
// the unit as `<keyword> * 1<unit>` so the result keeps its type.
|
|
109
|
-
if (node.type === 'Num' && isDegenerate(node.value)) {
|
|
110
|
-
return `${calcName}(${degenerateKeyword(node.value)})`;
|
|
111
|
-
}
|
|
112
|
-
if (node.type === 'Dim' && isDegenerate(node.value)) {
|
|
113
|
-
return `${calcName}(${degenerateKeyword(node.value)} * 1${node.rawUnit ?? node.unit})`;
|
|
166
|
+
function emitRoundedScalar(node, buffer, value) {
|
|
167
|
+
buffer.push(serializeNumber(value));
|
|
168
|
+
if (node.type === 'Dim') {
|
|
169
|
+
buffer.push(node.rawUnit ?? node.unit);
|
|
114
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
|
+
}
|
|
115
184
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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));
|
|
127
206
|
}
|
|
207
|
+
} else emitFiniteScalar(node, session, effective);
|
|
208
|
+
}
|
|
128
209
|
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
}
|
|
131
219
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
node.type === 'Sum' &&
|
|
137
|
-
node.grouped &&
|
|
138
|
-
node.terms.length > 1 &&
|
|
139
|
-
displaySign(node.terms[0]).sign === -1
|
|
140
|
-
) {
|
|
141
|
-
const invertedTerms = node.terms.map((t) => ({
|
|
142
|
-
sign: /** @type {1 | -1} */ (-t.sign),
|
|
143
|
-
node: t.node,
|
|
144
|
-
}));
|
|
145
|
-
return `${calcName}(-(${serializeSumTerms(invertedTerms, prec)}))`;
|
|
146
|
-
}
|
|
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
|
+
}
|
|
147
224
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
+
}
|
|
151
229
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (node.type === 'Sum'
|
|
155
|
-
|
|
156
|
-
|
|
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
|
+
}
|
|
157
236
|
|
|
158
|
-
|
|
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
|
+
);
|
|
159
248
|
}
|
|
160
249
|
|
|
161
|
-
|
|
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
|
+
}
|
|
162
272
|
|
|
163
273
|
/**
|
|
164
274
|
* @param {Node} node
|
|
165
|
-
* @param {
|
|
166
|
-
* @return {
|
|
275
|
+
* @param {ReturnType<typeof makeContext>} session
|
|
276
|
+
* @return {void}
|
|
167
277
|
*/
|
|
168
|
-
function
|
|
278
|
+
function emitNodeBody(node, session) {
|
|
279
|
+
const buffer = session.buffer;
|
|
169
280
|
switch (node.type) {
|
|
170
281
|
case 'Num':
|
|
171
|
-
if (isDegenerate(node.value)) {
|
|
172
|
-
return degenerateKeyword(node.value);
|
|
173
|
-
}
|
|
174
|
-
return serializeScalar(node, prec).text;
|
|
175
282
|
case 'Dim':
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// parses back as one Dim factor. The bare form round-trips wrong
|
|
179
|
-
// inside a Product — `0 * Dim(Infinity, px)` would re-fold as NaN.
|
|
180
|
-
return `calc(${degenerateKeyword(node.value)} * 1${node.rawUnit ?? node.unit})`;
|
|
181
|
-
}
|
|
182
|
-
return serializeScalar(node, prec).text;
|
|
283
|
+
emitScalar(node, session);
|
|
284
|
+
return;
|
|
183
285
|
case 'Ident':
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
const args = node.args.map((a) => serializeExpr(a, prec)).join(', ');
|
|
194
|
-
return `${node.rawName ?? node.name}(${args})`;
|
|
195
|
-
}
|
|
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;
|
|
196
294
|
case 'Sum':
|
|
197
|
-
|
|
295
|
+
emitSum(node, session);
|
|
296
|
+
return;
|
|
198
297
|
case 'Product':
|
|
199
|
-
|
|
298
|
+
emitProduct(node, session);
|
|
299
|
+
return;
|
|
200
300
|
}
|
|
201
301
|
}
|
|
202
302
|
|
|
203
303
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* tokenize as a `-calc` function.
|
|
209
|
-
* @param {{sign: 1 | -1, node: Node}} term
|
|
210
|
-
* @return {{sign: 1 | -1, magnitude: Node}}
|
|
304
|
+
* @param {import('./node.js').Call} node
|
|
305
|
+
* @param {ReturnType<typeof makeContext>} session
|
|
306
|
+
* @param {string} [callNameOverride]
|
|
307
|
+
* @return {void}
|
|
211
308
|
*/
|
|
212
|
-
function
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
};
|
|
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);
|
|
219
315
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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);
|
|
225
375
|
}
|
|
226
|
-
return { sign, magnitude: node };
|
|
227
376
|
}
|
|
228
377
|
|
|
229
378
|
/**
|
|
230
379
|
* @param {import('./node.js').SumTerm[]} terms
|
|
231
|
-
* @param {
|
|
232
|
-
* @
|
|
380
|
+
* @param {ReturnType<typeof makeContext>} session
|
|
381
|
+
* @param {1 | -1} [multiplier]
|
|
382
|
+
* @return {void}
|
|
233
383
|
*/
|
|
234
|
-
function
|
|
235
|
-
|
|
384
|
+
function emitSumTerms(terms, session, multiplier = 1) {
|
|
385
|
+
const buffer = session.buffer;
|
|
236
386
|
for (let i = 0; i < terms.length; i++) {
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
+
}
|
|
243
413
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const sign = /** @type {1 | -1} */ (term.sign * multiplier);
|
|
417
|
+
if (i === 0) {
|
|
418
|
+
emitSumTerm(term, session, sign);
|
|
248
419
|
} else {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (magnitude.type === 'Sum' && magnitude.grouped) {
|
|
252
|
-
body = `(${body})`;
|
|
253
|
-
}
|
|
254
|
-
out += sign === 1 ? ` + ${body}` : ` - ${body}`;
|
|
420
|
+
buffer.push(sign === 1 ? ' + ' : ' - ');
|
|
421
|
+
emitNode(termNode, session, SUM_PRECEDENCE, true);
|
|
255
422
|
}
|
|
256
423
|
}
|
|
257
|
-
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** @param {Sum} sum @param {ReturnType<typeof makeContext>} session @return {void} */
|
|
427
|
+
function emitSum(sum, session) {
|
|
428
|
+
emitSumTerms(sum.terms, session);
|
|
258
429
|
}
|
|
259
430
|
|
|
260
431
|
/**
|
|
261
|
-
* @param {
|
|
262
|
-
* @param {
|
|
263
|
-
* @return {
|
|
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}
|
|
264
461
|
*/
|
|
265
|
-
function
|
|
266
|
-
|
|
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);
|
|
267
518
|
}
|
|
268
519
|
|
|
269
520
|
/**
|
|
270
|
-
* Fold a leading negation into a finite leading Num if there is one
|
|
271
|
-
* (`-(0.5 * x)` → `-0.5 * x`); else use `-(…)` for Sum/Product or `-x`.
|
|
272
521
|
* @param {Node} node
|
|
273
|
-
* @param {
|
|
274
|
-
* @
|
|
522
|
+
* @param {ReturnType<typeof makeContext>} session
|
|
523
|
+
* @param {string} wrapper
|
|
524
|
+
* @return {void}
|
|
275
525
|
*/
|
|
276
|
-
function
|
|
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
|
+
}
|
|
277
543
|
if (
|
|
278
|
-
node.type === '
|
|
279
|
-
node.
|
|
280
|
-
node.
|
|
281
|
-
node.
|
|
282
|
-
Number.isFinite(node.factors[0].node.value) &&
|
|
283
|
-
node.factors[0].node.value !== 0
|
|
544
|
+
node.type === 'Sum' &&
|
|
545
|
+
node.grouped &&
|
|
546
|
+
node.terms.length > 1 &&
|
|
547
|
+
termSign(node.terms[0], 1, session.precision) === -1
|
|
284
548
|
) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
/** @type {ProductFactor[]} */
|
|
290
|
-
const negatedFactors =
|
|
291
|
-
negatedValue === 1
|
|
292
|
-
? rest
|
|
293
|
-
: [{ exponent: 1, node: num(negatedValue) }, ...rest];
|
|
294
|
-
return serializeFactors(negatedFactors, prec);
|
|
549
|
+
session.buffer.push(wrapper, '(-1 * (');
|
|
550
|
+
emitSumTerms(node.terms, session, -1);
|
|
551
|
+
session.buffer.push('))');
|
|
552
|
+
return;
|
|
295
553
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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(')');
|
|
300
571
|
}
|
|
301
572
|
|
|
302
573
|
/**
|
|
303
|
-
*
|
|
304
|
-
* @param {
|
|
574
|
+
* Serialize a scalar result without allocating a render context or buffer.
|
|
575
|
+
* @param {import('./node.js').Num | import('./node.js').Dim} node
|
|
576
|
+
* @param {number | false} precision
|
|
577
|
+
* @param {'standard' | 'unwrap-all'} scalarPolicy
|
|
578
|
+
* @param {string} wrapper
|
|
305
579
|
* @return {string}
|
|
306
580
|
*/
|
|
307
|
-
function
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
581
|
+
function serializeScalarResult(node, precision, scalarPolicy, wrapper) {
|
|
582
|
+
const value = roundedScalarValue(node, precision);
|
|
583
|
+
const unit = node.type === 'Dim' ? (node.rawUnit ?? node.unit) : '';
|
|
584
|
+
if (isDegenerate(value)) {
|
|
585
|
+
return `${wrapper}(${degenerateKeyword(value)}${unit ? ` * 1${unit}` : ''})`;
|
|
586
|
+
}
|
|
587
|
+
const scalar = serializeNumber(value) + unit;
|
|
588
|
+
return scalarPolicy === 'standard' ? `${wrapper}(${scalar})` : scalar;
|
|
589
|
+
}
|
|
590
|
+
|
|
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
|
+
/**
|
|
606
|
+
* @param {SerializeOptions} opts
|
|
607
|
+
* @return {{buffer: string[], precision: number | false, scalarPolicy: 'standard' | 'unwrap-all'}}
|
|
608
|
+
*/
|
|
609
|
+
function makeContext(opts) {
|
|
610
|
+
return {
|
|
611
|
+
buffer: [],
|
|
612
|
+
precision: opts.precision ?? 5,
|
|
613
|
+
scalarPolicy: normalizeScalarPolicy(opts),
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* @param {Node} node
|
|
619
|
+
* @param {SerializeOptions} opts
|
|
620
|
+
* @return {{kind: 'math', node: Node, session: ReturnType<typeof makeContext>, wrapper: string}}
|
|
621
|
+
*/
|
|
622
|
+
function planSerialize(node, opts) {
|
|
623
|
+
return {
|
|
624
|
+
kind: 'math',
|
|
625
|
+
node,
|
|
626
|
+
session: makeContext(opts),
|
|
627
|
+
wrapper: opts.calcName ?? 'calc',
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* @param {{tree: Node, status: 'resolved' | 'unresolved', rootName: string, rootSpelling: string, calculation?: boolean, original?: string}} result
|
|
633
|
+
* @param {SerializeOptions} opts
|
|
634
|
+
* @return {{kind: 'original', text: string} | {kind: 'root-call', node: Node, session: ReturnType<typeof makeContext>, callNameOverride: string} | {kind: 'wrapped-expr', node: Node, session: ReturnType<typeof makeContext>, wrapper: string} | {kind: 'math', node: Node, session: ReturnType<typeof makeContext>, wrapper: string}}
|
|
635
|
+
*/
|
|
636
|
+
function planSerializeResult(result, opts) {
|
|
637
|
+
const isCalc = result.calculation ?? isCalculationFunction(result.rootName);
|
|
638
|
+
const normalizedRootName =
|
|
639
|
+
result.calculation === undefined
|
|
640
|
+
? result.rootName.toLowerCase()
|
|
641
|
+
: result.rootName;
|
|
642
|
+
const wrapper = isCalc
|
|
643
|
+
? result.rootSpelling || opts.calcName || 'calc'
|
|
644
|
+
: 'calc';
|
|
645
|
+
const session = makeContext(opts);
|
|
646
|
+
|
|
647
|
+
if (!isCalc && result.status === 'unresolved') {
|
|
648
|
+
if (
|
|
649
|
+
(result.tree.type === 'Call' || result.tree.type === 'OpaqueCall') &&
|
|
650
|
+
result.tree.name.toLowerCase() === normalizedRootName
|
|
651
|
+
) {
|
|
652
|
+
return {
|
|
653
|
+
kind: 'root-call',
|
|
654
|
+
node: result.tree,
|
|
655
|
+
session,
|
|
656
|
+
callNameOverride: result.rootSpelling,
|
|
657
|
+
};
|
|
316
658
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
659
|
+
return { kind: 'original', text: result.original ?? '' };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (session.scalarPolicy === 'standard') {
|
|
663
|
+
if (isScalar(result.tree))
|
|
664
|
+
return { kind: 'math', node: result.tree, session, wrapper };
|
|
665
|
+
return { kind: 'wrapped-expr', node: result.tree, session, wrapper };
|
|
666
|
+
}
|
|
667
|
+
return { kind: 'math', node: result.tree, session, wrapper };
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* @param {ReturnType<typeof planSerialize> | ReturnType<typeof planSerializeResult>} renderSpec
|
|
672
|
+
* @return {string}
|
|
673
|
+
* */
|
|
674
|
+
function emitOutput(renderSpec) {
|
|
675
|
+
if (renderSpec.kind === 'original') return renderSpec.text;
|
|
676
|
+
const { session } = renderSpec;
|
|
677
|
+
if (renderSpec.kind === 'root-call') {
|
|
678
|
+
if (renderSpec.node.type === 'Call') {
|
|
679
|
+
emitCall(renderSpec.node, session, renderSpec.callNameOverride);
|
|
320
680
|
} else {
|
|
321
|
-
|
|
681
|
+
emitOpaqueCall(
|
|
682
|
+
/** @type {import('./node.js').OpaqueCall} */ (renderSpec.node),
|
|
683
|
+
session,
|
|
684
|
+
renderSpec.callNameOverride
|
|
685
|
+
);
|
|
322
686
|
}
|
|
687
|
+
} else if (renderSpec.kind === 'wrapped-expr') {
|
|
688
|
+
session.buffer.push(renderSpec.wrapper, '(');
|
|
689
|
+
emitRootExpr(renderSpec.node, session);
|
|
690
|
+
session.buffer.push(')');
|
|
691
|
+
} else {
|
|
692
|
+
emitMathResult(renderSpec.node, session, renderSpec.wrapper);
|
|
323
693
|
}
|
|
324
|
-
return
|
|
694
|
+
return session.buffer.join('');
|
|
325
695
|
}
|
|
326
696
|
|
|
327
697
|
/**
|
|
328
|
-
* @param {
|
|
329
|
-
* @param {
|
|
698
|
+
* @param {Node} node
|
|
699
|
+
* @param {SerializeOptions} [opts]
|
|
700
|
+
* @return {string}
|
|
701
|
+
*/
|
|
702
|
+
function serialize(node, opts = {}) {
|
|
703
|
+
if (isScalar(node)) {
|
|
704
|
+
return serializeScalarResult(
|
|
705
|
+
node,
|
|
706
|
+
opts.precision ?? 5,
|
|
707
|
+
normalizeScalarPolicy(opts),
|
|
708
|
+
opts.calcName ?? 'calc'
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
checkCalculationDepth(node);
|
|
712
|
+
return emitOutput(planSerialize(node, opts));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* @param {{tree: Node, status: 'resolved' | 'unresolved', rootName: string, rootSpelling: string, calculation?: boolean, original?: string}} result
|
|
717
|
+
* @param {SerializeOptions} [opts]
|
|
330
718
|
* @return {string}
|
|
331
719
|
*/
|
|
332
|
-
function
|
|
333
|
-
|
|
720
|
+
function serializeResult(result, opts = {}) {
|
|
721
|
+
const node = result.tree;
|
|
722
|
+
if (isScalar(node)) {
|
|
723
|
+
const isCalc = result.calculation ?? isCalculationFunction(result.rootName);
|
|
724
|
+
if (!isCalc && result.status === 'unresolved') return result.original ?? '';
|
|
725
|
+
const wrapper = isCalc
|
|
726
|
+
? result.rootSpelling || opts.calcName || 'calc'
|
|
727
|
+
: 'calc';
|
|
728
|
+
return serializeScalarResult(
|
|
729
|
+
node,
|
|
730
|
+
opts.precision ?? 5,
|
|
731
|
+
normalizeScalarPolicy(opts),
|
|
732
|
+
wrapper
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
checkCalculationDepth(result.tree);
|
|
736
|
+
return emitOutput(planSerializeResult(result, opts));
|
|
334
737
|
}
|
|
335
738
|
|
|
336
|
-
export { serialize };
|
|
739
|
+
export { serialize, serializeResult };
|