postcss-calc 11.2.2 → 11.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -33
- package/package.json +8 -8
- package/src/lib/function-analyzers.js +119 -0
- package/src/lib/functions.js +25 -175
- package/src/lib/parser/opaque.js +3 -2
- package/src/lib/serialize/expression.js +110 -157
- package/src/lib/serialize/precedence.js +47 -0
- package/src/lib/serialize/precision.js +21 -21
- package/src/lib/serialize.js +1 -1
- package/src/lib/simplify/product-barrier.js +55 -0
- package/src/lib/simplify/product-quotient.js +141 -0
- package/src/lib/simplify/product.js +7 -178
- package/src/lib/types.js +94 -0
- package/types/lib/function-analyzers.d.ts +27 -0
- package/types/lib/functions.d.ts +3 -28
- package/types/lib/serialize/expression.d.ts +9 -49
- package/types/lib/serialize/precedence.d.ts +18 -0
- package/types/lib/serialize/precision.d.ts +11 -10
- package/types/lib/simplify/product-barrier.d.ts +26 -0
- package/types/lib/simplify/product-quotient.d.ts +48 -0
- package/types/lib/types.d.ts +48 -0
- package/types/lib/calculation-type.d.ts +0 -18
|
@@ -1,62 +1,37 @@
|
|
|
1
1
|
// Spec: https://www.w3.org/TR/css-values-4/#serialize-a-calculation-tree
|
|
2
|
-
// Canonical AST expression emission:
|
|
3
|
-
//
|
|
2
|
+
// Canonical AST expression emission: node dispatch, sums, products, function
|
|
3
|
+
// calls, and math results. These emitters recurse into each other, so they
|
|
4
|
+
// share one module; precision and precedence rules live in sibling modules.
|
|
4
5
|
|
|
5
|
-
import { serializeComponents } from '../opaque.js';
|
|
6
6
|
import { num } from '../node.js';
|
|
7
|
+
import { serializeComponents } from '../opaque.js';
|
|
7
8
|
import {
|
|
8
|
-
round,
|
|
9
|
-
isDegenerate,
|
|
10
9
|
degenerateKeyword,
|
|
11
|
-
|
|
10
|
+
emitBareSignedZero,
|
|
12
11
|
emitRoundedScalar,
|
|
13
12
|
emitScalar,
|
|
14
13
|
emitSignedZero,
|
|
14
|
+
isDegenerate,
|
|
15
15
|
isScalar,
|
|
16
16
|
isSignedZero,
|
|
17
|
-
|
|
17
|
+
round,
|
|
18
|
+
roundedScalarValue,
|
|
18
19
|
} from './precision.js';
|
|
20
|
+
import {
|
|
21
|
+
needsParentheses,
|
|
22
|
+
PRODUCT_PRECEDENCE,
|
|
23
|
+
SUM_PRECEDENCE,
|
|
24
|
+
UNARY_PRECEDENCE,
|
|
25
|
+
} from './precedence.js';
|
|
19
26
|
|
|
20
27
|
/**
|
|
21
28
|
* @typedef {import('../node.js').Node} Node
|
|
22
29
|
* @typedef {import('../node.js').Sum} Sum
|
|
23
|
-
* @typedef {import('../node.js').
|
|
30
|
+
* @typedef {import('../node.js').SumTerm} SumTerm
|
|
24
31
|
* @typedef {import('../node.js').ProductFactor} ProductFactor
|
|
25
32
|
* @typedef {import('./precision.js').SerializeSession} SerializeSession
|
|
26
33
|
*/
|
|
27
34
|
|
|
28
|
-
// The AST is canonical: sums and products are flat, except grouped nodes that
|
|
29
|
-
// keep their parentheses, so these precedence levels cover every binary expression
|
|
30
|
-
const SUM_PRECEDENCE = 1;
|
|
31
|
-
const PRODUCT_PRECEDENCE = 2;
|
|
32
|
-
const ATOMIC_PRECEDENCE = 3;
|
|
33
|
-
// Negation (-1 * ...) binds more tightly than a sum but has the same atomic boundary
|
|
34
|
-
// for deciding whether the operand needs parentheses.
|
|
35
|
-
const UNARY_PRECEDENCE = ATOMIC_PRECEDENCE;
|
|
36
|
-
|
|
37
|
-
/** @param {Node} node @return {number} */
|
|
38
|
-
function precedence(node) {
|
|
39
|
-
if (node.type === 'Sum') return SUM_PRECEDENCE;
|
|
40
|
-
if (node.type === 'Product') return PRODUCT_PRECEDENCE;
|
|
41
|
-
return ATOMIC_PRECEDENCE;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* @param {Node} node
|
|
46
|
-
* @param {number} parentPrecedence
|
|
47
|
-
* @param {boolean} groupedRequired
|
|
48
|
-
* @return {boolean}
|
|
49
|
-
*/
|
|
50
|
-
function needsParentheses(node, parentPrecedence, groupedRequired) {
|
|
51
|
-
return (
|
|
52
|
-
precedence(node) < parentPrecedence ||
|
|
53
|
-
(node.type === 'Sum' &&
|
|
54
|
-
node.grouped === true &&
|
|
55
|
-
groupedRequired === true) ||
|
|
56
|
-
(node.type === 'Product' && node.grouped === true && parentPrecedence > 0)
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
35
|
/**
|
|
61
36
|
* @param {Node} node
|
|
62
37
|
* @param {SerializeSession} session
|
|
@@ -105,129 +80,88 @@ function emitNodeBody(node, session) {
|
|
|
105
80
|
emitSum(node, session);
|
|
106
81
|
return;
|
|
107
82
|
case 'Product':
|
|
108
|
-
|
|
83
|
+
emitProductFactors(node.factors, session);
|
|
109
84
|
return;
|
|
110
85
|
}
|
|
111
86
|
}
|
|
112
87
|
|
|
113
|
-
/**
|
|
114
|
-
* @param {import('../node.js').Call} node
|
|
115
|
-
* @param {SerializeSession} session
|
|
116
|
-
* @param {string} [callNameOverride]
|
|
117
|
-
* @return {void}
|
|
118
|
-
*/
|
|
119
|
-
function emitCall(node, session, callNameOverride) {
|
|
120
|
-
const buffer = session.buffer;
|
|
121
|
-
buffer.push(callNameOverride ?? node.rawName ?? node.name, '(');
|
|
122
|
-
for (let i = 0; i < node.args.length; i++) {
|
|
123
|
-
if (i > 0) buffer.push(', ');
|
|
124
|
-
emitNode(node.args[i], session);
|
|
125
|
-
}
|
|
126
|
-
buffer.push(')');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* @param {import('../node.js').OpaqueCall} node
|
|
131
|
-
* @param {SerializeSession} session
|
|
132
|
-
* @param {string} [callNameOverride]
|
|
133
|
-
* @return {void}
|
|
134
|
-
*/
|
|
135
|
-
function emitOpaqueCall(node, session, callNameOverride) {
|
|
136
|
-
const buffer = session.buffer;
|
|
137
|
-
buffer.push(callNameOverride ?? node.rawName ?? node.name, '(');
|
|
138
|
-
serializeComponents(node.components, buffer, (child, childBuffer) => {
|
|
139
|
-
emitNestedMathResult(child, session, childBuffer);
|
|
140
|
-
});
|
|
141
|
-
buffer.push(')');
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* @param {import('../node.js').SumTerm} term
|
|
146
|
-
* @param {1 | -1} multiplier
|
|
147
|
-
* @param {number | false} precision
|
|
148
|
-
* @return {1 | -1}
|
|
149
|
-
*/
|
|
150
|
-
function termSign(term, multiplier, precision) {
|
|
151
|
-
let sign = /** @type {1 | -1} */ (term.sign * multiplier);
|
|
152
|
-
if (isEffectivelyNegative(term.node, precision)) {
|
|
153
|
-
sign = /** @type {1 | -1} */ (-sign);
|
|
154
|
-
}
|
|
155
|
-
return sign;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* @param {import('../node.js').SumTerm} term
|
|
160
|
-
* @param {SerializeSession} session
|
|
161
|
-
* @param {1 | -1} sign
|
|
162
|
-
* @return {void}
|
|
163
|
-
*/
|
|
164
|
-
function emitSumTerm(term, session, sign) {
|
|
165
|
-
if (sign === 1) {
|
|
166
|
-
emitNode(term.node, session, SUM_PRECEDENCE, true);
|
|
167
|
-
} else {
|
|
168
|
-
emitLeadingNeg(term.node, session);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
88
|
/**
|
|
173
89
|
* @param {import('../node.js').Num | import('../node.js').Dim} termNode
|
|
174
90
|
* @param {1 | -1} sign
|
|
175
|
-
* @param {
|
|
91
|
+
* @param {boolean} leading
|
|
176
92
|
* @param {SerializeSession} session
|
|
177
93
|
* @return {void}
|
|
178
94
|
*/
|
|
179
|
-
function emitScalarSumTerm(termNode, sign,
|
|
95
|
+
function emitScalarSumTerm(termNode, sign, leading, session) {
|
|
180
96
|
const buffer = session.buffer;
|
|
181
97
|
const effectiveVal = sign * termNode.value;
|
|
182
98
|
if (Object.is(effectiveVal, -0)) {
|
|
183
|
-
|
|
184
|
-
|
|
99
|
+
// `x - 0` reparses as x + -(0), so subtraction round-trips negative zero.
|
|
100
|
+
// A leading term starts a <calc-product>, so `-1 * 0` needs no calc().
|
|
101
|
+
if (leading) {
|
|
102
|
+
emitBareSignedZero(buffer, termNode);
|
|
103
|
+
} else {
|
|
104
|
+
buffer.push(' - ');
|
|
105
|
+
emitRoundedScalar(termNode, buffer, 0);
|
|
106
|
+
}
|
|
185
107
|
return;
|
|
186
108
|
}
|
|
187
109
|
if (isDegenerate(effectiveVal)) {
|
|
188
|
-
if (
|
|
189
|
-
else if (
|
|
110
|
+
if (!leading) buffer.push(sign === 1 ? ' + ' : ' - ');
|
|
111
|
+
else if (sign === -1) buffer.push('-');
|
|
190
112
|
emitScalar(termNode, session);
|
|
191
113
|
return;
|
|
192
114
|
}
|
|
193
115
|
const rounded = round(effectiveVal, session.precision);
|
|
194
116
|
if (rounded < 0) {
|
|
195
|
-
buffer.push(
|
|
117
|
+
buffer.push(leading ? '-' : ' - ');
|
|
196
118
|
emitRoundedScalar(termNode, buffer, -rounded);
|
|
197
119
|
} else {
|
|
198
|
-
if (
|
|
120
|
+
if (!leading) buffer.push(' + ');
|
|
199
121
|
emitRoundedScalar(termNode, buffer, rounded);
|
|
200
122
|
}
|
|
201
123
|
}
|
|
202
124
|
|
|
203
125
|
/**
|
|
204
|
-
* @param {
|
|
126
|
+
* @param {SumTerm} term
|
|
127
|
+
* @param {boolean} leading
|
|
205
128
|
* @param {SerializeSession} session
|
|
206
|
-
* @param {1 | -1} [multiplier]
|
|
207
129
|
* @return {void}
|
|
208
130
|
*/
|
|
209
|
-
function
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
if (i === 0) {
|
|
220
|
-
emitSumTerm(term, session, sign);
|
|
221
|
-
} else {
|
|
222
|
-
buffer.push(sign === 1 ? ' + ' : ' - ');
|
|
223
|
-
emitNode(termNode, session, SUM_PRECEDENCE, true);
|
|
224
|
-
}
|
|
131
|
+
function emitSumTerm(term, leading, session) {
|
|
132
|
+
const termNode = term.node;
|
|
133
|
+
if (isScalar(termNode)) {
|
|
134
|
+
emitScalarSumTerm(termNode, term.sign, leading, session);
|
|
135
|
+
} else if (leading && term.sign === -1) {
|
|
136
|
+
emitLeadingNeg(termNode, session);
|
|
137
|
+
} else {
|
|
138
|
+
if (!leading) session.buffer.push(term.sign === 1 ? ' + ' : ' - ');
|
|
139
|
+
emitNode(termNode, session, SUM_PRECEDENCE, true);
|
|
225
140
|
}
|
|
226
141
|
}
|
|
227
142
|
|
|
228
|
-
/** @param {
|
|
143
|
+
/** @param {SumTerm} term @return {boolean} */
|
|
144
|
+
function isNegativeZeroTerm(term) {
|
|
145
|
+
return isScalar(term.node) && Object.is(term.sign * term.node.value, -0);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @param {Sum} sum
|
|
150
|
+
* @param {SerializeSession} session
|
|
151
|
+
* @return {void}
|
|
152
|
+
*/
|
|
229
153
|
function emitSum(sum, session) {
|
|
230
|
-
|
|
154
|
+
const terms = sum.terms;
|
|
155
|
+
// x + -0 is exactly x for every x, so leading negative zeros can move
|
|
156
|
+
// behind the first other term and use the shorter subtraction form. When
|
|
157
|
+
// every term is negative zero, keep the order so reducing again is stable.
|
|
158
|
+
let lead = 0;
|
|
159
|
+
while (lead < terms.length && isNegativeZeroTerm(terms[lead])) lead++;
|
|
160
|
+
if (lead === terms.length) lead = 0;
|
|
161
|
+
emitSumTerm(terms[lead], true, session);
|
|
162
|
+
for (let i = 0; i < terms.length; i++) {
|
|
163
|
+
if (i !== lead) emitSumTerm(terms[i], false, session);
|
|
164
|
+
}
|
|
231
165
|
}
|
|
232
166
|
|
|
233
167
|
/**
|
|
@@ -269,34 +203,64 @@ function emitProductFactors(
|
|
|
269
203
|
coefficientNode
|
|
270
204
|
) {
|
|
271
205
|
const buffer = session.buffer;
|
|
272
|
-
|
|
206
|
+
// Only the product's first multiplied scalar may print negative zero as a
|
|
207
|
+
// bare `-1 * 0`: reparsing folds the `-1` back into that same coefficient.
|
|
208
|
+
// After any operator, the `-1` would bind to the preceding operand instead.
|
|
209
|
+
let leading = true;
|
|
273
210
|
if (coefficientValue !== undefined && coefficientValue !== 1) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
session,
|
|
277
|
-
coefficientValue
|
|
211
|
+
const coefficient = /** @type {import('../node.js').Num} */ (
|
|
212
|
+
coefficientNode
|
|
278
213
|
);
|
|
279
|
-
|
|
214
|
+
emitScalar(coefficient, session, coefficientValue, true);
|
|
215
|
+
leading = false;
|
|
280
216
|
}
|
|
281
217
|
for (let i = start; i < factors.length; i++) {
|
|
282
218
|
const factor = factors[i];
|
|
283
219
|
const factorNode = factor.node;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
220
|
+
const multiplied = factor.exponent === 1;
|
|
221
|
+
if (!leading) buffer.push(multiplied ? ' * ' : ' / ');
|
|
222
|
+
else if (!multiplied) buffer.push('1 / ');
|
|
223
|
+
if (isScalar(factorNode)) {
|
|
224
|
+
emitScalar(factorNode, session, undefined, leading && multiplied);
|
|
289
225
|
} else {
|
|
290
|
-
|
|
291
|
-
if (isScalar(factorNode)) emitScalar(factorNode, session);
|
|
292
|
-
else emitNode(factorNode, session, PRODUCT_PRECEDENCE);
|
|
226
|
+
emitNode(factorNode, session, PRODUCT_PRECEDENCE);
|
|
293
227
|
}
|
|
228
|
+
leading = false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* @param {import('../node.js').Call} node
|
|
234
|
+
* @param {SerializeSession} session
|
|
235
|
+
* @param {string} [callNameOverride]
|
|
236
|
+
* @return {void}
|
|
237
|
+
*/
|
|
238
|
+
function emitCall(node, session, callNameOverride) {
|
|
239
|
+
const buffer = session.buffer;
|
|
240
|
+
buffer.push(callNameOverride ?? node.rawName ?? node.name, '(');
|
|
241
|
+
for (let i = 0; i < node.args.length; i++) {
|
|
242
|
+
if (i > 0) buffer.push(', ');
|
|
243
|
+
const arg = node.args[i];
|
|
244
|
+
// Every math-function argument is a <calc-sum>, so it needs no calc()
|
|
245
|
+
if (isSignedZero(arg)) emitBareSignedZero(buffer, arg);
|
|
246
|
+
else emitNode(arg, session);
|
|
294
247
|
}
|
|
248
|
+
buffer.push(')');
|
|
295
249
|
}
|
|
296
250
|
|
|
297
|
-
/**
|
|
298
|
-
|
|
299
|
-
|
|
251
|
+
/**
|
|
252
|
+
* @param {import('../node.js').OpaqueCall} node
|
|
253
|
+
* @param {SerializeSession} session
|
|
254
|
+
* @param {string} [callNameOverride]
|
|
255
|
+
* @return {void}
|
|
256
|
+
*/
|
|
257
|
+
function emitOpaqueCall(node, session, callNameOverride) {
|
|
258
|
+
const buffer = session.buffer;
|
|
259
|
+
buffer.push(callNameOverride ?? node.rawName ?? node.name, '(');
|
|
260
|
+
serializeComponents(node.components, buffer, (child, childBuffer) => {
|
|
261
|
+
emitNestedMathResult(child, session, childBuffer);
|
|
262
|
+
});
|
|
263
|
+
buffer.push(')');
|
|
300
264
|
}
|
|
301
265
|
|
|
302
266
|
/**
|
|
@@ -356,21 +320,10 @@ function emitNestedMathResult(node, session, buffer = session.buffer) {
|
|
|
356
320
|
}
|
|
357
321
|
|
|
358
322
|
export {
|
|
359
|
-
SUM_PRECEDENCE,
|
|
360
|
-
PRODUCT_PRECEDENCE,
|
|
361
|
-
ATOMIC_PRECEDENCE,
|
|
362
|
-
UNARY_PRECEDENCE,
|
|
363
|
-
precedence,
|
|
364
|
-
needsParentheses,
|
|
365
323
|
emitNode,
|
|
324
|
+
emitLeadingNeg,
|
|
366
325
|
emitCall,
|
|
367
326
|
emitOpaqueCall,
|
|
368
|
-
termSign,
|
|
369
|
-
emitSumTerms,
|
|
370
|
-
emitSum,
|
|
371
|
-
emitLeadingNeg,
|
|
372
|
-
emitProductFactors,
|
|
373
|
-
emitProduct,
|
|
374
327
|
emitMathResult,
|
|
375
328
|
emitNestedMathResult,
|
|
376
329
|
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Spec: https://www.w3.org/TR/css-values-4/#serialize-a-calculation-tree
|
|
2
|
+
// Operator precedence and parenthesization rules for canonical AST emission.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import('../node.js').Node} Node
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// The AST is canonical: sums and products are flat, except grouped nodes that
|
|
9
|
+
// keep their parentheses, so these precedence levels cover every binary expression
|
|
10
|
+
const SUM_PRECEDENCE = 1;
|
|
11
|
+
const PRODUCT_PRECEDENCE = 2;
|
|
12
|
+
const ATOMIC_PRECEDENCE = 3;
|
|
13
|
+
// Negation (-1 * ...) binds more tightly than a sum but has the same atomic boundary
|
|
14
|
+
// for deciding whether the operand needs parentheses.
|
|
15
|
+
const UNARY_PRECEDENCE = ATOMIC_PRECEDENCE;
|
|
16
|
+
|
|
17
|
+
/** @param {Node} node @return {number} */
|
|
18
|
+
function precedence(node) {
|
|
19
|
+
if (node.type === 'Sum') return SUM_PRECEDENCE;
|
|
20
|
+
if (node.type === 'Product') return PRODUCT_PRECEDENCE;
|
|
21
|
+
return ATOMIC_PRECEDENCE;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {Node} node
|
|
26
|
+
* @param {number} parentPrecedence
|
|
27
|
+
* @param {boolean} groupedRequired
|
|
28
|
+
* @return {boolean}
|
|
29
|
+
*/
|
|
30
|
+
function needsParentheses(node, parentPrecedence, groupedRequired) {
|
|
31
|
+
return (
|
|
32
|
+
precedence(node) < parentPrecedence ||
|
|
33
|
+
(node.type === 'Sum' &&
|
|
34
|
+
node.grouped === true &&
|
|
35
|
+
groupedRequired === true) ||
|
|
36
|
+
(node.type === 'Product' && node.grouped === true && parentPrecedence > 0)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
SUM_PRECEDENCE,
|
|
42
|
+
PRODUCT_PRECEDENCE,
|
|
43
|
+
ATOMIC_PRECEDENCE,
|
|
44
|
+
UNARY_PRECEDENCE,
|
|
45
|
+
precedence,
|
|
46
|
+
needsParentheses,
|
|
47
|
+
};
|
|
@@ -168,13 +168,16 @@ function emitFiniteScalar(node, session, value) {
|
|
|
168
168
|
* @param {import('../node.js').Num | import('../node.js').Dim} node
|
|
169
169
|
* @param {SerializeSession} session
|
|
170
170
|
* @param {number} [value]
|
|
171
|
+
* @param {boolean} [bare] emit negative zero without its calc() wrapper
|
|
171
172
|
* @return {void}
|
|
172
173
|
*/
|
|
173
|
-
function emitScalar(node, session, value) {
|
|
174
|
+
function emitScalar(node, session, value, bare = false) {
|
|
174
175
|
const buffer = session.buffer;
|
|
175
176
|
const effective = value ?? node.value;
|
|
176
|
-
if (Object.is(effective, -0))
|
|
177
|
-
|
|
177
|
+
if (Object.is(effective, -0)) {
|
|
178
|
+
if (bare) emitBareSignedZero(buffer, node);
|
|
179
|
+
else emitSignedZero(buffer, node);
|
|
180
|
+
} else if (isDegenerate(effective)) {
|
|
178
181
|
if (node.type === 'Dim') {
|
|
179
182
|
buffer.push(
|
|
180
183
|
'calc(',
|
|
@@ -195,8 +198,21 @@ function emitScalar(node, session, value) {
|
|
|
195
198
|
* @return {void}
|
|
196
199
|
*/
|
|
197
200
|
function emitSignedZero(buffer, node) {
|
|
201
|
+
buffer.push('calc(');
|
|
202
|
+
emitBareSignedZero(buffer, node);
|
|
203
|
+
buffer.push(')');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Negative zero as an unwrapped product, valid only where a `<calc-product>`
|
|
208
|
+
* may appear without changing how the surrounding expression parses.
|
|
209
|
+
* @param {string[]} buffer
|
|
210
|
+
* @param {import('../node.js').Num | import('../node.js').Dim} node
|
|
211
|
+
* @return {void}
|
|
212
|
+
*/
|
|
213
|
+
function emitBareSignedZero(buffer, node) {
|
|
198
214
|
const unit = node.type === 'Dim' ? (node.rawUnit ?? node.unit) : '';
|
|
199
|
-
buffer.push('
|
|
215
|
+
buffer.push('-1 * 0', unit);
|
|
200
216
|
}
|
|
201
217
|
|
|
202
218
|
/** @param {Node} node @return {node is import('../node.js').Num | import('../node.js').Dim} */
|
|
@@ -209,22 +225,6 @@ function isSignedZero(node) {
|
|
|
209
225
|
return isScalar(node) ? Object.is(node.value, -0) : false;
|
|
210
226
|
}
|
|
211
227
|
|
|
212
|
-
/**
|
|
213
|
-
* Whether a scalar node is strictly negative after precision rounding
|
|
214
|
-
* (excluding signed zero and sub-precision values that round to zero).
|
|
215
|
-
* @param {Node} node
|
|
216
|
-
* @param {number | false} precision
|
|
217
|
-
* @return {node is import('../node.js').Num | import('../node.js').Dim}
|
|
218
|
-
*/
|
|
219
|
-
function isEffectivelyNegative(node, precision) {
|
|
220
|
-
return (
|
|
221
|
-
isScalar(node) &&
|
|
222
|
-
!Object.is(node.value, -0) &&
|
|
223
|
-
Number.isFinite(node.value) &&
|
|
224
|
-
round(node.value, precision) < 0
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
228
|
export {
|
|
229
229
|
NOISE_FLOOR,
|
|
230
230
|
divideByPowerOfTen,
|
|
@@ -238,7 +238,7 @@ export {
|
|
|
238
238
|
emitFiniteScalar,
|
|
239
239
|
emitScalar,
|
|
240
240
|
emitSignedZero,
|
|
241
|
+
emitBareSignedZero,
|
|
241
242
|
isScalar,
|
|
242
243
|
isSignedZero,
|
|
243
|
-
isEffectivelyNegative,
|
|
244
244
|
};
|
package/src/lib/serialize.js
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { num } from '../node.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('../node.js').Node} Node
|
|
5
|
+
* @typedef {import('../node.js').ProductFactor} ProductFactor
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A substitution function (`var()`, `env()`, `attr()`, ...) is replaced by
|
|
10
|
+
* tokens, not by a value, so factors cannot move or cancel across it. Any
|
|
11
|
+
* function the parser does not recognise (`anchor()`, `foo()`, ...) is also an
|
|
12
|
+
* OpaqueCall and is treated the same, conservatively. A grouped Product is a
|
|
13
|
+
* parenthesized substitution and is equally opaque.
|
|
14
|
+
* @param {Node} node
|
|
15
|
+
* @return {boolean}
|
|
16
|
+
*/
|
|
17
|
+
function isBarrier(node) {
|
|
18
|
+
return (
|
|
19
|
+
node.type === 'OpaqueCall' ||
|
|
20
|
+
(node.type === 'Product' && node.grouped === true)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Flatten a simplified factor through nested ungrouped Products and the
|
|
26
|
+
* canonical negation form, so barriers are visible at the top level.
|
|
27
|
+
* @param {ProductFactor[]} out
|
|
28
|
+
* @param {1 | -1} exponent
|
|
29
|
+
* @param {Node} node
|
|
30
|
+
* @return {boolean} Whether a barrier was pushed.
|
|
31
|
+
*/
|
|
32
|
+
function flattenFactor(out, exponent, node) {
|
|
33
|
+
if (node.type === 'Product' && !node.grouped) {
|
|
34
|
+
let barrier = false;
|
|
35
|
+
for (const inner of node.factors) {
|
|
36
|
+
if (
|
|
37
|
+
flattenFactor(
|
|
38
|
+
out,
|
|
39
|
+
/** @type {1 | -1} */ (exponent * inner.exponent),
|
|
40
|
+
inner.node
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
barrier = true;
|
|
44
|
+
}
|
|
45
|
+
return barrier;
|
|
46
|
+
}
|
|
47
|
+
if (node.type === 'Sum' && node.terms.length === 1) {
|
|
48
|
+
out.push({ exponent, node: num(-1) });
|
|
49
|
+
return flattenFactor(out, exponent, node.terms[0].node);
|
|
50
|
+
}
|
|
51
|
+
out.push({ exponent, node });
|
|
52
|
+
return isBarrier(node);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { flattenFactor, isBarrier };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { mkProduct, num, dim } from '../node.js';
|
|
2
|
+
import { isExact } from './exact.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import('../node.js').Node} Node
|
|
6
|
+
* @typedef {import('../node.js').ProductFactor} ProductFactor
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {number} a non-negative
|
|
11
|
+
* @param {number} b non-negative
|
|
12
|
+
* @return {number}
|
|
13
|
+
*/
|
|
14
|
+
function gcd(a, b) {
|
|
15
|
+
let x = a;
|
|
16
|
+
let y = b;
|
|
17
|
+
while (y !== 0) {
|
|
18
|
+
[x, y] = [y, x % y];
|
|
19
|
+
}
|
|
20
|
+
return x || 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {{exponent: 1 | -1, value: number}[]} chain
|
|
25
|
+
* @return {number}
|
|
26
|
+
*/
|
|
27
|
+
function chainValue(chain) {
|
|
28
|
+
let value = 1;
|
|
29
|
+
for (const f of chain) {
|
|
30
|
+
value = f.exponent === 1 ? value * f.value : value / f.value;
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether the opaque factors are a single Sum of only Num/Dim terms.
|
|
37
|
+
* @param {ProductFactor[]} opaque
|
|
38
|
+
* @return {boolean}
|
|
39
|
+
*/
|
|
40
|
+
function isScalarSum(opaque) {
|
|
41
|
+
return (
|
|
42
|
+
opaque.length === 1 &&
|
|
43
|
+
opaque[0].exponent === 1 &&
|
|
44
|
+
opaque[0].node.type === 'Sum' &&
|
|
45
|
+
opaque[0].node.terms.every(
|
|
46
|
+
(t) => t.node.type === 'Num' || t.node.type === 'Dim'
|
|
47
|
+
)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether a folded quotient prints exactly. A quotient distributed over a
|
|
53
|
+
* scalar Sum is judged term by term, so `(3px + 6em) / 3` still folds.
|
|
54
|
+
* @param {number} value
|
|
55
|
+
* @param {ProductFactor[]} opaque
|
|
56
|
+
* @param {boolean} distributable
|
|
57
|
+
* @param {number | false} precision
|
|
58
|
+
* @return {boolean}
|
|
59
|
+
*/
|
|
60
|
+
function foldsExactly(value, opaque, distributable, precision) {
|
|
61
|
+
if (!Number.isFinite(value) || isExact(value, precision)) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (!distributable) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
const sum = /** @type {import('../node.js').Sum} */ (opaque[0].node);
|
|
68
|
+
return sum.terms.every((t) =>
|
|
69
|
+
isExact(
|
|
70
|
+
value *
|
|
71
|
+
/** @type {import('../node.js').Num | import('../node.js').Dim} */ (
|
|
72
|
+
t.node
|
|
73
|
+
).value,
|
|
74
|
+
precision
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Emit an inexact quotient as `numerator * dims * opaque / denominator`, with
|
|
81
|
+
* integer parts reduced by their gcd. A lone Dim absorbs the numerator.
|
|
82
|
+
* @param {number} numerator
|
|
83
|
+
* @param {number} denominator
|
|
84
|
+
* @param {{value: number, unit: string, rawUnit?: string} | null} loneDim
|
|
85
|
+
* @param {{exponent: 1 | -1, value: number, unit: string, rawUnit?: string}[]} dims
|
|
86
|
+
* @param {ProductFactor[]} opaque
|
|
87
|
+
* @param {number | false} precision
|
|
88
|
+
* @return {Node | null} null when a part would be rounded on output
|
|
89
|
+
*/
|
|
90
|
+
function rationalProduct(
|
|
91
|
+
numerator,
|
|
92
|
+
denominator,
|
|
93
|
+
loneDim,
|
|
94
|
+
dims,
|
|
95
|
+
opaque,
|
|
96
|
+
precision
|
|
97
|
+
) {
|
|
98
|
+
// Drop float noise (`1.9999999999999993 * 100`) so the parts print as
|
|
99
|
+
// written and the gcd below can reduce them.
|
|
100
|
+
let n = Number(
|
|
101
|
+
(loneDim === null ? numerator : numerator * loneDim.value).toPrecision(15)
|
|
102
|
+
);
|
|
103
|
+
let d = Number(denominator.toPrecision(15));
|
|
104
|
+
if (!isExact(n, precision) || !isExact(d, precision)) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
if (Number.isSafeInteger(n) && Number.isSafeInteger(d)) {
|
|
108
|
+
const g = gcd(Math.abs(n), Math.abs(d));
|
|
109
|
+
n /= g;
|
|
110
|
+
d /= g;
|
|
111
|
+
}
|
|
112
|
+
if (d < 0) {
|
|
113
|
+
n = -n;
|
|
114
|
+
d = -d;
|
|
115
|
+
}
|
|
116
|
+
/** @type {ProductFactor[]} */
|
|
117
|
+
const factors = [];
|
|
118
|
+
if (loneDim !== null) {
|
|
119
|
+
factors.push({
|
|
120
|
+
exponent: 1,
|
|
121
|
+
node: dim(n, loneDim.unit, loneDim.rawUnit),
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
if (n !== 1) {
|
|
125
|
+
factors.push({ exponent: 1, node: num(n) });
|
|
126
|
+
}
|
|
127
|
+
for (const dm of dims) {
|
|
128
|
+
factors.push({
|
|
129
|
+
exponent: dm.exponent,
|
|
130
|
+
node: dim(dm.value, dm.unit, dm.rawUnit),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
factors.push(...opaque);
|
|
135
|
+
if (d !== 1) {
|
|
136
|
+
factors.push({ exponent: -1, node: num(d) });
|
|
137
|
+
}
|
|
138
|
+
return mkProduct(factors);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export { chainValue, foldsExactly, isScalarSum, rationalProduct };
|