postcss-calc 11.1.1 → 11.2.0
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 +16 -11
- package/src/index.js +8 -5
- package/src/lib/analyze.js +228 -0
- package/src/lib/block-index.js +118 -0
- package/src/lib/calculation-type.js +25 -0
- package/src/lib/compile.js +80 -0
- package/src/lib/convertUnits.js +15 -1
- package/src/lib/functions.js +364 -0
- package/src/lib/limits.js +49 -0
- package/src/lib/node.js +85 -39
- package/src/lib/opaque.js +20 -0
- package/src/lib/parser.js +493 -257
- 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 +574 -207
- package/src/lib/simplify/abs.js +1 -1
- package/src/lib/simplify/bucket.js +18 -23
- package/src/lib/simplify/call.js +6 -92
- package/src/lib/simplify/product.js +7 -4
- package/src/lib/simplify/round.js +4 -4
- package/src/lib/simplify/sum.js +22 -10
- package/src/lib/simplify.js +20 -5
- package/src/reduce.js +46 -118
- package/types/index.d.ts +4 -0
- package/types/lib/analyze.d.ts +18 -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/convertUnits.d.ts +8 -1
- package/types/lib/functions.d.ts +40 -0
- package/types/lib/limits.d.ts +8 -0
- package/types/lib/node.d.ts +31 -9
- package/types/lib/opaque.d.ts +9 -0
- package/types/lib/parser.d.ts +47 -49
- 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/bucket.d.ts +2 -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 -139
- package/types/lib/tokenizer.d.ts +0 -32
package/src/lib/serialize.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
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 {
|
|
5
|
+
import { serializeComponents } from './opaque.js';
|
|
6
|
+
import { checkCalculationDepth } from './limits.js';
|
|
7
|
+
import { isCalculationFunction } from './functions.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* @typedef {import('./node.js').Node} Node
|
|
@@ -12,260 +14,377 @@ import { num, dim } from './node.js';
|
|
|
12
14
|
* @typedef {object} SerializeOptions
|
|
13
15
|
* @property {number | false} [precision] Decimal places for numbers. `false` disables rounding. Default 5.
|
|
14
16
|
* @property {string} [calcName] Wrapper name to use when `calc()` is needed. Default `'calc'`.
|
|
15
|
-
* @property {boolean} [unwrapSingleNegativeNumber]
|
|
17
|
+
* @property {boolean} [unwrapSingleNegativeNumber] Deprecated alias for `unwrapSingleValue`.
|
|
18
|
+
* @property {boolean} [unwrapSingleValue] Serialize fully resolved finite scalar results without calculation syntax.
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
|
-
//
|
|
21
|
+
// The AST is canonical: sums and products are flat, so these precedence
|
|
22
|
+
// levels cover every binary expression
|
|
23
|
+
const SUM_PRECEDENCE = 1;
|
|
24
|
+
const PRODUCT_PRECEDENCE = 2;
|
|
25
|
+
const ATOMIC_PRECEDENCE = 3;
|
|
26
|
+
// Unary minus binds more tightly than a sum but has the same atomic boundary
|
|
27
|
+
// for deciding whether `-x` needs parentheses.
|
|
28
|
+
const UNARY_PRECEDENCE = ATOMIC_PRECEDENCE;
|
|
19
29
|
const NOISE_FLOOR = 1e-12;
|
|
20
30
|
|
|
21
31
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
32
|
+
* Decimal rounding with "round half away from zero" (e.g. 1.005 at precision 2 -> 1.01).
|
|
33
|
+
*
|
|
34
|
+
* Binary floating-point (IEEE-754) cannot represent many decimal fractions exactly
|
|
35
|
+
* (e.g. 1.005 is binary 1.004999999999999893...), causing arithmetic formulas like
|
|
36
|
+
* `Math.round(v * 100) / 100` to round down to 1.00. Exponential notation string shifting
|
|
37
|
+
* (`1.005e2` -> `100.5`) lets the ECMAScript string-to-number parser read the exact
|
|
38
|
+
* intended decimal value before rounding.
|
|
26
39
|
*
|
|
27
40
|
* @param {number} v
|
|
28
41
|
* @param {number | false} prec
|
|
29
42
|
* @return {number}
|
|
30
43
|
*/
|
|
31
44
|
function round(v, prec) {
|
|
32
|
-
if (prec === false)
|
|
33
|
-
|
|
45
|
+
if (prec === false || !Number.isFinite(v)) return v;
|
|
46
|
+
if (Object.is(v, -0) || v === 0) return v;
|
|
47
|
+
const abs = Math.abs(v);
|
|
48
|
+
// Numbers >= MAX_SAFE_INTEGER (2^53 - 1) cannot represent fractional values, and
|
|
49
|
+
// integers already have 0 fractional places. Bypassing them avoids float drift.
|
|
50
|
+
if (abs >= Number.MAX_SAFE_INTEGER || Number.isInteger(v)) return v;
|
|
51
|
+
|
|
52
|
+
// Clamp precision to [0, 100] integer to prevent NaN from fractional precisions
|
|
53
|
+
// or exponent overflows into Infinity/NaN (e.g. exponent + prec > 308).
|
|
54
|
+
const p = Math.min(100, Math.max(0, Math.trunc(prec)));
|
|
55
|
+
const sign = v < 0 ? -1 : 1;
|
|
56
|
+
let rounded;
|
|
57
|
+
|
|
58
|
+
if (p === 0) {
|
|
59
|
+
// Fast path: rounding to integer with "round half away from zero".
|
|
60
|
+
rounded = sign * Math.round(abs);
|
|
61
|
+
} else {
|
|
62
|
+
// Avoid .split('e') allocations: for numbers between 1e-6 and MAX_SAFE_INTEGER,
|
|
63
|
+
// String(abs) never contains exponential notation ('e').
|
|
64
|
+
const absStr = String(abs);
|
|
65
|
+
const eIdx = absStr.indexOf('e');
|
|
66
|
+
let shifted;
|
|
67
|
+
if (eIdx === -1) {
|
|
68
|
+
shifted = Math.round(Number(absStr + 'e' + p));
|
|
69
|
+
} else {
|
|
70
|
+
const mantissa = absStr.slice(0, eIdx);
|
|
71
|
+
const exponent = Number(absStr.slice(eIdx + 1));
|
|
72
|
+
shifted = Math.round(Number(mantissa + 'e' + (exponent + p)));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// shifted is an integer. It only contains exponential notation ('e') if >= 1e21.
|
|
76
|
+
if (shifted >= 1e21) {
|
|
77
|
+
const shiftedStr = String(shifted);
|
|
78
|
+
const seIdx = shiftedStr.indexOf('e');
|
|
79
|
+
const sMantissa = shiftedStr.slice(0, seIdx);
|
|
80
|
+
const sExponent = Number(shiftedStr.slice(seIdx + 1));
|
|
81
|
+
rounded = sign * Number(sMantissa + 'e' + (sExponent - p));
|
|
82
|
+
} else {
|
|
83
|
+
rounded = sign * Number(shifted + 'e-' + p);
|
|
84
|
+
}
|
|
34
85
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return Number(v.toPrecision(Math.max(
|
|
86
|
+
|
|
87
|
+
// Preserve non-zero values smaller than precision (e.g. 1/1000000) from collapsing
|
|
88
|
+
// to zero, while still snapping true floating-point dust (< 1e-12) to zero.
|
|
89
|
+
if (rounded === 0 && abs > NOISE_FLOOR) {
|
|
90
|
+
return Number(v.toPrecision(Math.max(p, 1)));
|
|
40
91
|
}
|
|
41
92
|
return rounded;
|
|
42
93
|
}
|
|
43
94
|
|
|
44
95
|
// §10.13 / §10.7.2: Infinity/NaN serialize as canonical keywords.
|
|
45
|
-
/**
|
|
46
|
-
* @param {number} v
|
|
47
|
-
* @return {boolean}
|
|
48
|
-
*/
|
|
96
|
+
/** @param {number} v @return {boolean} */
|
|
49
97
|
function isDegenerate(v) {
|
|
50
98
|
return !Number.isFinite(v) || Number.isNaN(v);
|
|
51
99
|
}
|
|
52
100
|
|
|
53
|
-
/**
|
|
54
|
-
* @param {number} v
|
|
55
|
-
* @return {string}
|
|
56
|
-
*/
|
|
101
|
+
/** @param {number} v @return {string} */
|
|
57
102
|
function degenerateKeyword(v) {
|
|
58
|
-
if (Number.isNaN(v))
|
|
59
|
-
return 'NaN';
|
|
60
|
-
}
|
|
103
|
+
if (Number.isNaN(v)) return 'NaN';
|
|
61
104
|
return v > 0 ? 'infinity' : '-infinity';
|
|
62
105
|
}
|
|
63
106
|
|
|
64
|
-
/**
|
|
65
|
-
* Serialize a finite CSS number. CSS numbers may omit the zero before a
|
|
66
|
-
* fractional value between -1 and 1 (`.5`, `-.5`). Scientific notation is
|
|
67
|
-
* left untouched because it already has no leading zero to remove.
|
|
68
|
-
*
|
|
69
|
-
* @param {number} v
|
|
70
|
-
* @return {string}
|
|
71
|
-
*/
|
|
107
|
+
/** @param {number} v @return {string} */
|
|
72
108
|
function serializeNumber(v) {
|
|
109
|
+
if (Object.is(v, -0)) return '0';
|
|
73
110
|
const text = String(v);
|
|
74
|
-
if (text.startsWith('0.'))
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
if (text.startsWith('-0.')) {
|
|
78
|
-
return `-${text.slice(2)}`;
|
|
79
|
-
}
|
|
111
|
+
if (text.startsWith('0.')) return text.slice(1);
|
|
112
|
+
if (text.startsWith('-0.')) return `-${text.slice(2)}`;
|
|
80
113
|
return text;
|
|
81
114
|
}
|
|
82
115
|
|
|
116
|
+
/** @param {SerializeOptions} opts @return {'standard' | 'unwrap-all'} */
|
|
117
|
+
function normalizeScalarPolicy(opts) {
|
|
118
|
+
return opts.unwrapSingleValue || opts.unwrapSingleNegativeNumber
|
|
119
|
+
? 'unwrap-all'
|
|
120
|
+
: 'standard';
|
|
121
|
+
}
|
|
122
|
+
|
|
83
123
|
/**
|
|
84
|
-
* Round and serialize a finite scalar once so callers can use the same value
|
|
85
|
-
* to decide its syntactic context and render its text.
|
|
86
|
-
*
|
|
87
124
|
* @param {import('./node.js').Num | import('./node.js').Dim} node
|
|
88
|
-
* @param {number | false}
|
|
89
|
-
* @
|
|
125
|
+
* @param {number | false} precision
|
|
126
|
+
* @param {number} [value]
|
|
127
|
+
* @return {number}
|
|
90
128
|
*/
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
const text = `${serializeNumber(value)}${node.type === 'Dim' ? node.unit : ''}`;
|
|
94
|
-
return { value, text };
|
|
129
|
+
function roundedScalarValue(node, precision, value) {
|
|
130
|
+
return round(value ?? node.value, precision);
|
|
95
131
|
}
|
|
96
132
|
|
|
97
133
|
/**
|
|
98
|
-
* @param {
|
|
99
|
-
* @param {
|
|
100
|
-
* @
|
|
134
|
+
* @param {import('./node.js').Num | import('./node.js').Dim} node
|
|
135
|
+
* @param {string[]} buffer
|
|
136
|
+
* @param {number} value
|
|
137
|
+
* @return {void}
|
|
101
138
|
*/
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// §10.13: top-level Infinity/NaN wrap in calc(); dim degenerates carry
|
|
107
|
-
// the unit as `<keyword> * 1<unit>` so the result keeps its type.
|
|
108
|
-
if (node.type === 'Num' && isDegenerate(node.value)) {
|
|
109
|
-
return `${calcName}(${degenerateKeyword(node.value)})`;
|
|
110
|
-
}
|
|
111
|
-
if (node.type === 'Dim' && isDegenerate(node.value)) {
|
|
112
|
-
return `${calcName}(${degenerateKeyword(node.value)} * 1${node.unit})`;
|
|
139
|
+
function emitRoundedScalar(node, buffer, value) {
|
|
140
|
+
buffer.push(serializeNumber(value));
|
|
141
|
+
if (node.type === 'Dim') {
|
|
142
|
+
buffer.push(node.rawUnit ?? node.unit);
|
|
113
143
|
}
|
|
144
|
+
}
|
|
114
145
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
146
|
+
/**
|
|
147
|
+
* @param {import('./node.js').Num | import('./node.js').Dim} node
|
|
148
|
+
* @param {ReturnType<typeof makeContext>} session
|
|
149
|
+
* @param {number} [value]
|
|
150
|
+
* @return {number}
|
|
151
|
+
*/
|
|
152
|
+
function emitFiniteScalar(node, session, value) {
|
|
153
|
+
const rounded = roundedScalarValue(node, session.precision, value);
|
|
154
|
+
emitRoundedScalar(node, session.buffer, rounded);
|
|
155
|
+
return rounded;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {import('./node.js').Num | import('./node.js').Dim} node
|
|
160
|
+
* @param {ReturnType<typeof makeContext>} session
|
|
161
|
+
* @param {number} [value]
|
|
162
|
+
* @return {void}
|
|
163
|
+
*/
|
|
164
|
+
function emitScalar(node, session, value) {
|
|
165
|
+
const buffer = session.buffer;
|
|
166
|
+
if (Object.is(node.value, -0)) emitSignedZero(buffer, node);
|
|
167
|
+
else if (isDegenerate(node.value)) {
|
|
168
|
+
if (node.type === 'Dim') {
|
|
169
|
+
buffer.push(
|
|
170
|
+
'calc(',
|
|
171
|
+
degenerateKeyword(node.value),
|
|
172
|
+
' * 1',
|
|
173
|
+
node.rawUnit ?? node.unit,
|
|
174
|
+
')'
|
|
175
|
+
);
|
|
176
|
+
} else {
|
|
177
|
+
buffer.push(degenerateKeyword(node.value));
|
|
126
178
|
}
|
|
179
|
+
} else emitFiniteScalar(node, session, value);
|
|
180
|
+
}
|
|
127
181
|
|
|
128
|
-
|
|
129
|
-
|
|
182
|
+
/**
|
|
183
|
+
* @param {string[]} buffer
|
|
184
|
+
* @param {import('./node.js').Num | import('./node.js').Dim} node
|
|
185
|
+
* @return {void}
|
|
186
|
+
*/
|
|
187
|
+
function emitSignedZero(buffer, node) {
|
|
188
|
+
const unit = node.type === 'Dim' ? (node.rawUnit ?? node.unit) : '';
|
|
189
|
+
buffer.push('calc(-1 * 0', unit, ')');
|
|
190
|
+
}
|
|
130
191
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
node.type === 'Sum' &&
|
|
136
|
-
node.grouped &&
|
|
137
|
-
node.terms.length > 1 &&
|
|
138
|
-
displaySign(node.terms[0]).sign === -1
|
|
139
|
-
) {
|
|
140
|
-
const invertedTerms = node.terms.map((t) => ({
|
|
141
|
-
sign: /** @type {1 | -1} */ (-t.sign),
|
|
142
|
-
node: t.node,
|
|
143
|
-
}));
|
|
144
|
-
return `${calcName}(-(${serializeSumTerms(invertedTerms, prec)}))`;
|
|
145
|
-
}
|
|
192
|
+
/** @param {Node} node @return {node is import('./node.js').Num | import('./node.js').Dim} */
|
|
193
|
+
function isScalar(node) {
|
|
194
|
+
return node.type === 'Num' || node.type === 'Dim';
|
|
195
|
+
}
|
|
146
196
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
197
|
+
/** @param {Node} node @return {node is import('./node.js').Num | import('./node.js').Dim} */
|
|
198
|
+
function isSignedZero(node) {
|
|
199
|
+
return isScalar(node) ? Object.is(node.value, -0) : false;
|
|
200
|
+
}
|
|
150
201
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (node.type === 'Sum'
|
|
154
|
-
|
|
155
|
-
|
|
202
|
+
/** @param {Node} node @return {number} */
|
|
203
|
+
function precedence(node) {
|
|
204
|
+
if (node.type === 'Sum') return SUM_PRECEDENCE;
|
|
205
|
+
if (node.type === 'Product') return PRODUCT_PRECEDENCE;
|
|
206
|
+
return ATOMIC_PRECEDENCE;
|
|
207
|
+
}
|
|
156
208
|
|
|
157
|
-
|
|
209
|
+
/**
|
|
210
|
+
* @param {Node} node
|
|
211
|
+
* @param {number} parentPrecedence
|
|
212
|
+
* @param {boolean} groupedRequired
|
|
213
|
+
* @return {boolean}
|
|
214
|
+
*/
|
|
215
|
+
function needsParentheses(node, parentPrecedence, groupedRequired) {
|
|
216
|
+
return (
|
|
217
|
+
precedence(node) < parentPrecedence ||
|
|
218
|
+
(node.type === 'Sum' && node.grouped === true && groupedRequired === true)
|
|
219
|
+
);
|
|
158
220
|
}
|
|
159
221
|
|
|
160
|
-
|
|
222
|
+
/**
|
|
223
|
+
* @param {Node} node
|
|
224
|
+
* @param {ReturnType<typeof makeContext>} session
|
|
225
|
+
* @param {number} [parentPrecedence]
|
|
226
|
+
* @param {boolean} [groupedRequired]
|
|
227
|
+
* @param {number} [scalarValueOverride]
|
|
228
|
+
* @return {void}
|
|
229
|
+
*/
|
|
230
|
+
function emitNode(
|
|
231
|
+
node,
|
|
232
|
+
session,
|
|
233
|
+
parentPrecedence = 0,
|
|
234
|
+
groupedRequired = false,
|
|
235
|
+
scalarValueOverride
|
|
236
|
+
) {
|
|
237
|
+
const parenthesized = needsParentheses(
|
|
238
|
+
node,
|
|
239
|
+
parentPrecedence,
|
|
240
|
+
groupedRequired
|
|
241
|
+
);
|
|
242
|
+
if (parenthesized) session.buffer.push('(');
|
|
243
|
+
emitNodeBody(node, session, scalarValueOverride);
|
|
244
|
+
if (parenthesized) session.buffer.push(')');
|
|
245
|
+
}
|
|
161
246
|
|
|
162
247
|
/**
|
|
163
248
|
* @param {Node} node
|
|
164
|
-
* @param {
|
|
165
|
-
* @
|
|
249
|
+
* @param {ReturnType<typeof makeContext>} session
|
|
250
|
+
* @param {number} [scalarValueOverride]
|
|
251
|
+
* @return {void}
|
|
166
252
|
*/
|
|
167
|
-
function
|
|
253
|
+
function emitNodeBody(node, session, scalarValueOverride) {
|
|
254
|
+
const buffer = session.buffer;
|
|
168
255
|
switch (node.type) {
|
|
169
256
|
case 'Num':
|
|
170
|
-
if (isDegenerate(node.value)) {
|
|
171
|
-
return degenerateKeyword(node.value);
|
|
172
|
-
}
|
|
173
|
-
return serializeScalar(node, prec).text;
|
|
174
257
|
case 'Dim':
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// parses back as one Dim factor. The bare form round-trips wrong
|
|
178
|
-
// inside a Product — `0 * Dim(Infinity, px)` would re-fold as NaN.
|
|
179
|
-
return `calc(${degenerateKeyword(node.value)} * 1${node.unit})`;
|
|
180
|
-
}
|
|
181
|
-
return serializeScalar(node, prec).text;
|
|
258
|
+
emitScalar(node, session, scalarValueOverride);
|
|
259
|
+
return;
|
|
182
260
|
case 'Ident':
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
261
|
+
buffer.push(node.rawName ?? node.name);
|
|
262
|
+
return;
|
|
263
|
+
case 'Call':
|
|
264
|
+
emitCall(node, session);
|
|
265
|
+
return;
|
|
266
|
+
case 'OpaqueCall':
|
|
267
|
+
emitOpaqueCall(node, session);
|
|
268
|
+
return;
|
|
188
269
|
case 'Sum':
|
|
189
|
-
|
|
270
|
+
emitSum(node, session);
|
|
271
|
+
return;
|
|
190
272
|
case 'Product':
|
|
191
|
-
|
|
273
|
+
emitProduct(node, session);
|
|
274
|
+
return;
|
|
192
275
|
}
|
|
193
276
|
}
|
|
194
277
|
|
|
195
278
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* tokenize as a `-calc` function.
|
|
201
|
-
* @param {{sign: 1 | -1, node: Node}} term
|
|
202
|
-
* @return {{sign: 1 | -1, magnitude: Node}}
|
|
279
|
+
* @param {import('./node.js').Call} node
|
|
280
|
+
* @param {ReturnType<typeof makeContext>} session
|
|
281
|
+
* @param {string} [callNameOverride]
|
|
282
|
+
* @return {void}
|
|
203
283
|
*/
|
|
204
|
-
function
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
};
|
|
284
|
+
function emitCall(node, session, callNameOverride) {
|
|
285
|
+
const buffer = session.buffer;
|
|
286
|
+
buffer.push(callNameOverride ?? node.rawName ?? node.name, '(');
|
|
287
|
+
for (let i = 0; i < node.args.length; i++) {
|
|
288
|
+
if (i > 0) buffer.push(', ');
|
|
289
|
+
emitNode(node.args[i], session);
|
|
211
290
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
291
|
+
buffer.push(')');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* @param {import('./node.js').OpaqueCall} node
|
|
296
|
+
* @param {ReturnType<typeof makeContext>} session
|
|
297
|
+
* @param {string} [callNameOverride]
|
|
298
|
+
* @return {void}
|
|
299
|
+
*/
|
|
300
|
+
function emitOpaqueCall(node, session, callNameOverride) {
|
|
301
|
+
const buffer = session.buffer;
|
|
302
|
+
buffer.push(callNameOverride ?? node.rawName ?? node.name, '(');
|
|
303
|
+
serializeComponents(node.components, buffer, (child, childBuffer) => {
|
|
304
|
+
emitNestedMathResult(child, session, childBuffer);
|
|
305
|
+
});
|
|
306
|
+
buffer.push(')');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @param {import('./node.js').SumTerm} term
|
|
311
|
+
* @param {1 | -1} multiplier
|
|
312
|
+
* @return {1 | -1}
|
|
313
|
+
* */
|
|
314
|
+
function termSign(term, multiplier) {
|
|
315
|
+
let sign = /** @type {1 | -1} */ (term.sign * multiplier);
|
|
316
|
+
if (
|
|
317
|
+
isScalar(term.node) &&
|
|
318
|
+
Number.isFinite(term.node.value) &&
|
|
319
|
+
term.node.value < 0
|
|
320
|
+
) {
|
|
321
|
+
sign = /** @type {1 | -1} */ (-sign);
|
|
322
|
+
}
|
|
323
|
+
return sign;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* @param {import('./node.js').SumTerm} term
|
|
328
|
+
* @param {ReturnType<typeof makeContext>} session
|
|
329
|
+
* @param {1 | -1} sign
|
|
330
|
+
* @param {number | undefined} scalarValueOverride
|
|
331
|
+
* @return {void}
|
|
332
|
+
*/
|
|
333
|
+
function emitSumTerm(term, session, sign, scalarValueOverride) {
|
|
334
|
+
if (sign === 1) {
|
|
335
|
+
emitNode(term.node, session, SUM_PRECEDENCE, true, scalarValueOverride);
|
|
336
|
+
} else {
|
|
337
|
+
emitLeadingNeg(term.node, session, scalarValueOverride);
|
|
217
338
|
}
|
|
218
|
-
return { sign, magnitude: node };
|
|
219
339
|
}
|
|
220
340
|
|
|
221
341
|
/**
|
|
222
342
|
* @param {import('./node.js').SumTerm[]} terms
|
|
223
|
-
* @param {
|
|
224
|
-
* @
|
|
343
|
+
* @param {ReturnType<typeof makeContext>} session
|
|
344
|
+
* @param {1 | -1} [multiplier]
|
|
345
|
+
* @return {void}
|
|
225
346
|
*/
|
|
226
|
-
function
|
|
227
|
-
|
|
347
|
+
function emitSumTerms(terms, session, multiplier = 1) {
|
|
348
|
+
const buffer = session.buffer;
|
|
228
349
|
for (let i = 0; i < terms.length; i++) {
|
|
229
|
-
const
|
|
350
|
+
const term = terms[i];
|
|
351
|
+
const termNode = term.node;
|
|
352
|
+
const scalar = isScalar(termNode);
|
|
353
|
+
const negativeScalar =
|
|
354
|
+
scalar && Number.isFinite(termNode.value) && termNode.value < 0;
|
|
355
|
+
let sign = /** @type {1 | -1} */ (term.sign * multiplier);
|
|
356
|
+
if (negativeScalar) sign = /** @type {1 | -1} */ (-sign);
|
|
357
|
+
const scalarValueOverride = negativeScalar ? -termNode.value : undefined;
|
|
230
358
|
if (i === 0) {
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
359
|
+
if (scalar) {
|
|
360
|
+
if (sign === -1) buffer.push('-');
|
|
361
|
+
emitScalar(termNode, session, scalarValueOverride);
|
|
362
|
+
} else {
|
|
363
|
+
emitSumTerm(term, session, sign, scalarValueOverride);
|
|
235
364
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
buffer.push(sign === 1 ? ' + ' : ' - ');
|
|
368
|
+
if (scalar) {
|
|
369
|
+
emitScalar(termNode, session, scalarValueOverride);
|
|
240
370
|
} else {
|
|
241
|
-
|
|
242
|
-
let body = serializeExpr(magnitude, prec);
|
|
243
|
-
if (magnitude.type === 'Sum' && magnitude.grouped) {
|
|
244
|
-
body = `(${body})`;
|
|
245
|
-
}
|
|
246
|
-
out += sign === 1 ? ` + ${body}` : ` - ${body}`;
|
|
371
|
+
emitNode(termNode, session, SUM_PRECEDENCE, true);
|
|
247
372
|
}
|
|
248
373
|
}
|
|
249
|
-
return out;
|
|
250
374
|
}
|
|
251
375
|
|
|
252
|
-
/**
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
* @return {string}
|
|
256
|
-
*/
|
|
257
|
-
function serializeSum(sum, prec) {
|
|
258
|
-
return serializeSumTerms(sum.terms, prec);
|
|
376
|
+
/** @param {Sum} sum @param {ReturnType<typeof makeContext>} session @return {void} */
|
|
377
|
+
function emitSum(sum, session) {
|
|
378
|
+
emitSumTerms(sum.terms, session);
|
|
259
379
|
}
|
|
260
380
|
|
|
261
381
|
/**
|
|
262
|
-
* Fold a leading negation into a finite leading Num if there is one
|
|
263
|
-
* (`-(0.5 * x)` → `-0.5 * x`); else use `-(…)` for Sum/Product or `-x`.
|
|
264
382
|
* @param {Node} node
|
|
265
|
-
* @param {
|
|
266
|
-
* @
|
|
383
|
+
* @param {ReturnType<typeof makeContext>} session
|
|
384
|
+
* @param {number} [scalarValueOverride]
|
|
385
|
+
* @return {void}
|
|
267
386
|
*/
|
|
268
|
-
function
|
|
387
|
+
function emitLeadingNeg(node, session, scalarValueOverride) {
|
|
269
388
|
if (
|
|
270
389
|
node.type === 'Product' &&
|
|
271
390
|
node.factors.length > 0 &&
|
|
@@ -275,54 +394,302 @@ function serializeLeadingNeg(node, prec) {
|
|
|
275
394
|
node.factors[0].node.value !== 0
|
|
276
395
|
) {
|
|
277
396
|
const head = node.factors[0].node;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
// A coefficient of 1 is a no-op factor, matching mkProduct.
|
|
281
|
-
/** @type {ProductFactor[]} */
|
|
282
|
-
const negatedFactors =
|
|
283
|
-
negatedValue === 1
|
|
284
|
-
? rest
|
|
285
|
-
: [{ exponent: 1, node: num(negatedValue) }, ...rest];
|
|
286
|
-
return serializeFactors(negatedFactors, prec);
|
|
397
|
+
emitProductFactors(node.factors, session, 1, -head.value, head);
|
|
398
|
+
return;
|
|
287
399
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
400
|
+
session.buffer.push('-');
|
|
401
|
+
emitNode(
|
|
402
|
+
node,
|
|
403
|
+
session,
|
|
404
|
+
UNARY_PRECEDENCE,
|
|
405
|
+
false,
|
|
406
|
+
isScalar(node) ? scalarValueOverride : undefined
|
|
407
|
+
);
|
|
292
408
|
}
|
|
293
409
|
|
|
294
410
|
/**
|
|
295
411
|
* @param {ProductFactor[]} factors
|
|
296
|
-
* @param {
|
|
412
|
+
* @param {ReturnType<typeof makeContext>} session
|
|
413
|
+
* @param {number} [start]
|
|
414
|
+
* @param {number} [coefficientValue]
|
|
415
|
+
* @param {import('./node.js').Num} [coefficientNode]
|
|
416
|
+
* @return {void}
|
|
417
|
+
*/
|
|
418
|
+
function emitProductFactors(
|
|
419
|
+
factors,
|
|
420
|
+
session,
|
|
421
|
+
start = 0,
|
|
422
|
+
coefficientValue,
|
|
423
|
+
coefficientNode
|
|
424
|
+
) {
|
|
425
|
+
const buffer = session.buffer;
|
|
426
|
+
let first = true;
|
|
427
|
+
if (coefficientValue !== undefined && coefficientValue !== 1) {
|
|
428
|
+
emitScalar(
|
|
429
|
+
/** @type {import('./node.js').Num} */ (coefficientNode),
|
|
430
|
+
session,
|
|
431
|
+
coefficientValue
|
|
432
|
+
);
|
|
433
|
+
first = false;
|
|
434
|
+
}
|
|
435
|
+
for (let i = start; i < factors.length; i++) {
|
|
436
|
+
const factor = factors[i];
|
|
437
|
+
const factorNode = factor.node;
|
|
438
|
+
if (first) {
|
|
439
|
+
if (factor.exponent === -1) buffer.push('1 / ');
|
|
440
|
+
if (isScalar(factorNode)) emitScalar(factorNode, session);
|
|
441
|
+
else emitNode(factorNode, session, PRODUCT_PRECEDENCE);
|
|
442
|
+
first = false;
|
|
443
|
+
} else {
|
|
444
|
+
buffer.push(factor.exponent === 1 ? ' * ' : ' / ');
|
|
445
|
+
if (isScalar(factorNode)) emitScalar(factorNode, session);
|
|
446
|
+
else emitNode(factorNode, session, PRODUCT_PRECEDENCE);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** @param {Product} product @param {ReturnType<typeof makeContext>} session @return {void} */
|
|
452
|
+
function emitProduct(product, session) {
|
|
453
|
+
emitProductFactors(product.factors, session);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** @param {Node} node @param {ReturnType<typeof makeContext>} session @return {void} */
|
|
457
|
+
function emitRootExpr(node, session) {
|
|
458
|
+
if (
|
|
459
|
+
node.type === 'Sum' &&
|
|
460
|
+
node.grouped &&
|
|
461
|
+
node.terms.length > 1 &&
|
|
462
|
+
termSign(node.terms[0], 1) === -1
|
|
463
|
+
) {
|
|
464
|
+
session.buffer.push('-(');
|
|
465
|
+
emitSumTerms(node.terms, session, -1);
|
|
466
|
+
session.buffer.push(')');
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (node.type === 'Sum' && node.terms.length === 1) {
|
|
470
|
+
emitLeadingNeg(node.terms[0].node, session);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
emitNode(node, session);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* @param {Node} node
|
|
478
|
+
* @param {ReturnType<typeof makeContext>} session
|
|
479
|
+
* @param {string} wrapper
|
|
480
|
+
* @return {void}
|
|
481
|
+
*/
|
|
482
|
+
function emitMathResult(node, session, wrapper) {
|
|
483
|
+
if (isScalar(node)) {
|
|
484
|
+
const scalarValue = roundedScalarValue(node, session.precision);
|
|
485
|
+
const buffer = session.buffer;
|
|
486
|
+
if (isDegenerate(scalarValue)) {
|
|
487
|
+
buffer.push(wrapper, '(', degenerateKeyword(scalarValue));
|
|
488
|
+
if (node.type === 'Dim') buffer.push(' * 1', node.rawUnit ?? node.unit);
|
|
489
|
+
buffer.push(')');
|
|
490
|
+
} else if (session.scalarPolicy === 'standard') {
|
|
491
|
+
buffer.push(wrapper, '(');
|
|
492
|
+
emitRoundedScalar(node, buffer, scalarValue);
|
|
493
|
+
buffer.push(')');
|
|
494
|
+
} else {
|
|
495
|
+
emitRoundedScalar(node, buffer, scalarValue);
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (
|
|
500
|
+
node.type === 'Sum' &&
|
|
501
|
+
node.grouped &&
|
|
502
|
+
node.terms.length > 1 &&
|
|
503
|
+
termSign(node.terms[0], 1) === -1
|
|
504
|
+
) {
|
|
505
|
+
session.buffer.push(wrapper, '(-(');
|
|
506
|
+
emitSumTerms(node.terms, session, -1);
|
|
507
|
+
session.buffer.push('))');
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (
|
|
511
|
+
node.type === 'Ident' ||
|
|
512
|
+
node.type === 'Call' ||
|
|
513
|
+
node.type === 'OpaqueCall'
|
|
514
|
+
) {
|
|
515
|
+
emitNode(node, session);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (node.type === 'Sum' && node.terms.length === 1) {
|
|
519
|
+
session.buffer.push(wrapper, '(');
|
|
520
|
+
emitLeadingNeg(node.terms[0].node, session);
|
|
521
|
+
session.buffer.push(')');
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
session.buffer.push(wrapper, '(');
|
|
525
|
+
emitNode(node, session);
|
|
526
|
+
session.buffer.push(')');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Serialize a scalar result without allocating a render context or buffer.
|
|
531
|
+
* @param {import('./node.js').Num | import('./node.js').Dim} node
|
|
532
|
+
* @param {number | false} precision
|
|
533
|
+
* @param {'standard' | 'unwrap-all'} scalarPolicy
|
|
534
|
+
* @param {string} wrapper
|
|
297
535
|
* @return {string}
|
|
298
536
|
*/
|
|
299
|
-
function
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
537
|
+
function serializeScalarResult(node, precision, scalarPolicy, wrapper) {
|
|
538
|
+
const value = roundedScalarValue(node, precision);
|
|
539
|
+
const unit = node.type === 'Dim' ? (node.rawUnit ?? node.unit) : '';
|
|
540
|
+
if (isDegenerate(value)) {
|
|
541
|
+
return `${wrapper}(${degenerateKeyword(value)}${unit ? ` * 1${unit}` : ''})`;
|
|
542
|
+
}
|
|
543
|
+
const scalar = serializeNumber(value) + unit;
|
|
544
|
+
return scalarPolicy === 'standard' ? `${wrapper}(${scalar})` : scalar;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* @param {Node} node
|
|
549
|
+
* @param {ReturnType<typeof makeContext>} session
|
|
550
|
+
* @param {string[]} [buffer]
|
|
551
|
+
* @return {void}
|
|
552
|
+
*/
|
|
553
|
+
function emitNestedMathResult(node, session, buffer = session.buffer) {
|
|
554
|
+
if (isSignedZero(node)) {
|
|
555
|
+
emitSignedZero(buffer, node);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
emitMathResult(node, session, 'calc');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* @param {SerializeOptions} opts
|
|
563
|
+
* @return {{buffer: string[], precision: number | false, scalarPolicy: 'standard' | 'unwrap-all'}}
|
|
564
|
+
*/
|
|
565
|
+
function makeContext(opts) {
|
|
566
|
+
return {
|
|
567
|
+
buffer: [],
|
|
568
|
+
precision: opts.precision ?? 5,
|
|
569
|
+
scalarPolicy: normalizeScalarPolicy(opts),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* @param {Node} node
|
|
575
|
+
* @param {SerializeOptions} opts
|
|
576
|
+
* @return {{kind: 'math', node: Node, session: ReturnType<typeof makeContext>, wrapper: string}}
|
|
577
|
+
*/
|
|
578
|
+
function planSerialize(node, opts) {
|
|
579
|
+
return {
|
|
580
|
+
kind: 'math',
|
|
581
|
+
node,
|
|
582
|
+
session: makeContext(opts),
|
|
583
|
+
wrapper: opts.calcName ?? 'calc',
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* @param {{tree: Node, status: 'resolved' | 'unresolved', rootName: string, rootSpelling: string, calculation?: boolean, original?: string}} result
|
|
589
|
+
* @param {SerializeOptions} opts
|
|
590
|
+
* @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}}
|
|
591
|
+
*/
|
|
592
|
+
function planSerializeResult(result, opts) {
|
|
593
|
+
const isCalc = result.calculation ?? isCalculationFunction(result.rootName);
|
|
594
|
+
const normalizedRootName =
|
|
595
|
+
result.calculation === undefined
|
|
596
|
+
? result.rootName.toLowerCase()
|
|
597
|
+
: result.rootName;
|
|
598
|
+
const wrapper = isCalc
|
|
599
|
+
? result.rootSpelling || opts.calcName || 'calc'
|
|
600
|
+
: 'calc';
|
|
601
|
+
const session = makeContext(opts);
|
|
602
|
+
|
|
603
|
+
if (!isCalc && result.status === 'unresolved') {
|
|
604
|
+
if (
|
|
605
|
+
(result.tree.type === 'Call' || result.tree.type === 'OpaqueCall') &&
|
|
606
|
+
result.tree.name.toLowerCase() === normalizedRootName
|
|
607
|
+
) {
|
|
608
|
+
return {
|
|
609
|
+
kind: 'root-call',
|
|
610
|
+
node: result.tree,
|
|
611
|
+
session,
|
|
612
|
+
callNameOverride: result.rootSpelling,
|
|
613
|
+
};
|
|
308
614
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
615
|
+
return { kind: 'original', text: result.original ?? '' };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (session.scalarPolicy === 'standard') {
|
|
619
|
+
if (isScalar(result.tree))
|
|
620
|
+
return { kind: 'math', node: result.tree, session, wrapper };
|
|
621
|
+
return { kind: 'wrapped-expr', node: result.tree, session, wrapper };
|
|
622
|
+
}
|
|
623
|
+
return { kind: 'math', node: result.tree, session, wrapper };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* @param {ReturnType<typeof planSerialize> | ReturnType<typeof planSerializeResult>} renderSpec
|
|
628
|
+
* @return {string}
|
|
629
|
+
* */
|
|
630
|
+
function emitOutput(renderSpec) {
|
|
631
|
+
if (renderSpec.kind === 'original') return renderSpec.text;
|
|
632
|
+
const { session } = renderSpec;
|
|
633
|
+
if (renderSpec.kind === 'root-call') {
|
|
634
|
+
if (renderSpec.node.type === 'Call') {
|
|
635
|
+
emitCall(renderSpec.node, session, renderSpec.callNameOverride);
|
|
312
636
|
} else {
|
|
313
|
-
|
|
637
|
+
emitOpaqueCall(
|
|
638
|
+
/** @type {import('./node.js').OpaqueCall} */ (renderSpec.node),
|
|
639
|
+
session,
|
|
640
|
+
renderSpec.callNameOverride
|
|
641
|
+
);
|
|
314
642
|
}
|
|
643
|
+
} else if (renderSpec.kind === 'wrapped-expr') {
|
|
644
|
+
session.buffer.push(renderSpec.wrapper, '(');
|
|
645
|
+
emitRootExpr(renderSpec.node, session);
|
|
646
|
+
session.buffer.push(')');
|
|
647
|
+
} else {
|
|
648
|
+
emitMathResult(renderSpec.node, session, renderSpec.wrapper);
|
|
315
649
|
}
|
|
316
|
-
return
|
|
650
|
+
return session.buffer.join('');
|
|
317
651
|
}
|
|
318
652
|
|
|
319
653
|
/**
|
|
320
|
-
* @param {
|
|
321
|
-
* @param {
|
|
654
|
+
* @param {Node} node
|
|
655
|
+
* @param {SerializeOptions} [opts]
|
|
322
656
|
* @return {string}
|
|
323
657
|
*/
|
|
324
|
-
function
|
|
325
|
-
|
|
658
|
+
function serialize(node, opts = {}) {
|
|
659
|
+
if (isScalar(node)) {
|
|
660
|
+
return serializeScalarResult(
|
|
661
|
+
node,
|
|
662
|
+
opts.precision ?? 5,
|
|
663
|
+
normalizeScalarPolicy(opts),
|
|
664
|
+
opts.calcName ?? 'calc'
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
checkCalculationDepth(node);
|
|
668
|
+
return emitOutput(planSerialize(node, opts));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* @param {{tree: Node, status: 'resolved' | 'unresolved', rootName: string, rootSpelling: string, calculation?: boolean, original?: string}} result
|
|
673
|
+
* @param {SerializeOptions} [opts]
|
|
674
|
+
* @return {string}
|
|
675
|
+
*/
|
|
676
|
+
function serializeResult(result, opts = {}) {
|
|
677
|
+
const node = result.tree;
|
|
678
|
+
if (isScalar(node)) {
|
|
679
|
+
const isCalc = result.calculation ?? isCalculationFunction(result.rootName);
|
|
680
|
+
if (!isCalc && result.status === 'unresolved') return result.original ?? '';
|
|
681
|
+
const wrapper = isCalc
|
|
682
|
+
? result.rootSpelling || opts.calcName || 'calc'
|
|
683
|
+
: 'calc';
|
|
684
|
+
return serializeScalarResult(
|
|
685
|
+
node,
|
|
686
|
+
opts.precision ?? 5,
|
|
687
|
+
normalizeScalarPolicy(opts),
|
|
688
|
+
wrapper
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
checkCalculationDepth(result.tree);
|
|
692
|
+
return emitOutput(planSerializeResult(result, opts));
|
|
326
693
|
}
|
|
327
694
|
|
|
328
|
-
export { serialize };
|
|
695
|
+
export { serialize, serializeResult };
|