postcss-calc 11.2.0 → 11.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -0
- package/README.md +44 -16
- package/package.json +13 -13
- package/src/index.js +2 -1
- package/src/lib/analyze.js +81 -43
- package/src/lib/compile.js +1 -1
- package/src/lib/functions.js +40 -10
- package/src/lib/node.js +62 -5
- package/src/lib/parser/opaque.js +151 -0
- package/src/lib/parser/tokens.js +242 -0
- package/src/lib/parser.js +27 -382
- package/src/lib/serialize/expression.js +376 -0
- package/src/lib/serialize/precision.js +244 -0
- package/src/lib/serialize.js +24 -517
- package/src/lib/simplify/bucket.js +22 -5
- package/src/lib/simplify/cancel.js +35 -11
- package/src/lib/simplify/clamp.js +2 -2
- package/src/lib/simplify/exact.js +18 -0
- package/src/lib/simplify/fold.js +15 -1
- package/src/lib/simplify/min-max.js +2 -2
- package/src/lib/simplify/product.js +263 -48
- package/src/lib/simplify/sum.js +6 -2
- package/src/lib/simplify.js +22 -5
- package/src/reduce.js +2 -1
- package/types/lib/analyze.d.ts +27 -0
- package/types/lib/functions.d.ts +19 -1
- package/types/lib/node.d.ts +17 -2
- package/types/lib/parser/opaque.d.ts +12 -0
- package/types/lib/parser/tokens.d.ts +83 -0
- package/types/lib/parser.d.ts +4 -44
- package/types/lib/serialize/expression.d.ts +95 -0
- package/types/lib/serialize/precision.d.ts +101 -0
- package/types/lib/simplify/bucket.d.ts +4 -1
- package/types/lib/simplify/cancel.d.ts +11 -7
- package/types/lib/simplify/exact.d.ts +9 -0
- package/types/lib/simplify/fold.d.ts +14 -1
- package/types/lib/simplify/product.d.ts +3 -7
- package/types/lib/simplify/sum.d.ts +2 -1
- package/types/lib/simplify.d.ts +3 -8
- package/src/lib/calculation-type.js +0 -25
package/src/lib/serialize.js
CHANGED
|
@@ -2,9 +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';
|
|
7
|
+
import {
|
|
8
|
+
isScalar,
|
|
9
|
+
serializeNumber,
|
|
10
|
+
roundedScalarValue,
|
|
11
|
+
isDegenerate,
|
|
12
|
+
degenerateKeyword,
|
|
13
|
+
} from './serialize/precision.js';
|
|
14
|
+
import {
|
|
15
|
+
emitCall,
|
|
16
|
+
emitOpaqueCall,
|
|
17
|
+
emitMathResult,
|
|
18
|
+
emitNode,
|
|
19
|
+
emitLeadingNeg,
|
|
20
|
+
} from './serialize/expression.js';
|
|
8
21
|
|
|
9
22
|
/**
|
|
10
23
|
* @typedef {import('./node.js').Node} Node
|
|
@@ -18,101 +31,6 @@ import { isCalculationFunction } from './functions.js';
|
|
|
18
31
|
* @property {boolean} [unwrapSingleValue] Serialize fully resolved finite scalar results without calculation syntax.
|
|
19
32
|
*/
|
|
20
33
|
|
|
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;
|
|
29
|
-
const NOISE_FLOOR = 1e-12;
|
|
30
|
-
|
|
31
|
-
/**
|
|
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.
|
|
39
|
-
*
|
|
40
|
-
* @param {number} v
|
|
41
|
-
* @param {number | false} prec
|
|
42
|
-
* @return {number}
|
|
43
|
-
*/
|
|
44
|
-
function round(v, prec) {
|
|
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
|
-
}
|
|
85
|
-
}
|
|
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)));
|
|
91
|
-
}
|
|
92
|
-
return rounded;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// §10.13 / §10.7.2: Infinity/NaN serialize as canonical keywords.
|
|
96
|
-
/** @param {number} v @return {boolean} */
|
|
97
|
-
function isDegenerate(v) {
|
|
98
|
-
return !Number.isFinite(v) || Number.isNaN(v);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** @param {number} v @return {string} */
|
|
102
|
-
function degenerateKeyword(v) {
|
|
103
|
-
if (Number.isNaN(v)) return 'NaN';
|
|
104
|
-
return v > 0 ? 'infinity' : '-infinity';
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** @param {number} v @return {string} */
|
|
108
|
-
function serializeNumber(v) {
|
|
109
|
-
if (Object.is(v, -0)) return '0';
|
|
110
|
-
const text = String(v);
|
|
111
|
-
if (text.startsWith('0.')) return text.slice(1);
|
|
112
|
-
if (text.startsWith('-0.')) return `-${text.slice(2)}`;
|
|
113
|
-
return text;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
34
|
/** @param {SerializeOptions} opts @return {'standard' | 'unwrap-all'} */
|
|
117
35
|
function normalizeScalarPolicy(opts) {
|
|
118
36
|
return opts.unwrapSingleValue || opts.unwrapSingleNegativeNumber
|
|
@@ -120,412 +38,6 @@ function normalizeScalarPolicy(opts) {
|
|
|
120
38
|
: 'standard';
|
|
121
39
|
}
|
|
122
40
|
|
|
123
|
-
/**
|
|
124
|
-
* @param {import('./node.js').Num | import('./node.js').Dim} node
|
|
125
|
-
* @param {number | false} precision
|
|
126
|
-
* @param {number} [value]
|
|
127
|
-
* @return {number}
|
|
128
|
-
*/
|
|
129
|
-
function roundedScalarValue(node, precision, value) {
|
|
130
|
-
return round(value ?? node.value, precision);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* @param {import('./node.js').Num | import('./node.js').Dim} node
|
|
135
|
-
* @param {string[]} buffer
|
|
136
|
-
* @param {number} value
|
|
137
|
-
* @return {void}
|
|
138
|
-
*/
|
|
139
|
-
function emitRoundedScalar(node, buffer, value) {
|
|
140
|
-
buffer.push(serializeNumber(value));
|
|
141
|
-
if (node.type === 'Dim') {
|
|
142
|
-
buffer.push(node.rawUnit ?? node.unit);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
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));
|
|
178
|
-
}
|
|
179
|
-
} else emitFiniteScalar(node, session, value);
|
|
180
|
-
}
|
|
181
|
-
|
|
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
|
-
}
|
|
191
|
-
|
|
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
|
-
}
|
|
196
|
-
|
|
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
|
-
}
|
|
201
|
-
|
|
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
|
-
}
|
|
208
|
-
|
|
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
|
-
);
|
|
220
|
-
}
|
|
221
|
-
|
|
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
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* @param {Node} node
|
|
249
|
-
* @param {ReturnType<typeof makeContext>} session
|
|
250
|
-
* @param {number} [scalarValueOverride]
|
|
251
|
-
* @return {void}
|
|
252
|
-
*/
|
|
253
|
-
function emitNodeBody(node, session, scalarValueOverride) {
|
|
254
|
-
const buffer = session.buffer;
|
|
255
|
-
switch (node.type) {
|
|
256
|
-
case 'Num':
|
|
257
|
-
case 'Dim':
|
|
258
|
-
emitScalar(node, session, scalarValueOverride);
|
|
259
|
-
return;
|
|
260
|
-
case 'Ident':
|
|
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;
|
|
269
|
-
case 'Sum':
|
|
270
|
-
emitSum(node, session);
|
|
271
|
-
return;
|
|
272
|
-
case 'Product':
|
|
273
|
-
emitProduct(node, session);
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* @param {import('./node.js').Call} node
|
|
280
|
-
* @param {ReturnType<typeof makeContext>} session
|
|
281
|
-
* @param {string} [callNameOverride]
|
|
282
|
-
* @return {void}
|
|
283
|
-
*/
|
|
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);
|
|
290
|
-
}
|
|
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);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* @param {import('./node.js').SumTerm[]} terms
|
|
343
|
-
* @param {ReturnType<typeof makeContext>} session
|
|
344
|
-
* @param {1 | -1} [multiplier]
|
|
345
|
-
* @return {void}
|
|
346
|
-
*/
|
|
347
|
-
function emitSumTerms(terms, session, multiplier = 1) {
|
|
348
|
-
const buffer = session.buffer;
|
|
349
|
-
for (let i = 0; i < terms.length; i++) {
|
|
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;
|
|
358
|
-
if (i === 0) {
|
|
359
|
-
if (scalar) {
|
|
360
|
-
if (sign === -1) buffer.push('-');
|
|
361
|
-
emitScalar(termNode, session, scalarValueOverride);
|
|
362
|
-
} else {
|
|
363
|
-
emitSumTerm(term, session, sign, scalarValueOverride);
|
|
364
|
-
}
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
buffer.push(sign === 1 ? ' + ' : ' - ');
|
|
368
|
-
if (scalar) {
|
|
369
|
-
emitScalar(termNode, session, scalarValueOverride);
|
|
370
|
-
} else {
|
|
371
|
-
emitNode(termNode, session, SUM_PRECEDENCE, true);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/** @param {Sum} sum @param {ReturnType<typeof makeContext>} session @return {void} */
|
|
377
|
-
function emitSum(sum, session) {
|
|
378
|
-
emitSumTerms(sum.terms, session);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* @param {Node} node
|
|
383
|
-
* @param {ReturnType<typeof makeContext>} session
|
|
384
|
-
* @param {number} [scalarValueOverride]
|
|
385
|
-
* @return {void}
|
|
386
|
-
*/
|
|
387
|
-
function emitLeadingNeg(node, session, scalarValueOverride) {
|
|
388
|
-
if (
|
|
389
|
-
node.type === 'Product' &&
|
|
390
|
-
node.factors.length > 0 &&
|
|
391
|
-
node.factors[0].exponent === 1 &&
|
|
392
|
-
node.factors[0].node.type === 'Num' &&
|
|
393
|
-
Number.isFinite(node.factors[0].node.value) &&
|
|
394
|
-
node.factors[0].node.value !== 0
|
|
395
|
-
) {
|
|
396
|
-
const head = node.factors[0].node;
|
|
397
|
-
emitProductFactors(node.factors, session, 1, -head.value, head);
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
session.buffer.push('-');
|
|
401
|
-
emitNode(
|
|
402
|
-
node,
|
|
403
|
-
session,
|
|
404
|
-
UNARY_PRECEDENCE,
|
|
405
|
-
false,
|
|
406
|
-
isScalar(node) ? scalarValueOverride : undefined
|
|
407
|
-
);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* @param {ProductFactor[]} factors
|
|
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
41
|
/**
|
|
530
42
|
* Serialize a scalar result without allocating a render context or buffer.
|
|
531
43
|
* @param {import('./node.js').Num | import('./node.js').Dim} node
|
|
@@ -544,20 +56,6 @@ function serializeScalarResult(node, precision, scalarPolicy, wrapper) {
|
|
|
544
56
|
return scalarPolicy === 'standard' ? `${wrapper}(${scalar})` : scalar;
|
|
545
57
|
}
|
|
546
58
|
|
|
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
59
|
/**
|
|
562
60
|
* @param {SerializeOptions} opts
|
|
563
61
|
* @return {{buffer: string[], precision: number | false, scalarPolicy: 'standard' | 'unwrap-all'}}
|
|
@@ -623,10 +121,19 @@ function planSerializeResult(result, opts) {
|
|
|
623
121
|
return { kind: 'math', node: result.tree, session, wrapper };
|
|
624
122
|
}
|
|
625
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
|
+
|
|
626
133
|
/**
|
|
627
134
|
* @param {ReturnType<typeof planSerialize> | ReturnType<typeof planSerializeResult>} renderSpec
|
|
628
135
|
* @return {string}
|
|
629
|
-
|
|
136
|
+
*/
|
|
630
137
|
function emitOutput(renderSpec) {
|
|
631
138
|
if (renderSpec.kind === 'original') return renderSpec.text;
|
|
632
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
|
}
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { baseOf, convert } from '../convertUnits.js';
|
|
2
|
+
import { isExact } from './exact.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* If `dims` contain exactly one numerator / one denominator pair with the
|
|
5
|
-
* same base type and convertible units, return the
|
|
6
|
-
*
|
|
7
|
-
* Otherwise return null. Used by `simplifyProduct` for typed
|
|
8
|
-
* (§10.2). More complex cancellation (e.g. `px^2 / px`) is left
|
|
6
|
+
* same base type and convertible units, return the two sides expressed in a
|
|
7
|
+
* common unit, so the caller can fold them into its own numerator and
|
|
8
|
+
* denominator. Otherwise return null. Used by `simplifyProduct` for typed
|
|
9
|
+
* division (§10.2). More complex cancellation (e.g. `px^2 / px`) is left
|
|
9
10
|
* unreduced — consumers rarely rely on it and the spec doesn't require it.
|
|
10
11
|
* @template {{ exponent: 1 | -1, value: number, unit: string }} D
|
|
11
12
|
* @param {D[]} dims
|
|
12
|
-
* @
|
|
13
|
+
* @param {number | false} [precision] The denominator is converted into the
|
|
14
|
+
* numerator's unit when that is exact at this precision (`1px / 1in` →
|
|
15
|
+
* `1 / 96`); otherwise the numerator is converted (`1in / 1px` → `96 / 1`).
|
|
16
|
+
* @return {{ numerator: number, denominator: number, remaining: D[] } | null}
|
|
13
17
|
*/
|
|
14
|
-
function tryCancelPair(dims) {
|
|
18
|
+
function tryCancelPair(dims, precision = false) {
|
|
15
19
|
if (dims.length !== 2) {
|
|
16
20
|
return null;
|
|
17
21
|
}
|
|
@@ -26,12 +30,32 @@ function tryCancelPair(dims) {
|
|
|
26
30
|
if (!numBase || numBase !== denBase) {
|
|
27
31
|
return null;
|
|
28
32
|
}
|
|
29
|
-
const converted = convert(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
const converted = convert(
|
|
34
|
+
denominator.value,
|
|
35
|
+
denominator.unit,
|
|
36
|
+
numerator.unit
|
|
37
|
+
);
|
|
33
38
|
// denominator.value === 0 yields ±Infinity / NaN naturally (§10.9.1).
|
|
34
|
-
|
|
39
|
+
if (converted !== null && isExact(converted, precision)) {
|
|
40
|
+
return {
|
|
41
|
+
numerator: numerator.value,
|
|
42
|
+
denominator: converted,
|
|
43
|
+
remaining: [],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const numConverted = convert(
|
|
47
|
+
numerator.value,
|
|
48
|
+
numerator.unit,
|
|
49
|
+
denominator.unit
|
|
50
|
+
);
|
|
51
|
+
if (numConverted !== null && isExact(numConverted, precision)) {
|
|
52
|
+
return {
|
|
53
|
+
numerator: numConverted,
|
|
54
|
+
denominator: denominator.value,
|
|
55
|
+
remaining: [],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
35
59
|
}
|
|
36
60
|
|
|
37
61
|
export { tryCancelPair };
|