postcss-calc 11.1.2 → 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 -12
- 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/functions.js +364 -0
- package/src/lib/limits.js +49 -0
- package/src/lib/node.js +63 -21
- package/src/lib/opaque.js +12 -32
- package/src/lib/parser.js +383 -328
- package/src/lib/print.js +75 -0
- package/src/lib/regex.js +4 -0
- package/src/lib/scan.js +61 -0
- package/src/lib/serialize.js +574 -215
- package/src/lib/simplify/call.js +5 -98
- package/src/lib/simplify/round.js +4 -4
- package/src/lib/simplify/sum.js +14 -5
- package/src/lib/simplify.js +20 -5
- package/src/reduce.js +46 -115
- package/types/index.d.ts +4 -0
- package/types/lib/analyze.d.ts +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/functions.d.ts +40 -0
- package/types/lib/limits.d.ts +8 -0
- package/types/lib/node.d.ts +19 -3
- package/types/lib/opaque.d.ts +8 -14
- package/types/lib/parser.d.ts +42 -48
- package/types/lib/print.d.ts +16 -0
- package/types/lib/regex.d.ts +2 -0
- package/types/lib/scan.d.ts +35 -0
- package/types/lib/serialize.d.ts +19 -2
- package/types/lib/simplify/call.d.ts +4 -18
- package/types/lib/simplify/round.d.ts +3 -1
- package/types/lib/simplify.d.ts +5 -2
- package/types/reduce.d.ts +47 -6
- package/src/lib/tokenizer.js +0 -10
- package/types/lib/tokenizer.d.ts +0 -3
|
@@ -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,7 +11,11 @@
|
|
|
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
|
/**
|
|
@@ -19,11 +23,13 @@
|
|
|
19
23
|
* @typedef {{type: 'Dim', value: number, unit: string, rawUnit?: string}} Dim
|
|
20
24
|
* @typedef {{type: 'Ident', name: string, rawName?: string}} Ident
|
|
21
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
|
/**
|
|
@@ -69,6 +75,18 @@ function call(name, args, rawName) {
|
|
|
69
75
|
: { type: 'Call', name, args, rawName };
|
|
70
76
|
}
|
|
71
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 };
|
|
88
|
+
}
|
|
89
|
+
|
|
72
90
|
/**
|
|
73
91
|
* @param {SumTerm[]} rawTerms
|
|
74
92
|
* @return {Node}
|
|
@@ -76,13 +94,32 @@ function call(name, args, rawName) {
|
|
|
76
94
|
function mkSum(rawTerms) {
|
|
77
95
|
/** @type {SumTerm[]} */
|
|
78
96
|
const flat = [];
|
|
97
|
+
let hasNegativeZero = false;
|
|
79
98
|
for (const t of rawTerms) {
|
|
80
|
-
pushSumTerm(flat, t);
|
|
99
|
+
if (pushSumTerm(flat, t)) hasNegativeZero = true;
|
|
81
100
|
}
|
|
82
|
-
|
|
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) {
|
|
83
120
|
return num(0);
|
|
84
121
|
}
|
|
85
|
-
if (
|
|
122
|
+
if (length === 1 && flat[0].sign === 1) {
|
|
86
123
|
return flat[0].node;
|
|
87
124
|
}
|
|
88
125
|
return { type: 'Sum', terms: flat };
|
|
@@ -91,19 +128,24 @@ function mkSum(rawTerms) {
|
|
|
91
128
|
/**
|
|
92
129
|
* @param {SumTerm[]} out
|
|
93
130
|
* @param {SumTerm} term
|
|
94
|
-
* @return {
|
|
131
|
+
* @return {boolean} Whether the appended terms contain negative zero.
|
|
95
132
|
*/
|
|
96
133
|
function pushSumTerm(out, term) {
|
|
97
134
|
let { sign, node } = term;
|
|
98
135
|
|
|
99
136
|
if (node.type === 'Sum' && !node.grouped) {
|
|
137
|
+
let hasNegativeZero = false;
|
|
100
138
|
for (const inner of node.terms) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
139
|
+
if (
|
|
140
|
+
pushSumTerm(out, {
|
|
141
|
+
sign: /** @type {1 | -1} */ (sign * inner.sign),
|
|
142
|
+
node: inner.node,
|
|
143
|
+
})
|
|
144
|
+
) {
|
|
145
|
+
hasNegativeZero = true;
|
|
146
|
+
}
|
|
105
147
|
}
|
|
106
|
-
return;
|
|
148
|
+
return hasNegativeZero;
|
|
107
149
|
}
|
|
108
150
|
|
|
109
151
|
// sign=-1 around a Num/Dim leaf collapses into the value's sign — the
|
|
@@ -113,12 +155,8 @@ function pushSumTerm(out, term) {
|
|
|
113
155
|
sign = 1;
|
|
114
156
|
}
|
|
115
157
|
|
|
116
|
-
// Drop zero-valued Nums. Dims with value 0 stay — the unit carries type.
|
|
117
|
-
if (node.type === 'Num' && node.value === 0) {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
158
|
out.push({ sign, node });
|
|
159
|
+
return node.type === 'Num' && Object.is(node.value, -0);
|
|
122
160
|
}
|
|
123
161
|
|
|
124
162
|
/**
|
|
@@ -176,19 +214,23 @@ function negate(node) {
|
|
|
176
214
|
return dim(-node.value, node.unit, node.rawUnit);
|
|
177
215
|
}
|
|
178
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
|
+
}
|
|
179
223
|
const result = mkSum(
|
|
180
224
|
node.terms.map((t) => ({
|
|
181
225
|
sign: /** @type {1 | -1} */ (-t.sign),
|
|
182
226
|
node: t.node,
|
|
183
227
|
}))
|
|
184
228
|
);
|
|
185
|
-
return
|
|
186
|
-
? { ...result, grouped: true }
|
|
187
|
-
: result;
|
|
229
|
+
return result;
|
|
188
230
|
}
|
|
189
|
-
// Opaque (Ident, Call, Product): wrap as a single negative-sign term —
|
|
231
|
+
// Opaque (Ident, Call, OpaqueCall, Product): wrap as a single negative-sign term —
|
|
190
232
|
// the only case where sign=-1 remains on a SumTerm.
|
|
191
233
|
return mkSum([{ sign: -1, node }]);
|
|
192
234
|
}
|
|
193
235
|
|
|
194
|
-
export { num, dim, ident, call, mkSum, mkProduct, negate };
|
|
236
|
+
export { num, dim, ident, call, opaqueCall, mkSum, mkProduct, negate };
|
package/src/lib/opaque.js
CHANGED
|
@@ -1,40 +1,20 @@
|
|
|
1
|
-
// Private metadata for opaque function contents (including var() fallbacks).
|
|
2
|
-
// Keeping it in a WeakMap means the public calculation AST remains unchanged.
|
|
3
1
|
/** @typedef {import('./node.js').Node} Node */
|
|
4
|
-
/** @typedef {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
components.set(node, tree);
|
|
10
|
-
return node;
|
|
11
|
-
}
|
|
12
|
-
/** @param {Extract<Node, {type: 'Call'}>} node */
|
|
13
|
-
function getComponents(node) {
|
|
14
|
-
return components.get(node);
|
|
15
|
-
}
|
|
16
|
-
/** @param {Component[]} tree @param {(node: Node) => Node} simplify @return {Component[]} */
|
|
17
|
-
function simplifyComponents(tree, simplify) {
|
|
18
|
-
return tree.map((part) => {
|
|
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) => {
|
|
19
7
|
if (typeof part === 'string') return part;
|
|
20
8
|
if (Array.isArray(part)) return simplifyComponents(part, simplify);
|
|
21
9
|
return simplify(part);
|
|
22
10
|
});
|
|
23
11
|
}
|
|
24
|
-
/** @param {
|
|
25
|
-
function serializeComponents(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
else
|
|
30
|
-
result += serializeComponents(part, serialize);
|
|
31
|
-
else result += serialize(part);
|
|
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);
|
|
32
18
|
}
|
|
33
|
-
return result;
|
|
34
19
|
}
|
|
35
|
-
export {
|
|
36
|
-
getComponents,
|
|
37
|
-
setComponents,
|
|
38
|
-
simplifyComponents,
|
|
39
|
-
serializeComponents,
|
|
40
|
-
};
|
|
20
|
+
export { simplifyComponents, serializeComponents };
|