@texuli/core 0.7.0 → 0.8.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 +11 -5
- package/dist/data-source/collection.d.ts +67 -0
- package/dist/data-source/collection.js +247 -0
- package/dist/data-source/errors.d.ts +58 -0
- package/dist/data-source/errors.js +138 -0
- package/dist/data-source/index.d.ts +17 -0
- package/dist/data-source/index.js +17 -0
- package/dist/data-source/loader.d.ts +120 -0
- package/dist/data-source/loader.js +571 -0
- package/dist/data-source/query.d.ts +78 -0
- package/dist/data-source/query.js +339 -0
- package/dist/data-source/registry.d.ts +83 -0
- package/dist/data-source/registry.js +63 -0
- package/dist/data-source/schema.d.ts +122 -0
- package/dist/data-source/schema.js +197 -0
- package/dist/expression/analyze.d.ts +17 -0
- package/dist/expression/analyze.js +151 -0
- package/dist/expression/evaluate.d.ts +55 -0
- package/dist/expression/evaluate.js +365 -0
- package/dist/expression/functions.d.ts +7 -0
- package/dist/expression/functions.js +374 -0
- package/dist/expression/index.d.ts +7 -0
- package/dist/expression/index.js +12 -0
- package/dist/expression/parse.d.ts +19 -0
- package/dist/expression/parse.js +284 -0
- package/dist/expression/text.d.ts +15 -0
- package/dist/expression/text.js +489 -0
- package/dist/expression/types.d.ts +197 -0
- package/dist/expression/types.js +35 -0
- package/dist/expression/values.d.ts +14 -0
- package/dist/expression/values.js +68 -0
- package/dist/i18n/catalog.d.ts +51 -0
- package/dist/i18n/catalog.js +109 -0
- package/dist/i18n/format.d.ts +41 -0
- package/dist/i18n/format.js +117 -0
- package/dist/i18n/i18n.d.ts +70 -0
- package/dist/i18n/i18n.js +185 -0
- package/dist/i18n/index.d.ts +8 -0
- package/dist/i18n/index.js +8 -0
- package/dist/i18n/pseudo.d.ts +4 -0
- package/dist/i18n/pseudo.js +41 -0
- package/package.json +4 -1
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { CORE_FUNCTIONS } from "./functions.js";
|
|
2
|
+
import { EXPRESSION_LIMITS, } from "./types.js";
|
|
3
|
+
import { ERRORS, isExpressionError, LIMIT, roundHalfAway, toExpressionValue, VALUE, } from "./values.js";
|
|
4
|
+
/** Make a budget for one recalculation pass. */
|
|
5
|
+
export function createExpressionBudget(limit = EXPRESSION_LIMITS.passSteps) {
|
|
6
|
+
const cap = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 0;
|
|
7
|
+
let used = 0;
|
|
8
|
+
let exhausted = cap === 0;
|
|
9
|
+
return {
|
|
10
|
+
limit: cap,
|
|
11
|
+
get used() {
|
|
12
|
+
return used;
|
|
13
|
+
},
|
|
14
|
+
get exhausted() {
|
|
15
|
+
return exhausted;
|
|
16
|
+
},
|
|
17
|
+
spend(steps) {
|
|
18
|
+
if (exhausted)
|
|
19
|
+
return false;
|
|
20
|
+
used += steps;
|
|
21
|
+
if (used > cap)
|
|
22
|
+
exhausted = true;
|
|
23
|
+
return !exhausted;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function charge(run, steps) {
|
|
28
|
+
if (run.exhausted)
|
|
29
|
+
return false;
|
|
30
|
+
const cost = Number.isFinite(steps) && steps > 0 ? Math.ceil(steps) : 0;
|
|
31
|
+
run.left -= cost;
|
|
32
|
+
if (run.left < 0 || (run.budget && !run.budget.spend(cost)))
|
|
33
|
+
run.exhausted = true;
|
|
34
|
+
return !run.exhausted;
|
|
35
|
+
}
|
|
36
|
+
const isList = (value) => Array.isArray(value);
|
|
37
|
+
function num(run, value) {
|
|
38
|
+
if (isExpressionError(value))
|
|
39
|
+
return value;
|
|
40
|
+
if (typeof value === 'number')
|
|
41
|
+
return value;
|
|
42
|
+
if (typeof value === 'boolean')
|
|
43
|
+
return value ? 1 : 0;
|
|
44
|
+
if (value === null)
|
|
45
|
+
return run.zero ? 0 : null;
|
|
46
|
+
return VALUE;
|
|
47
|
+
}
|
|
48
|
+
function finite(value) {
|
|
49
|
+
if (!Number.isFinite(value))
|
|
50
|
+
return ERRORS['#NUM!'];
|
|
51
|
+
return value === 0 ? 0 : value;
|
|
52
|
+
}
|
|
53
|
+
/** Kleene truth: true, false, null (unknown) or an error. */
|
|
54
|
+
function truth(value) {
|
|
55
|
+
if (isExpressionError(value))
|
|
56
|
+
return value;
|
|
57
|
+
if (typeof value === 'boolean')
|
|
58
|
+
return value;
|
|
59
|
+
if (value === null)
|
|
60
|
+
return null;
|
|
61
|
+
if (typeof value === 'number')
|
|
62
|
+
return value !== 0;
|
|
63
|
+
return VALUE;
|
|
64
|
+
}
|
|
65
|
+
function compare(run, op, a, b) {
|
|
66
|
+
if (isExpressionError(a))
|
|
67
|
+
return a;
|
|
68
|
+
if (isExpressionError(b))
|
|
69
|
+
return b;
|
|
70
|
+
if (run.zero) {
|
|
71
|
+
if (a === null)
|
|
72
|
+
a = typeof b === 'string' ? '' : typeof b === 'boolean' ? false : 0;
|
|
73
|
+
if (b === null)
|
|
74
|
+
b = typeof a === 'string' ? '' : typeof a === 'boolean' ? false : 0;
|
|
75
|
+
}
|
|
76
|
+
if (op === 'eq' || op === 'neq') {
|
|
77
|
+
const same = equal(run, a, b);
|
|
78
|
+
if (isExpressionError(same))
|
|
79
|
+
return same;
|
|
80
|
+
return op === 'eq' ? same : !same;
|
|
81
|
+
}
|
|
82
|
+
if (a === null || b === null)
|
|
83
|
+
return null;
|
|
84
|
+
let order;
|
|
85
|
+
if (typeof a === 'number' && typeof b === 'number')
|
|
86
|
+
order = a < b ? -1 : a > b ? 1 : 0;
|
|
87
|
+
else if (typeof a === 'string' && typeof b === 'string') {
|
|
88
|
+
if (!charge(run, Math.min(a.length, b.length) / 16))
|
|
89
|
+
return LIMIT;
|
|
90
|
+
const x = run.ci ? a.toLowerCase() : a;
|
|
91
|
+
const y = run.ci ? b.toLowerCase() : b;
|
|
92
|
+
order = x < y ? -1 : x > y ? 1 : 0;
|
|
93
|
+
}
|
|
94
|
+
else if (typeof a === 'boolean' && typeof b === 'boolean')
|
|
95
|
+
order = Number(a) - Number(b);
|
|
96
|
+
else
|
|
97
|
+
return VALUE;
|
|
98
|
+
if (op === 'lt')
|
|
99
|
+
return order < 0;
|
|
100
|
+
if (op === 'lte')
|
|
101
|
+
return order <= 0;
|
|
102
|
+
if (op === 'gt')
|
|
103
|
+
return order > 0;
|
|
104
|
+
return order >= 0;
|
|
105
|
+
}
|
|
106
|
+
function equal(run, a, b) {
|
|
107
|
+
if (isList(a) || isList(b)) {
|
|
108
|
+
if (!isList(a) || !isList(b))
|
|
109
|
+
return false;
|
|
110
|
+
if (a.length !== b.length)
|
|
111
|
+
return false;
|
|
112
|
+
if (!charge(run, a.length))
|
|
113
|
+
return LIMIT;
|
|
114
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
115
|
+
const same = equal(run, a[index] ?? null, b[index] ?? null);
|
|
116
|
+
if (same !== true)
|
|
117
|
+
return same;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (typeof a === 'string' && typeof b === 'string') {
|
|
122
|
+
if (!charge(run, Math.min(a.length, b.length) / 16))
|
|
123
|
+
return LIMIT;
|
|
124
|
+
return run.ci ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
125
|
+
}
|
|
126
|
+
return a === b;
|
|
127
|
+
}
|
|
128
|
+
function text(value) {
|
|
129
|
+
if (value === null)
|
|
130
|
+
return '';
|
|
131
|
+
return String(value);
|
|
132
|
+
}
|
|
133
|
+
function evaluate(run, node, depth) {
|
|
134
|
+
if (depth > EXPRESSION_LIMITS.depth || !charge(run, 1))
|
|
135
|
+
return LIMIT;
|
|
136
|
+
const next = depth + 1;
|
|
137
|
+
switch (node.op) {
|
|
138
|
+
case 'num':
|
|
139
|
+
case 'str':
|
|
140
|
+
case 'bool':
|
|
141
|
+
return node.value;
|
|
142
|
+
case 'null':
|
|
143
|
+
return null;
|
|
144
|
+
case 'var':
|
|
145
|
+
case 'field':
|
|
146
|
+
return run.env.field ? toExpressionValue(run.env.field(node.name)) : null;
|
|
147
|
+
case 'ref':
|
|
148
|
+
return run.env.ref ? toExpressionValue(run.env.ref(node.ref)) : ERRORS['#REF!'];
|
|
149
|
+
case 'range': {
|
|
150
|
+
if (!run.env.range)
|
|
151
|
+
return ERRORS['#REF!'];
|
|
152
|
+
const value = toExpressionValue(run.env.range(node.from, node.to));
|
|
153
|
+
if (isList(value) && !charge(run, value.length))
|
|
154
|
+
return LIMIT;
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
case 'add':
|
|
158
|
+
case 'sub':
|
|
159
|
+
case 'mul':
|
|
160
|
+
case 'div':
|
|
161
|
+
case 'mod': {
|
|
162
|
+
const a = num(run, evaluate(run, node.left, next));
|
|
163
|
+
if (isExpressionError(a))
|
|
164
|
+
return a;
|
|
165
|
+
const b = num(run, evaluate(run, node.right, next));
|
|
166
|
+
if (isExpressionError(b))
|
|
167
|
+
return b;
|
|
168
|
+
if (a === null || b === null)
|
|
169
|
+
return null;
|
|
170
|
+
if (node.op === 'add')
|
|
171
|
+
return finite(a + b);
|
|
172
|
+
if (node.op === 'sub')
|
|
173
|
+
return finite(a - b);
|
|
174
|
+
if (node.op === 'mul')
|
|
175
|
+
return finite(a * b);
|
|
176
|
+
if (b === 0)
|
|
177
|
+
return ERRORS['#DIV/0!'];
|
|
178
|
+
if (node.op === 'div')
|
|
179
|
+
return finite(a / b);
|
|
180
|
+
return finite(a - b * Math.floor(a / b));
|
|
181
|
+
}
|
|
182
|
+
case 'pow': {
|
|
183
|
+
const a = num(run, evaluate(run, node.base, next));
|
|
184
|
+
if (isExpressionError(a))
|
|
185
|
+
return a;
|
|
186
|
+
const b = num(run, evaluate(run, node.exponent, next));
|
|
187
|
+
if (isExpressionError(b))
|
|
188
|
+
return b;
|
|
189
|
+
if (a === null || b === null)
|
|
190
|
+
return null;
|
|
191
|
+
if (Math.abs(b) > EXPRESSION_LIMITS.exponent)
|
|
192
|
+
return ERRORS['#NUM!'];
|
|
193
|
+
return finite(a ** b);
|
|
194
|
+
}
|
|
195
|
+
case 'neg': {
|
|
196
|
+
const a = num(run, evaluate(run, node.value, next));
|
|
197
|
+
if (isExpressionError(a) || a === null)
|
|
198
|
+
return a;
|
|
199
|
+
return finite(-a);
|
|
200
|
+
}
|
|
201
|
+
case 'lt':
|
|
202
|
+
case 'lte':
|
|
203
|
+
case 'gt':
|
|
204
|
+
case 'gte':
|
|
205
|
+
case 'eq':
|
|
206
|
+
case 'neq': {
|
|
207
|
+
const a = evaluate(run, node.left, next);
|
|
208
|
+
if (isExpressionError(a))
|
|
209
|
+
return a;
|
|
210
|
+
return compare(run, node.op, a, evaluate(run, node.right, next));
|
|
211
|
+
}
|
|
212
|
+
case 'and':
|
|
213
|
+
case 'or': {
|
|
214
|
+
const stop = node.op === 'or';
|
|
215
|
+
let unknown = false;
|
|
216
|
+
for (const arg of node.args) {
|
|
217
|
+
const value = truth(evaluate(run, arg, next));
|
|
218
|
+
if (isExpressionError(value))
|
|
219
|
+
return value;
|
|
220
|
+
if (value === stop)
|
|
221
|
+
return stop;
|
|
222
|
+
if (value === null)
|
|
223
|
+
unknown = true;
|
|
224
|
+
}
|
|
225
|
+
return unknown ? null : !stop;
|
|
226
|
+
}
|
|
227
|
+
case 'not': {
|
|
228
|
+
const value = truth(evaluate(run, node.value, next));
|
|
229
|
+
if (isExpressionError(value) || value === null)
|
|
230
|
+
return value;
|
|
231
|
+
return !value;
|
|
232
|
+
}
|
|
233
|
+
case 'if': {
|
|
234
|
+
const test = truth(evaluate(run, node.test, next));
|
|
235
|
+
if (isExpressionError(test))
|
|
236
|
+
return test;
|
|
237
|
+
return evaluate(run, test === true ? node.ifTrue : node.ifFalse, next);
|
|
238
|
+
}
|
|
239
|
+
case 'min':
|
|
240
|
+
case 'max': {
|
|
241
|
+
let result = null;
|
|
242
|
+
for (const arg of node.args) {
|
|
243
|
+
const value = evaluate(run, arg, next);
|
|
244
|
+
if (isExpressionError(value))
|
|
245
|
+
return value;
|
|
246
|
+
const items = isList(value) ? value : [value];
|
|
247
|
+
for (const item of items) {
|
|
248
|
+
if (isList(value) && typeof item !== 'number')
|
|
249
|
+
continue;
|
|
250
|
+
const n = num(run, item);
|
|
251
|
+
if (isExpressionError(n))
|
|
252
|
+
return n;
|
|
253
|
+
if (n === null) {
|
|
254
|
+
if (isList(value))
|
|
255
|
+
continue;
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
result =
|
|
259
|
+
result === null ? n : node.op === 'min' ? Math.min(result, n) : Math.max(result, n);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
case 'concat': {
|
|
265
|
+
let out = '';
|
|
266
|
+
for (const arg of node.args) {
|
|
267
|
+
const value = evaluate(run, arg, next);
|
|
268
|
+
if (isExpressionError(value))
|
|
269
|
+
return value;
|
|
270
|
+
const part = isList(value) ? value.map(text).join(', ') : text(value);
|
|
271
|
+
if (out.length + part.length > EXPRESSION_LIMITS.stringLength)
|
|
272
|
+
return LIMIT;
|
|
273
|
+
if (!charge(run, part.length / 16))
|
|
274
|
+
return LIMIT;
|
|
275
|
+
out += part;
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
case 'round': {
|
|
280
|
+
const value = num(run, evaluate(run, node.value, next));
|
|
281
|
+
if (isExpressionError(value) || value === null)
|
|
282
|
+
return value;
|
|
283
|
+
const rounded = roundHalfAway(value, node.digits ?? 0);
|
|
284
|
+
return rounded === null ? ERRORS['#NUM!'] : rounded;
|
|
285
|
+
}
|
|
286
|
+
case 'clamp': {
|
|
287
|
+
const value = num(run, evaluate(run, node.value, next));
|
|
288
|
+
if (isExpressionError(value))
|
|
289
|
+
return value;
|
|
290
|
+
const min = num(run, evaluate(run, node.min, next));
|
|
291
|
+
if (isExpressionError(min))
|
|
292
|
+
return min;
|
|
293
|
+
const max = num(run, evaluate(run, node.max, next));
|
|
294
|
+
if (isExpressionError(max))
|
|
295
|
+
return max;
|
|
296
|
+
if (value === null || min === null || max === null)
|
|
297
|
+
return null;
|
|
298
|
+
if (min > max)
|
|
299
|
+
return ERRORS['#NUM!'];
|
|
300
|
+
return Math.min(max, Math.max(min, value));
|
|
301
|
+
}
|
|
302
|
+
case 'call': {
|
|
303
|
+
const fn = Object.hasOwn(run.functions, node.fn) ? run.functions[node.fn] : undefined;
|
|
304
|
+
if (!fn || node.args.length < fn.minArgs || node.args.length > fn.maxArgs) {
|
|
305
|
+
return ERRORS['#NAME?'];
|
|
306
|
+
}
|
|
307
|
+
const args = [];
|
|
308
|
+
for (const arg of node.args) {
|
|
309
|
+
const value = evaluate(run, arg, next);
|
|
310
|
+
if (run.exhausted)
|
|
311
|
+
return LIMIT;
|
|
312
|
+
if (isExpressionError(value) && !fn.acceptsErrors)
|
|
313
|
+
return value;
|
|
314
|
+
args.push(value);
|
|
315
|
+
}
|
|
316
|
+
let result;
|
|
317
|
+
try {
|
|
318
|
+
result = fn.call(Object.freeze(args), run.context);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return VALUE;
|
|
322
|
+
}
|
|
323
|
+
if (run.exhausted)
|
|
324
|
+
return LIMIT;
|
|
325
|
+
const value = toExpressionValue(result);
|
|
326
|
+
if (isList(value) && !charge(run, value.length))
|
|
327
|
+
return LIMIT;
|
|
328
|
+
return value;
|
|
329
|
+
}
|
|
330
|
+
default:
|
|
331
|
+
return VALUE;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Evaluate a validated expression (from `parseExpression` or
|
|
336
|
+
* `parseExpressionText`). Never throws: errors are values, and running past
|
|
337
|
+
* the per-expression or per-pass step limit is `#LIMIT!`.
|
|
338
|
+
*/
|
|
339
|
+
export function evaluateExpression(expression, env = {}, options = {}) {
|
|
340
|
+
const cap = Math.min(EXPRESSION_LIMITS.steps, options.steps !== undefined && Number.isFinite(options.steps)
|
|
341
|
+
? Math.max(0, options.steps)
|
|
342
|
+
: Infinity);
|
|
343
|
+
const budget = options.budget;
|
|
344
|
+
if (budget?.exhausted)
|
|
345
|
+
return LIMIT;
|
|
346
|
+
const run = {
|
|
347
|
+
left: cap,
|
|
348
|
+
exhausted: false,
|
|
349
|
+
budget,
|
|
350
|
+
env,
|
|
351
|
+
functions: options.functions ?? CORE_FUNCTIONS,
|
|
352
|
+
zero: options.blank === 'zero',
|
|
353
|
+
ci: options.caseInsensitive === true,
|
|
354
|
+
context: {
|
|
355
|
+
charge: (steps) => charge(run, steps),
|
|
356
|
+
now: () => {
|
|
357
|
+
const raw = env.now?.();
|
|
358
|
+
const ms = raw instanceof Date ? raw.getTime() : raw;
|
|
359
|
+
return typeof ms === 'number' && Number.isFinite(ms) ? ms : undefined;
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
const value = evaluate(run, expression.ast, 1);
|
|
364
|
+
return run.exhausted ? LIMIT : value;
|
|
365
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ExpressionFunctionTable } from './types.js';
|
|
2
|
+
/** Days since 1970-01-01 for a valid `YYYY-MM-DD`, else null. */
|
|
3
|
+
export declare function isoDateToDays(value: unknown): number | null;
|
|
4
|
+
/** The `YYYY-MM-DD` of a day count since 1970-01-01 (years 0000–9999), else null. */
|
|
5
|
+
export declare function daysToIsoDate(days: number): string | null;
|
|
6
|
+
/** The core function table (ADR-015). Names are case-sensitive. */
|
|
7
|
+
export declare const CORE_FUNCTIONS: ExpressionFunctionTable;
|