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/convertUnits.js
CHANGED
|
@@ -139,4 +139,18 @@ function convert(value, from, to) {
|
|
|
139
139
|
return (value * f) / t;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Return a base type only for units present in the static conversion table.
|
|
144
|
+
* Units with context-dependent values (em, rem, vw, etc.) return null.
|
|
145
|
+
* @param {string} unit
|
|
146
|
+
* @return {BaseType | null}
|
|
147
|
+
*/
|
|
148
|
+
function staticBaseOf(unit) {
|
|
149
|
+
const key = unit.toLowerCase();
|
|
150
|
+
if (TO_CANONICAL[key] === undefined) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return UNIT_TO_BASE[key] ?? null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export { baseOf, staticBaseOf, convert };
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { simplifyMinMax } from './simplify/min-max.js';
|
|
2
|
+
import { simplifyClamp } from './simplify/clamp.js';
|
|
3
|
+
import { simplifyAbs } from './simplify/abs.js';
|
|
4
|
+
import { simplifySign } from './simplify/sign.js';
|
|
5
|
+
import { simplifyModRem } from './simplify/mod-rem.js';
|
|
6
|
+
import { ROUND_STRATEGIES, simplifyRound } from './simplify/round.js';
|
|
7
|
+
import { simplifyTrig } from './simplify/trig.js';
|
|
8
|
+
import { simplifyInverseTrig } from './simplify/inverse-trig.js';
|
|
9
|
+
import { simplifyAtan2 } from './simplify/atan2.js';
|
|
10
|
+
import { simplifyPow } from './simplify/pow.js';
|
|
11
|
+
import { simplifySqrt } from './simplify/sqrt.js';
|
|
12
|
+
import { simplifyExp } from './simplify/exp.js';
|
|
13
|
+
import { simplifyLog } from './simplify/log.js';
|
|
14
|
+
import { simplifyHypot } from './simplify/hypot.js';
|
|
15
|
+
|
|
16
|
+
/** @typedef {import('./node.js').Node} Node */
|
|
17
|
+
/** @typedef {{kind: 'number'} | {kind: 'dimension', base: string | null} | {kind: 'unknown'} | {kind: 'failure'}} CalculationType */
|
|
18
|
+
/** @typedef {(name: string, args: Node[]) => Node} MathSimplifier */
|
|
19
|
+
/** @typedef {(args: CalculationType[], nodes: Node[]) => CalculationType} TypeAnalyzer */
|
|
20
|
+
/** @typedef {{analyze: TypeAnalyzer, simplify?: MathSimplifier, isKeyword?: (node: Node, index: number) => boolean, calculation?: boolean}} MathFunction */
|
|
21
|
+
|
|
22
|
+
/** @type {CalculationType} */ const numberType = { kind: 'number' };
|
|
23
|
+
/** @type {CalculationType} */ const unknownType = { kind: 'unknown' };
|
|
24
|
+
/** @type {CalculationType} */ const failureType = { kind: 'failure' };
|
|
25
|
+
|
|
26
|
+
/** @param {CalculationType} type @return {boolean} */
|
|
27
|
+
function isFailure(type) {
|
|
28
|
+
return type.kind === 'failure';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @param {CalculationType} a @param {CalculationType} b @return {CalculationType} */
|
|
32
|
+
function addTypes(a, b) {
|
|
33
|
+
if (isFailure(a) || isFailure(b)) return failureType;
|
|
34
|
+
if (a.kind === 'unknown' || b.kind === 'unknown') return unknownType;
|
|
35
|
+
if (a.kind === 'number' && b.kind === 'number') return numberType;
|
|
36
|
+
if (a.kind === 'dimension' && b.kind === 'dimension') {
|
|
37
|
+
if (a.base === null || b.base === null) return unknownType;
|
|
38
|
+
return a.base === b.base ? a : failureType;
|
|
39
|
+
}
|
|
40
|
+
return failureType;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check an all-number function without assuming anything about an unresolved
|
|
45
|
+
* operand. A concrete dimension can never become a number, so it is still a
|
|
46
|
+
* definite error when another argument is opaque.
|
|
47
|
+
* @param {CalculationType[]} args
|
|
48
|
+
* @param {number} min
|
|
49
|
+
* @param {number} max
|
|
50
|
+
* @return {CalculationType}
|
|
51
|
+
*/
|
|
52
|
+
function numberArguments(args, min, max) {
|
|
53
|
+
if (args.length < min || args.length > max) return failureType;
|
|
54
|
+
let hasUnknown = false;
|
|
55
|
+
for (const arg of args) {
|
|
56
|
+
if (arg.kind === 'dimension') return failureType;
|
|
57
|
+
hasUnknown = hasUnknown || arg.kind === 'unknown';
|
|
58
|
+
}
|
|
59
|
+
return hasUnknown ? unknownType : numberType;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check values that must share a calculation type. Unknown operands remain
|
|
64
|
+
* unknown: they might resolve to the concrete type required by their peers.
|
|
65
|
+
* @param {CalculationType[]} args
|
|
66
|
+
* @param {number} min
|
|
67
|
+
* @param {number} max
|
|
68
|
+
* @return {CalculationType}
|
|
69
|
+
*/
|
|
70
|
+
function matchingArguments(args, min, max) {
|
|
71
|
+
if (args.length < min || args.length > max) return failureType;
|
|
72
|
+
let result = args[0];
|
|
73
|
+
for (let index = 1; index < args.length; index++) {
|
|
74
|
+
result = addTypes(result, args[index]);
|
|
75
|
+
if (isFailure(result)) return failureType;
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
81
|
+
function analyzeTrig(args) {
|
|
82
|
+
return args.length === 1 &&
|
|
83
|
+
(args[0].kind === 'unknown' ||
|
|
84
|
+
args[0].kind === 'number' ||
|
|
85
|
+
(args[0].kind === 'dimension' && args[0].base === 'angle'))
|
|
86
|
+
? numberType
|
|
87
|
+
: failureType;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
91
|
+
function analyzeInverseTrig(args) {
|
|
92
|
+
if (args.length !== 1 || args[0].kind === 'dimension') {
|
|
93
|
+
return failureType;
|
|
94
|
+
}
|
|
95
|
+
return args[0].kind === 'unknown'
|
|
96
|
+
? unknownType
|
|
97
|
+
: { kind: 'dimension', base: 'angle' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
101
|
+
function analyzeIdentity(args) {
|
|
102
|
+
return args.length === 1 ? args[0] : failureType;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
106
|
+
function analyzeSign(args) {
|
|
107
|
+
return args.length === 1 ? numberType : failureType;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @param {CalculationType[]} args @param {Node[]} nodes @return {CalculationType} */
|
|
111
|
+
function analyzeRound(args, nodes) {
|
|
112
|
+
const strategy = nodes[0];
|
|
113
|
+
const hasStrategy =
|
|
114
|
+
strategy?.type === 'Ident' &&
|
|
115
|
+
ROUND_STRATEGIES.has(strategy.name.toLowerCase());
|
|
116
|
+
const start = hasStrategy ? 1 : 0;
|
|
117
|
+
const valueCount = args.length - start;
|
|
118
|
+
if (valueCount === 1) {
|
|
119
|
+
const value = args[start];
|
|
120
|
+
if (value.kind === 'dimension') return failureType;
|
|
121
|
+
if (value.kind === 'unknown') return unknownType;
|
|
122
|
+
return numberType;
|
|
123
|
+
}
|
|
124
|
+
if (valueCount !== 2) return failureType;
|
|
125
|
+
const type = addTypes(args[start], args[start + 1]);
|
|
126
|
+
return isFailure(type) ? failureType : type;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
130
|
+
function analyzeAtan2(args) {
|
|
131
|
+
const type = matchingArguments(args, 2, 2);
|
|
132
|
+
return isFailure(type) || type.kind === 'unknown'
|
|
133
|
+
? type
|
|
134
|
+
: { kind: 'dimension', base: 'angle' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
138
|
+
function analyzeCalc(args) {
|
|
139
|
+
return args.length === 1 ? args[0] : failureType;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** @param {Node} node @return {boolean} */
|
|
143
|
+
function isRoundStrategy(node) {
|
|
144
|
+
return node.type === 'Ident' && ROUND_STRATEGIES.has(node.name.toLowerCase());
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** @param {Node} node @param {number} index @return {boolean} */
|
|
148
|
+
function isClampKeyword(node, index) {
|
|
149
|
+
return (
|
|
150
|
+
(index === 0 || index === 2) &&
|
|
151
|
+
node.type === 'Ident' &&
|
|
152
|
+
node.name.toLowerCase() === 'none'
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** @param {CalculationType[]} args @param {Node[]} nodes @return {CalculationType} */
|
|
157
|
+
function analyzeClamp(args, nodes) {
|
|
158
|
+
if (args.length !== 3) return failureType;
|
|
159
|
+
let valueCount = 0;
|
|
160
|
+
/** @type {CalculationType} */ let result = failureType;
|
|
161
|
+
for (let index = 0; index < args.length; index++) {
|
|
162
|
+
if (isClampKeyword(nodes[index], index)) continue;
|
|
163
|
+
result = valueCount === 0 ? args[index] : addTypes(result, args[index]);
|
|
164
|
+
valueCount++;
|
|
165
|
+
}
|
|
166
|
+
if (valueCount < 1 || isFailure(result)) return failureType;
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const mathFunctions = new Map(
|
|
171
|
+
/** @type {[string, MathFunction][]} */ ([
|
|
172
|
+
['calc', { analyze: analyzeCalc, calculation: true }],
|
|
173
|
+
['-webkit-calc', { analyze: analyzeCalc, calculation: true }],
|
|
174
|
+
['-moz-calc', { analyze: analyzeCalc, calculation: true }],
|
|
175
|
+
[
|
|
176
|
+
'min',
|
|
177
|
+
{
|
|
178
|
+
analyze: (args) => matchingArguments(args, 1, Infinity),
|
|
179
|
+
simplify: simplifyMinMax,
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
[
|
|
183
|
+
'max',
|
|
184
|
+
{
|
|
185
|
+
analyze: (args) => matchingArguments(args, 1, Infinity),
|
|
186
|
+
simplify: simplifyMinMax,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
[
|
|
190
|
+
'clamp',
|
|
191
|
+
{
|
|
192
|
+
analyze: analyzeClamp,
|
|
193
|
+
simplify: (_name, args) => simplifyClamp(args),
|
|
194
|
+
isKeyword: isClampKeyword,
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
[
|
|
198
|
+
'abs',
|
|
199
|
+
{
|
|
200
|
+
analyze: analyzeIdentity,
|
|
201
|
+
simplify: (_name, args) => simplifyAbs(args),
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
[
|
|
205
|
+
'sign',
|
|
206
|
+
{
|
|
207
|
+
analyze: analyzeSign,
|
|
208
|
+
simplify: (_name, args) => simplifySign(args),
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
[
|
|
212
|
+
'mod',
|
|
213
|
+
{
|
|
214
|
+
analyze: (args) => matchingArguments(args, 2, 2),
|
|
215
|
+
simplify: (_name, args) => simplifyModRem('mod', args),
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
[
|
|
219
|
+
'rem',
|
|
220
|
+
{
|
|
221
|
+
analyze: (args) => matchingArguments(args, 2, 2),
|
|
222
|
+
simplify: (_name, args) => simplifyModRem('rem', args),
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
[
|
|
226
|
+
'round',
|
|
227
|
+
{
|
|
228
|
+
analyze: analyzeRound,
|
|
229
|
+
simplify: (_name, args) => simplifyRound(args),
|
|
230
|
+
isKeyword: (node, index) => index === 0 && isRoundStrategy(node),
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
[
|
|
234
|
+
'sin',
|
|
235
|
+
{
|
|
236
|
+
analyze: analyzeTrig,
|
|
237
|
+
simplify: (_name, args) => simplifyTrig('sin', args),
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
[
|
|
241
|
+
'cos',
|
|
242
|
+
{
|
|
243
|
+
analyze: analyzeTrig,
|
|
244
|
+
simplify: (_name, args) => simplifyTrig('cos', args),
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
[
|
|
248
|
+
'tan',
|
|
249
|
+
{
|
|
250
|
+
analyze: analyzeTrig,
|
|
251
|
+
simplify: (_name, args) => simplifyTrig('tan', args),
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
[
|
|
255
|
+
'asin',
|
|
256
|
+
{
|
|
257
|
+
analyze: analyzeInverseTrig,
|
|
258
|
+
simplify: (_name, args) => simplifyInverseTrig('asin', args),
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
[
|
|
262
|
+
'acos',
|
|
263
|
+
{
|
|
264
|
+
analyze: analyzeInverseTrig,
|
|
265
|
+
simplify: (_name, args) => simplifyInverseTrig('acos', args),
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
[
|
|
269
|
+
'atan',
|
|
270
|
+
{
|
|
271
|
+
analyze: analyzeInverseTrig,
|
|
272
|
+
simplify: (_name, args) => simplifyInverseTrig('atan', args),
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
[
|
|
276
|
+
'atan2',
|
|
277
|
+
{
|
|
278
|
+
analyze: analyzeAtan2,
|
|
279
|
+
simplify: (_name, args) => simplifyAtan2(args),
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
[
|
|
283
|
+
'pow',
|
|
284
|
+
{
|
|
285
|
+
analyze: (args) => numberArguments(args, 2, 2),
|
|
286
|
+
simplify: (_name, args) => simplifyPow(args),
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
[
|
|
290
|
+
'sqrt',
|
|
291
|
+
{
|
|
292
|
+
analyze: (args) => numberArguments(args, 1, 1),
|
|
293
|
+
simplify: (_name, args) => simplifySqrt(args),
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
[
|
|
297
|
+
'hypot',
|
|
298
|
+
{
|
|
299
|
+
analyze: (args) => matchingArguments(args, 1, Infinity),
|
|
300
|
+
simplify: (_name, args) => simplifyHypot(args),
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
[
|
|
304
|
+
'log',
|
|
305
|
+
{
|
|
306
|
+
analyze: (args) => numberArguments(args, 1, 2),
|
|
307
|
+
simplify: (_name, args) => simplifyLog(args),
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
[
|
|
311
|
+
'exp',
|
|
312
|
+
{
|
|
313
|
+
analyze: (args) => numberArguments(args, 1, 1),
|
|
314
|
+
simplify: (_name, args) => simplifyExp(args),
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
])
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* @param {string} name
|
|
322
|
+
* @return {{normalizedName: string, definition: MathFunction} | undefined}
|
|
323
|
+
*/
|
|
324
|
+
function lookupMathFunction(name) {
|
|
325
|
+
const normalizedName = name.toLowerCase();
|
|
326
|
+
const definition = mathFunctions.get(normalizedName);
|
|
327
|
+
return definition === undefined ? undefined : { normalizedName, definition };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const mathFunctionNames = [...mathFunctions.keys()];
|
|
331
|
+
const QUICK_MATH_TEST = new RegExp(
|
|
332
|
+
`(?:${mathFunctionNames.join('|')})\\(`,
|
|
333
|
+
'i'
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
/** @param {string} name @return {boolean} */
|
|
337
|
+
function isCalculationFunction(name) {
|
|
338
|
+
return mathFunctions.get(name.toLowerCase())?.calculation === true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** @param {string} name @return {boolean} */
|
|
342
|
+
function isSupportedMathFunction(name) {
|
|
343
|
+
const normalizedName = name.toLowerCase();
|
|
344
|
+
const definition = mathFunctions.get(normalizedName);
|
|
345
|
+
return definition !== undefined && definition.calculation !== true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** @param {string} value @return {boolean} */
|
|
349
|
+
function hasPotentialMathFunction(value) {
|
|
350
|
+
return (
|
|
351
|
+
value.includes('(') && (QUICK_MATH_TEST.test(value) || value.includes('\\'))
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export {
|
|
356
|
+
addTypes,
|
|
357
|
+
mathFunctions,
|
|
358
|
+
lookupMathFunction,
|
|
359
|
+
QUICK_MATH_TEST,
|
|
360
|
+
isFailure,
|
|
361
|
+
isCalculationFunction,
|
|
362
|
+
isSupportedMathFunction,
|
|
363
|
+
hasPotentialMathFunction,
|
|
364
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** @typedef {import('./node.js').Node} Node */
|
|
2
|
+
|
|
3
|
+
const MAX_CALCULATION_DEPTH = 1024;
|
|
4
|
+
|
|
5
|
+
/** @param {number} depth @return {void} */
|
|
6
|
+
function assertDepth(depth) {
|
|
7
|
+
if (depth > MAX_CALCULATION_DEPTH) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
`Calculation nesting exceeds the limit of ${MAX_CALCULATION_DEPTH}`
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @param {unknown} part @param {number} depth @return {void} */
|
|
15
|
+
function checkOpaquePartDepth(part, depth) {
|
|
16
|
+
assertDepth(depth);
|
|
17
|
+
if (typeof part === 'string') return;
|
|
18
|
+
if (Array.isArray(part)) {
|
|
19
|
+
for (const child of part) checkOpaquePartDepth(child, depth + 1);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
checkCalculationDepth(/** @type {Node} */ (part), depth + 1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @param {Node} node @param {number} [depth] @return {void} */
|
|
26
|
+
function checkCalculationDepth(node, depth = 0) {
|
|
27
|
+
assertDepth(depth);
|
|
28
|
+
switch (node.type) {
|
|
29
|
+
case 'Sum':
|
|
30
|
+
for (const term of node.terms) {
|
|
31
|
+
checkCalculationDepth(term.node, depth + 1);
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
case 'Product':
|
|
35
|
+
for (const factor of node.factors) {
|
|
36
|
+
checkCalculationDepth(factor.node, depth + 1);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
case 'Call':
|
|
40
|
+
for (const child of node.args) checkCalculationDepth(child, depth + 1);
|
|
41
|
+
return;
|
|
42
|
+
case 'OpaqueCall':
|
|
43
|
+
for (const part of node.components) {
|
|
44
|
+
checkOpaquePartDepth(part, depth + 1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { MAX_CALCULATION_DEPTH, assertDepth, checkCalculationDepth };
|
package/src/lib/node.js
CHANGED
|
@@ -11,19 +11,25 @@
|
|
|
11
11
|
// - No Product directly contains another Product (flattened).
|
|
12
12
|
// - A Sum/Product with one positive element collapses to that element.
|
|
13
13
|
// - A Sum/Product with no elements collapses to Num(0) / Num(1).
|
|
14
|
-
// -
|
|
14
|
+
// - Positive zero-valued Nums are dropped from all-number sums. They are
|
|
15
|
+
// retained in mixed or unresolved sums because they constrain the other
|
|
16
|
+
// terms to <number>. Negative zero is retained until calculation
|
|
17
|
+
// evaluation has finished, because it is an IEEE-754 value with observable
|
|
18
|
+
// math-function behavior.
|
|
15
19
|
// Zero-valued Dims are kept — the unit carries type info.
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
22
|
* @typedef {{type: 'Num', value: number}} Num
|
|
19
|
-
* @typedef {{type: 'Dim', value: number, unit: string}} Dim
|
|
20
|
-
* @typedef {{type: 'Ident', name: string}} Ident
|
|
21
|
-
* @typedef {{type: 'Call', name: string, args: Node[]}} Call
|
|
23
|
+
* @typedef {{type: 'Dim', value: number, unit: string, rawUnit?: string}} Dim
|
|
24
|
+
* @typedef {{type: 'Ident', name: string, rawName?: string}} Ident
|
|
25
|
+
* @typedef {{type: 'Call', name: string, args: Node[], rawName?: string}} Call
|
|
26
|
+
* @typedef {string | Node | OpaqueComponent[]} OpaqueComponent
|
|
27
|
+
* @typedef {{type: 'OpaqueCall', name: string, components: OpaqueComponent[], rawName?: string}} OpaqueCall
|
|
22
28
|
* @typedef {{sign: 1 | -1, node: Node}} SumTerm Sign is always +1 when node is Num or Dim.
|
|
23
29
|
* @typedef {{type: 'Sum', terms: SumTerm[], grouped?: boolean}} Sum
|
|
24
30
|
* @typedef {{exponent: 1 | -1, node: Node}} ProductFactor exponent +1 = numerator, -1 = denominator.
|
|
25
31
|
* @typedef {{type: 'Product', factors: ProductFactor[]}} Product
|
|
26
|
-
* @typedef {Num | Dim | Ident | Call | Sum | Product} Node
|
|
32
|
+
* @typedef {Num | Dim | Ident | Call | OpaqueCall | Sum | Product} Node
|
|
27
33
|
*/
|
|
28
34
|
|
|
29
35
|
/**
|
|
@@ -37,27 +43,48 @@ function num(value) {
|
|
|
37
43
|
/**
|
|
38
44
|
* @param {number} value
|
|
39
45
|
* @param {string} unit
|
|
46
|
+
* @param {string} [rawUnit]
|
|
40
47
|
* @return {Dim}
|
|
41
48
|
*/
|
|
42
|
-
function dim(value, unit) {
|
|
43
|
-
return
|
|
49
|
+
function dim(value, unit, rawUnit) {
|
|
50
|
+
return rawUnit === undefined
|
|
51
|
+
? { type: 'Dim', value, unit }
|
|
52
|
+
: { type: 'Dim', value, unit, rawUnit };
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
/**
|
|
47
56
|
* @param {string} name
|
|
57
|
+
* @param {string} [rawName]
|
|
48
58
|
* @return {Ident}
|
|
49
59
|
*/
|
|
50
|
-
function ident(name) {
|
|
51
|
-
return
|
|
60
|
+
function ident(name, rawName) {
|
|
61
|
+
return rawName === undefined
|
|
62
|
+
? { type: 'Ident', name }
|
|
63
|
+
: { type: 'Ident', name, rawName };
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
/**
|
|
55
67
|
* @param {string} name
|
|
56
68
|
* @param {Node[]} args
|
|
69
|
+
* @param {string} [rawName]
|
|
57
70
|
* @return {Call}
|
|
58
71
|
*/
|
|
59
|
-
function call(name, args) {
|
|
60
|
-
return
|
|
72
|
+
function call(name, args, rawName) {
|
|
73
|
+
return rawName === undefined
|
|
74
|
+
? { type: 'Call', name, args }
|
|
75
|
+
: { type: 'Call', name, args, rawName };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} name
|
|
80
|
+
* @param {OpaqueComponent[]} components
|
|
81
|
+
* @param {string} [rawName]
|
|
82
|
+
* @return {OpaqueCall}
|
|
83
|
+
*/
|
|
84
|
+
function opaqueCall(name, components, rawName) {
|
|
85
|
+
return rawName === undefined
|
|
86
|
+
? { type: 'OpaqueCall', name, components }
|
|
87
|
+
: { type: 'OpaqueCall', name, components, rawName };
|
|
61
88
|
}
|
|
62
89
|
|
|
63
90
|
/**
|
|
@@ -67,13 +94,32 @@ function call(name, args) {
|
|
|
67
94
|
function mkSum(rawTerms) {
|
|
68
95
|
/** @type {SumTerm[]} */
|
|
69
96
|
const flat = [];
|
|
97
|
+
let hasNegativeZero = false;
|
|
70
98
|
for (const t of rawTerms) {
|
|
71
|
-
pushSumTerm(flat, t);
|
|
99
|
+
if (pushSumTerm(flat, t)) hasNegativeZero = true;
|
|
72
100
|
}
|
|
73
|
-
|
|
101
|
+
const zeroIsTypeAnchor = flat.some((term) => term.node.type !== 'Num');
|
|
102
|
+
// `+0 + -0` evaluates to +0. Keep positive zero terms when the sum also
|
|
103
|
+
// contains -0 so simplification can perform that IEEE-754 operation before
|
|
104
|
+
// the canonical zero-elision below.
|
|
105
|
+
let length = 0;
|
|
106
|
+
for (let i = 0; i < flat.length; i++) {
|
|
107
|
+
const term = flat[i];
|
|
108
|
+
if (
|
|
109
|
+
!hasNegativeZero &&
|
|
110
|
+
!zeroIsTypeAnchor &&
|
|
111
|
+
term.node.type === 'Num' &&
|
|
112
|
+
term.node.value === 0
|
|
113
|
+
) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
flat[length++] = term;
|
|
117
|
+
}
|
|
118
|
+
flat.length = length;
|
|
119
|
+
if (length === 0) {
|
|
74
120
|
return num(0);
|
|
75
121
|
}
|
|
76
|
-
if (
|
|
122
|
+
if (length === 1 && flat[0].sign === 1) {
|
|
77
123
|
return flat[0].node;
|
|
78
124
|
}
|
|
79
125
|
return { type: 'Sum', terms: flat };
|
|
@@ -82,39 +128,35 @@ function mkSum(rawTerms) {
|
|
|
82
128
|
/**
|
|
83
129
|
* @param {SumTerm[]} out
|
|
84
130
|
* @param {SumTerm} term
|
|
85
|
-
* @return {
|
|
131
|
+
* @return {boolean} Whether the appended terms contain negative zero.
|
|
86
132
|
*/
|
|
87
133
|
function pushSumTerm(out, term) {
|
|
88
134
|
let { sign, node } = term;
|
|
89
135
|
|
|
90
136
|
if (node.type === 'Sum' && !node.grouped) {
|
|
137
|
+
let hasNegativeZero = false;
|
|
91
138
|
for (const inner of node.terms) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
139
|
+
if (
|
|
140
|
+
pushSumTerm(out, {
|
|
141
|
+
sign: /** @type {1 | -1} */ (sign * inner.sign),
|
|
142
|
+
node: inner.node,
|
|
143
|
+
})
|
|
144
|
+
) {
|
|
145
|
+
hasNegativeZero = true;
|
|
146
|
+
}
|
|
96
147
|
}
|
|
97
|
-
return;
|
|
148
|
+
return hasNegativeZero;
|
|
98
149
|
}
|
|
99
150
|
|
|
100
151
|
// sign=-1 around a Num/Dim leaf collapses into the value's sign — the
|
|
101
152
|
// canonical-form rule downstream code relies on.
|
|
102
|
-
if (sign === -1) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
sign = 1;
|
|
106
|
-
} else if (node.type === 'Dim') {
|
|
107
|
-
node = dim(-node.value, node.unit);
|
|
108
|
-
sign = 1;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Drop zero-valued Nums. Dims with value 0 stay — the unit carries type.
|
|
113
|
-
if (node.type === 'Num' && node.value === 0) {
|
|
114
|
-
return;
|
|
153
|
+
if (sign === -1 && (node.type === 'Num' || node.type === 'Dim')) {
|
|
154
|
+
node = negate(node);
|
|
155
|
+
sign = 1;
|
|
115
156
|
}
|
|
116
157
|
|
|
117
158
|
out.push({ sign, node });
|
|
159
|
+
return node.type === 'Num' && Object.is(node.value, -0);
|
|
118
160
|
}
|
|
119
161
|
|
|
120
162
|
/**
|
|
@@ -169,22 +211,26 @@ function negate(node) {
|
|
|
169
211
|
return num(-node.value);
|
|
170
212
|
}
|
|
171
213
|
if (node.type === 'Dim') {
|
|
172
|
-
return dim(-node.value, node.unit);
|
|
214
|
+
return dim(-node.value, node.unit, node.rawUnit);
|
|
173
215
|
}
|
|
174
216
|
if (node.type === 'Sum') {
|
|
217
|
+
// A grouped sum may contain opaque terms whose meaning depends on the
|
|
218
|
+
// surrounding context. Keep the group intact so `-(a + b)` cannot turn
|
|
219
|
+
// into `-a - b` while it is still unresolved.
|
|
220
|
+
if (node.grouped) {
|
|
221
|
+
return mkSum([{ sign: -1, node }]);
|
|
222
|
+
}
|
|
175
223
|
const result = mkSum(
|
|
176
224
|
node.terms.map((t) => ({
|
|
177
225
|
sign: /** @type {1 | -1} */ (-t.sign),
|
|
178
226
|
node: t.node,
|
|
179
227
|
}))
|
|
180
228
|
);
|
|
181
|
-
return
|
|
182
|
-
? { ...result, grouped: true }
|
|
183
|
-
: result;
|
|
229
|
+
return result;
|
|
184
230
|
}
|
|
185
|
-
// Opaque (Ident, Call, Product): wrap as a single negative-sign term —
|
|
231
|
+
// Opaque (Ident, Call, OpaqueCall, Product): wrap as a single negative-sign term —
|
|
186
232
|
// the only case where sign=-1 remains on a SumTerm.
|
|
187
233
|
return mkSum([{ sign: -1, node }]);
|
|
188
234
|
}
|
|
189
235
|
|
|
190
|
-
export { num, dim, ident, call, mkSum, mkProduct, negate };
|
|
236
|
+
export { num, dim, ident, call, opaqueCall, mkSum, mkProduct, negate };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** @typedef {import('./node.js').Node} Node */
|
|
2
|
+
/** @typedef {import('./node.js').OpaqueComponent} OpaqueComponent */
|
|
3
|
+
|
|
4
|
+
/** @param {OpaqueComponent[]} components @param {(node: Node) => Node} simplify @return {OpaqueComponent[]} */
|
|
5
|
+
function simplifyComponents(components, simplify) {
|
|
6
|
+
return components.map((part) => {
|
|
7
|
+
if (typeof part === 'string') return part;
|
|
8
|
+
if (Array.isArray(part)) return simplifyComponents(part, simplify);
|
|
9
|
+
return simplify(part);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
/** @param {OpaqueComponent[]} components @param {string[]} buffer @param {(node: Node, buffer: string[]) => void} serialize @return {void} */
|
|
13
|
+
function serializeComponents(components, buffer, serialize) {
|
|
14
|
+
for (const part of components) {
|
|
15
|
+
if (typeof part === 'string') buffer.push(part);
|
|
16
|
+
else if (Array.isArray(part)) serializeComponents(part, buffer, serialize);
|
|
17
|
+
else serialize(part, buffer);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export { simplifyComponents, serializeComponents };
|