@elwood-lang/core 0.1.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/dist/ast.d.ts +237 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +2 -0
- package/dist/ast.js.map +1 -0
- package/dist/evaluator.d.ts +4 -0
- package/dist/evaluator.d.ts.map +1 -0
- package/dist/evaluator.js +879 -0
- package/dist/evaluator.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/lexer.d.ts +14 -0
- package/dist/lexer.d.ts.map +1 -0
- package/dist/lexer.js +279 -0
- package/dist/lexer.js.map +1 -0
- package/dist/parser.d.ts +15 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +610 -0
- package/dist/parser.js.map +1 -0
- package/dist/scope.d.ts +13 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +27 -0
- package/dist/scope.js.map +1 -0
- package/dist/token.d.ts +65 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +66 -0
- package/dist/token.js.map +1 -0
- package/package.json +36 -0
- package/src/ast.ts +235 -0
- package/src/evaluator.ts +832 -0
- package/src/index.ts +78 -0
- package/src/lexer.ts +300 -0
- package/src/parser.ts +626 -0
- package/src/scope.ts +26 -0
- package/src/token.ts +87 -0
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
import { Scope } from './scope.js';
|
|
2
|
+
// ── Helpers ──
|
|
3
|
+
function isArray(v) { return Array.isArray(v); }
|
|
4
|
+
function isObject(v) {
|
|
5
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
6
|
+
}
|
|
7
|
+
function toArray(v) {
|
|
8
|
+
if (isArray(v))
|
|
9
|
+
return v;
|
|
10
|
+
return [v];
|
|
11
|
+
}
|
|
12
|
+
function isTruthy(v) {
|
|
13
|
+
if (v === null || v === undefined)
|
|
14
|
+
return false;
|
|
15
|
+
if (typeof v === 'boolean')
|
|
16
|
+
return v;
|
|
17
|
+
if (typeof v === 'number')
|
|
18
|
+
return v !== 0;
|
|
19
|
+
if (typeof v === 'string')
|
|
20
|
+
return v !== '';
|
|
21
|
+
if (isArray(v))
|
|
22
|
+
return v.length > 0;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
function valuesEqual(a, b) {
|
|
26
|
+
if (a === b)
|
|
27
|
+
return true;
|
|
28
|
+
if (a === null || b === null)
|
|
29
|
+
return a === b;
|
|
30
|
+
if (typeof a !== typeof b)
|
|
31
|
+
return false;
|
|
32
|
+
if (typeof a === 'number' && typeof b === 'number')
|
|
33
|
+
return Math.abs(a - b) < 1e-10;
|
|
34
|
+
if (isArray(a) && isArray(b)) {
|
|
35
|
+
if (a.length !== b.length)
|
|
36
|
+
return false;
|
|
37
|
+
return a.every((v, i) => valuesEqual(v, b[i]));
|
|
38
|
+
}
|
|
39
|
+
if (isObject(a) && isObject(b)) {
|
|
40
|
+
const ka = Object.keys(a), kb = Object.keys(b);
|
|
41
|
+
if (ka.length !== kb.length)
|
|
42
|
+
return false;
|
|
43
|
+
return ka.every(k => valuesEqual(a[k], b[k]));
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
function serialize(v) {
|
|
48
|
+
if (v === null || v === undefined)
|
|
49
|
+
return 'null';
|
|
50
|
+
if (typeof v === 'string')
|
|
51
|
+
return JSON.stringify(v);
|
|
52
|
+
if (typeof v === 'number' || typeof v === 'boolean')
|
|
53
|
+
return String(v);
|
|
54
|
+
return JSON.stringify(v);
|
|
55
|
+
}
|
|
56
|
+
function valueToString(v) {
|
|
57
|
+
if (v === null || v === undefined)
|
|
58
|
+
return '';
|
|
59
|
+
if (typeof v === 'string')
|
|
60
|
+
return v;
|
|
61
|
+
if (typeof v === 'number')
|
|
62
|
+
return String(v);
|
|
63
|
+
if (typeof v === 'boolean')
|
|
64
|
+
return v ? 'true' : 'false';
|
|
65
|
+
return JSON.stringify(v);
|
|
66
|
+
}
|
|
67
|
+
function getProperty(obj, name) {
|
|
68
|
+
if (isObject(obj))
|
|
69
|
+
return obj[name] ?? null;
|
|
70
|
+
// Auto-map over arrays
|
|
71
|
+
if (isArray(obj))
|
|
72
|
+
return obj.map(item => getProperty(item, name)).filter(v => v !== null);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function levenshtein(a, b) {
|
|
76
|
+
const m = a.length, n = b.length;
|
|
77
|
+
const d = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
78
|
+
for (let i = 0; i <= m; i++)
|
|
79
|
+
d[i][0] = i;
|
|
80
|
+
for (let j = 0; j <= n; j++)
|
|
81
|
+
d[0][j] = j;
|
|
82
|
+
for (let i = 1; i <= m; i++)
|
|
83
|
+
for (let j = 1; j <= n; j++)
|
|
84
|
+
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
85
|
+
return d[m][n];
|
|
86
|
+
}
|
|
87
|
+
function suggestProperty(attempted, obj) {
|
|
88
|
+
if (!isObject(obj))
|
|
89
|
+
return undefined;
|
|
90
|
+
const names = Object.keys(obj);
|
|
91
|
+
if (names.length === 0)
|
|
92
|
+
return undefined;
|
|
93
|
+
const closest = names
|
|
94
|
+
.map(n => ({ n, d: levenshtein(attempted.toLowerCase(), n.toLowerCase()) }))
|
|
95
|
+
.filter(x => x.d <= 3)
|
|
96
|
+
.sort((a, b) => a.d - b.d)[0];
|
|
97
|
+
if (closest)
|
|
98
|
+
return `Did you mean '${closest.n}'? Available: ${names.slice(0, 10).join(', ')}`;
|
|
99
|
+
return `Available properties: ${names.slice(0, 10).join(', ')}`;
|
|
100
|
+
}
|
|
101
|
+
// ── Memoized Function ──
|
|
102
|
+
class MemoizedFunction {
|
|
103
|
+
lambda;
|
|
104
|
+
closure;
|
|
105
|
+
evalFn;
|
|
106
|
+
cache = new Map();
|
|
107
|
+
constructor(lambda, closure, evalFn) {
|
|
108
|
+
this.lambda = lambda;
|
|
109
|
+
this.closure = closure;
|
|
110
|
+
this.evalFn = evalFn;
|
|
111
|
+
}
|
|
112
|
+
invoke(args, _current) {
|
|
113
|
+
const key = args.map(serialize).join('|');
|
|
114
|
+
if (this.cache.has(key))
|
|
115
|
+
return this.cache.get(key);
|
|
116
|
+
const childScope = this.closure.child();
|
|
117
|
+
for (let i = 0; i < this.lambda.parameters.length && i < args.length; i++)
|
|
118
|
+
childScope.set(this.lambda.parameters[i], args[i]);
|
|
119
|
+
const root = this.closure.get('$root') ?? _current;
|
|
120
|
+
const result = this.evalFn(this.lambda.body, root, childScope);
|
|
121
|
+
this.cache.set(key, result);
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// ── Main Evaluator ──
|
|
126
|
+
export function evaluateExpression(expr, input) {
|
|
127
|
+
const scope = new Scope();
|
|
128
|
+
scope.set('$root', input);
|
|
129
|
+
return evaluate(expr, input, scope);
|
|
130
|
+
}
|
|
131
|
+
export function evaluateScript(script, input) {
|
|
132
|
+
const scope = new Scope();
|
|
133
|
+
scope.set('$root', input);
|
|
134
|
+
for (const binding of script.bindings) {
|
|
135
|
+
scope.set(binding.name, evaluate(binding.value, input, scope));
|
|
136
|
+
}
|
|
137
|
+
if (script.returnExpression)
|
|
138
|
+
return evaluate(script.returnExpression, input, scope);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
function evaluate(expr, current, scope) {
|
|
142
|
+
switch (expr.type) {
|
|
143
|
+
case 'Literal': return expr.value;
|
|
144
|
+
case 'Path': return evalPath(expr, current, scope);
|
|
145
|
+
case 'Identifier': return evalIdentifier(expr, scope);
|
|
146
|
+
case 'Binary': return evalBinary(expr, current, scope);
|
|
147
|
+
case 'Unary': return evalUnary(expr, current, scope);
|
|
148
|
+
case 'If': return isTruthy(evaluate(expr.condition, current, scope))
|
|
149
|
+
? evaluate(expr.thenBranch, current, scope)
|
|
150
|
+
: evaluate(expr.elseBranch, current, scope);
|
|
151
|
+
case 'Object': return evalObject(expr, current, scope);
|
|
152
|
+
case 'Array': return expr.items.map(i => evaluate(i, current, scope));
|
|
153
|
+
case 'Pipeline': return evalPipeline(expr, current, scope);
|
|
154
|
+
case 'MemberAccess': return evalMemberAccess(expr, current, scope);
|
|
155
|
+
case 'MethodCall': return evalMethodCall(expr, current, scope);
|
|
156
|
+
case 'FunctionCall': return evalFunctionCall(expr, current, scope);
|
|
157
|
+
case 'Index': return evalIndex(expr, current, scope);
|
|
158
|
+
case 'InterpolatedString': return evalInterpolation(expr, current, scope);
|
|
159
|
+
case 'Match': return evalMatch(expr, current, scope);
|
|
160
|
+
case 'Memo': return new MemoizedFunction(expr.lambda, scope, evaluate);
|
|
161
|
+
case 'Lambda': throw new Error('Lambda cannot be evaluated directly');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// ── Path evaluation ──
|
|
165
|
+
function evalPath(expr, current, scope) {
|
|
166
|
+
let value = expr.isRooted ? (scope.get('$root') ?? current) : current;
|
|
167
|
+
for (const seg of expr.segments) {
|
|
168
|
+
switch (seg.type) {
|
|
169
|
+
case 'Property':
|
|
170
|
+
if (isArray(value)) {
|
|
171
|
+
value = value.map(item => isObject(item) ? item[seg.name] : null).filter(v => v !== null);
|
|
172
|
+
}
|
|
173
|
+
else if (isObject(value)) {
|
|
174
|
+
const prop = value[seg.name];
|
|
175
|
+
if (prop === undefined) {
|
|
176
|
+
const suggestion = suggestProperty(seg.name, value);
|
|
177
|
+
throw new Error(`Property '${seg.name}' not found on Object.${suggestion ? ' ' + suggestion : ''}`);
|
|
178
|
+
}
|
|
179
|
+
value = prop;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
case 'Index':
|
|
186
|
+
if (seg.index === null)
|
|
187
|
+
value = toArray(value); // [*]
|
|
188
|
+
else
|
|
189
|
+
value = toArray(value)[seg.index] ?? null;
|
|
190
|
+
break;
|
|
191
|
+
case 'Slice': {
|
|
192
|
+
const arr = toArray(value);
|
|
193
|
+
let s = seg.start ?? 0, e = seg.end ?? arr.length;
|
|
194
|
+
if (s < 0)
|
|
195
|
+
s = Math.max(0, arr.length + s);
|
|
196
|
+
if (e < 0)
|
|
197
|
+
e = Math.max(0, arr.length + e);
|
|
198
|
+
value = arr.slice(s, e);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case 'RecursiveDescent': {
|
|
202
|
+
const results = [];
|
|
203
|
+
collectRecursive(value, seg.name, results);
|
|
204
|
+
value = results;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
function collectRecursive(value, name, results) {
|
|
212
|
+
if (isObject(value)) {
|
|
213
|
+
if (name in value)
|
|
214
|
+
results.push(value[name]);
|
|
215
|
+
for (const k of Object.keys(value))
|
|
216
|
+
collectRecursive(value[k], name, results);
|
|
217
|
+
}
|
|
218
|
+
else if (isArray(value)) {
|
|
219
|
+
for (const item of value)
|
|
220
|
+
collectRecursive(item, name, results);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function evalIdentifier(expr, scope) {
|
|
224
|
+
const val = scope.get(expr.name);
|
|
225
|
+
if (val === undefined)
|
|
226
|
+
throw new Error(`Undefined variable '${expr.name}'.`);
|
|
227
|
+
return val;
|
|
228
|
+
}
|
|
229
|
+
// ── Binary / Unary ──
|
|
230
|
+
function evalBinary(expr, current, scope) {
|
|
231
|
+
const left = evaluate(expr.left, current, scope);
|
|
232
|
+
const right = evaluate(expr.right, current, scope);
|
|
233
|
+
switch (expr.operator) {
|
|
234
|
+
case 'Add':
|
|
235
|
+
if (typeof left === 'string' || typeof right === 'string')
|
|
236
|
+
return valueToString(left) + valueToString(right);
|
|
237
|
+
return left + right;
|
|
238
|
+
case 'Subtract': return left - right;
|
|
239
|
+
case 'Multiply': return left * right;
|
|
240
|
+
case 'Divide': return right !== 0 ? left / right : 0;
|
|
241
|
+
case 'Equal': return valuesEqual(left, right);
|
|
242
|
+
case 'NotEqual': return !valuesEqual(left, right);
|
|
243
|
+
case 'LessThan':
|
|
244
|
+
if (typeof left === 'string' && typeof right === 'string')
|
|
245
|
+
return left < right;
|
|
246
|
+
return left < right;
|
|
247
|
+
case 'LessThanOrEqual':
|
|
248
|
+
if (typeof left === 'string' && typeof right === 'string')
|
|
249
|
+
return left <= right;
|
|
250
|
+
return left <= right;
|
|
251
|
+
case 'GreaterThan':
|
|
252
|
+
if (typeof left === 'string' && typeof right === 'string')
|
|
253
|
+
return left > right;
|
|
254
|
+
return left > right;
|
|
255
|
+
case 'GreaterThanOrEqual':
|
|
256
|
+
if (typeof left === 'string' && typeof right === 'string')
|
|
257
|
+
return left >= right;
|
|
258
|
+
return left >= right;
|
|
259
|
+
case 'And': return isTruthy(left) && isTruthy(right);
|
|
260
|
+
case 'Or': return isTruthy(left) || isTruthy(right);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function evalUnary(expr, current, scope) {
|
|
264
|
+
const operand = evaluate(expr.operand, current, scope);
|
|
265
|
+
return expr.operator === 'Not' ? !isTruthy(operand) : -operand;
|
|
266
|
+
}
|
|
267
|
+
// ── Object ──
|
|
268
|
+
function evalObject(expr, current, scope) {
|
|
269
|
+
const result = {};
|
|
270
|
+
for (const p of expr.properties) {
|
|
271
|
+
if (p.isSpread) {
|
|
272
|
+
const spread = evaluate(p.value, current, scope);
|
|
273
|
+
if (isObject(spread))
|
|
274
|
+
Object.assign(result, spread);
|
|
275
|
+
}
|
|
276
|
+
else if (p.computedKey) {
|
|
277
|
+
const key = valueToString(evaluate(p.computedKey, current, scope));
|
|
278
|
+
result[key] = evaluate(p.value, current, scope);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
result[p.key] = evaluate(p.value, current, scope);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
// ── Pipeline ──
|
|
287
|
+
function evalPipeline(expr, current, scope) {
|
|
288
|
+
let value = evaluate(expr.source, current, scope);
|
|
289
|
+
for (const op of expr.operations) {
|
|
290
|
+
value = evalPipeOp(op, value, scope);
|
|
291
|
+
}
|
|
292
|
+
return value;
|
|
293
|
+
}
|
|
294
|
+
function evalWithLambdaOrImplicit(expr, item, scope) {
|
|
295
|
+
if (expr.type === 'Lambda') {
|
|
296
|
+
const child = scope.child();
|
|
297
|
+
if (expr.parameters.length >= 1)
|
|
298
|
+
child.set(expr.parameters[0], item);
|
|
299
|
+
child.set('$root', item);
|
|
300
|
+
return evaluate(expr.body, item, child);
|
|
301
|
+
}
|
|
302
|
+
const child = scope.child();
|
|
303
|
+
child.set('$root', item);
|
|
304
|
+
return evaluate(expr, item, child);
|
|
305
|
+
}
|
|
306
|
+
function evalPipeOp(op, input, scope) {
|
|
307
|
+
const items = toArray(input);
|
|
308
|
+
switch (op.type) {
|
|
309
|
+
case 'Where': return items.filter(item => isTruthy(evalWithLambdaOrImplicit(op.predicate, item, scope)));
|
|
310
|
+
case 'Select': return items.map(item => evalWithLambdaOrImplicit(op.projection, item, scope));
|
|
311
|
+
case 'SelectMany': return items.flatMap(item => {
|
|
312
|
+
const r = evalWithLambdaOrImplicit(op.projection, item, scope);
|
|
313
|
+
return isArray(r) ? r : [r];
|
|
314
|
+
});
|
|
315
|
+
case 'Distinct': {
|
|
316
|
+
const seen = new Set();
|
|
317
|
+
return items.filter(item => { const k = serialize(item); if (seen.has(k))
|
|
318
|
+
return false; seen.add(k); return true; });
|
|
319
|
+
}
|
|
320
|
+
case 'Aggregate': return evalAggregate(op, items, scope);
|
|
321
|
+
case 'Slice':
|
|
322
|
+
return op.kind === 'take' ? items.slice(0, evaluate(op.count, input, scope))
|
|
323
|
+
: items.slice(evaluate(op.count, input, scope));
|
|
324
|
+
case 'OrderBy': return evalOrderBy(op, items, scope);
|
|
325
|
+
case 'GroupBy': return evalGroupBy(op, items, scope);
|
|
326
|
+
case 'Batch': {
|
|
327
|
+
const size = evaluate(op.size, input, scope);
|
|
328
|
+
const batches = [];
|
|
329
|
+
for (let i = 0; i < items.length; i += size)
|
|
330
|
+
batches.push(items.slice(i, i + size));
|
|
331
|
+
return batches;
|
|
332
|
+
}
|
|
333
|
+
case 'Concat': {
|
|
334
|
+
const sep = op.separator ? valueToString(evaluate(op.separator, input, scope)) : '|';
|
|
335
|
+
return items.map(valueToString).join(sep);
|
|
336
|
+
}
|
|
337
|
+
case 'Reduce': return evalReduce(op, items, scope);
|
|
338
|
+
case 'Join': return evalJoin(op, items, scope);
|
|
339
|
+
case 'Quantifier':
|
|
340
|
+
return op.kind === 'all'
|
|
341
|
+
? items.every(item => isTruthy(evalWithLambdaOrImplicit(op.predicate, item, scope)))
|
|
342
|
+
: items.some(item => isTruthy(evalWithLambdaOrImplicit(op.predicate, item, scope)));
|
|
343
|
+
case 'MatchOp': return evalMatchArms(op.arms, input, scope);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function evalAggregate(op, items, scope) {
|
|
347
|
+
if (op.name === 'first') {
|
|
348
|
+
if (op.predicate) {
|
|
349
|
+
return items.find(item => isTruthy(evalWithLambdaOrImplicit(op.predicate, item, scope))) ?? null;
|
|
350
|
+
}
|
|
351
|
+
return items[0] ?? null;
|
|
352
|
+
}
|
|
353
|
+
if (op.name === 'last' && op.predicate) {
|
|
354
|
+
return [...items].reverse().find(item => isTruthy(evalWithLambdaOrImplicit(op.predicate, item, scope))) ?? null;
|
|
355
|
+
}
|
|
356
|
+
switch (op.name) {
|
|
357
|
+
case 'count': return items.length;
|
|
358
|
+
case 'last': return items[items.length - 1] ?? null;
|
|
359
|
+
case 'sum': return items.reduce((a, b) => a + b, 0);
|
|
360
|
+
case 'min': return Math.min(...items.map(Number));
|
|
361
|
+
case 'max': return Math.max(...items.map(Number));
|
|
362
|
+
case 'index': return items.map((_, i) => i);
|
|
363
|
+
default: throw new Error(`Unknown aggregate: ${op.name}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function evalOrderBy(op, items, scope) {
|
|
367
|
+
return [...items].sort((a, b) => {
|
|
368
|
+
for (const { key, ascending } of op.keys) {
|
|
369
|
+
const ka = evalWithLambdaOrImplicit(key, a, scope);
|
|
370
|
+
const kb = evalWithLambdaOrImplicit(key, b, scope);
|
|
371
|
+
let cmp = 0;
|
|
372
|
+
if (typeof ka === 'string' && typeof kb === 'string')
|
|
373
|
+
cmp = ka.localeCompare(kb);
|
|
374
|
+
else if (typeof ka === 'number' && typeof kb === 'number')
|
|
375
|
+
cmp = ka - kb;
|
|
376
|
+
else
|
|
377
|
+
cmp = String(ka).localeCompare(String(kb));
|
|
378
|
+
if (cmp !== 0)
|
|
379
|
+
return ascending ? cmp : -cmp;
|
|
380
|
+
}
|
|
381
|
+
return 0;
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
function evalGroupBy(op, items, scope) {
|
|
385
|
+
const groups = new Map();
|
|
386
|
+
for (const item of items) {
|
|
387
|
+
const key = evalWithLambdaOrImplicit(op.keySelector, item, scope);
|
|
388
|
+
const keyStr = serialize(key);
|
|
389
|
+
if (!groups.has(keyStr))
|
|
390
|
+
groups.set(keyStr, { key, items: [] });
|
|
391
|
+
groups.get(keyStr).items.push(item);
|
|
392
|
+
}
|
|
393
|
+
return [...groups.values()];
|
|
394
|
+
}
|
|
395
|
+
function evalReduce(op, items, scope) {
|
|
396
|
+
if (items.length === 0)
|
|
397
|
+
return op.initialValue ? evaluate(op.initialValue, null, scope) : null;
|
|
398
|
+
const lambda = op.accumulator;
|
|
399
|
+
if (lambda.type !== 'Lambda' || lambda.parameters.length < 2)
|
|
400
|
+
throw new Error('reduce requires (acc, item) => expr');
|
|
401
|
+
let acc;
|
|
402
|
+
let startIdx;
|
|
403
|
+
if (op.initialValue) {
|
|
404
|
+
acc = evaluate(op.initialValue, null, scope);
|
|
405
|
+
startIdx = 0;
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
acc = items[0];
|
|
409
|
+
startIdx = 1;
|
|
410
|
+
}
|
|
411
|
+
for (let i = startIdx; i < items.length; i++) {
|
|
412
|
+
const child = scope.child();
|
|
413
|
+
child.set(lambda.parameters[0], acc);
|
|
414
|
+
child.set(lambda.parameters[1], items[i]);
|
|
415
|
+
acc = evaluate(lambda.body, items[i], child);
|
|
416
|
+
}
|
|
417
|
+
return acc;
|
|
418
|
+
}
|
|
419
|
+
function evalJoin(op, leftItems, scope) {
|
|
420
|
+
const root = scope.get('$root');
|
|
421
|
+
const rightItems = toArray(evaluate(op.source, root, scope));
|
|
422
|
+
const rightLookup = new Map();
|
|
423
|
+
for (const r of rightItems) {
|
|
424
|
+
const key = serialize(evalWithLambdaOrImplicit(op.rightKey, r, scope));
|
|
425
|
+
if (!rightLookup.has(key))
|
|
426
|
+
rightLookup.set(key, []);
|
|
427
|
+
rightLookup.get(key).push(r);
|
|
428
|
+
}
|
|
429
|
+
const matchedRight = new Set();
|
|
430
|
+
const results = [];
|
|
431
|
+
for (const l of leftItems) {
|
|
432
|
+
const key = serialize(evalWithLambdaOrImplicit(op.leftKey, l, scope));
|
|
433
|
+
const matches = rightLookup.get(key);
|
|
434
|
+
if (matches) {
|
|
435
|
+
matchedRight.add(key);
|
|
436
|
+
for (const r of matches)
|
|
437
|
+
results.push(mergeJoin(l, r, op.intoAlias));
|
|
438
|
+
}
|
|
439
|
+
else if (op.mode === 'left' || op.mode === 'full') {
|
|
440
|
+
results.push(mergeJoin(l, null, op.intoAlias));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (op.mode === 'right' || op.mode === 'full') {
|
|
444
|
+
for (const r of rightItems) {
|
|
445
|
+
const key = serialize(evalWithLambdaOrImplicit(op.rightKey, r, scope));
|
|
446
|
+
if (!matchedRight.has(key))
|
|
447
|
+
results.push(mergeJoin(null, r, op.intoAlias));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return results;
|
|
451
|
+
}
|
|
452
|
+
function mergeJoin(left, right, alias) {
|
|
453
|
+
const result = {};
|
|
454
|
+
if (isObject(left))
|
|
455
|
+
Object.assign(result, left);
|
|
456
|
+
if (alias) {
|
|
457
|
+
result[alias] = right;
|
|
458
|
+
}
|
|
459
|
+
else if (isObject(right)) {
|
|
460
|
+
for (const [k, v] of Object.entries(right))
|
|
461
|
+
if (!(k in result))
|
|
462
|
+
result[k] = v;
|
|
463
|
+
}
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
// ── Match ──
|
|
467
|
+
function evalMatch(expr, current, scope) {
|
|
468
|
+
const input = evaluate(expr.input, current, scope);
|
|
469
|
+
return evalMatchArms(expr.arms, input, scope);
|
|
470
|
+
}
|
|
471
|
+
function evalMatchArms(arms, input, scope) {
|
|
472
|
+
for (const arm of arms) {
|
|
473
|
+
if (arm.pattern === null)
|
|
474
|
+
return evaluate(arm.result, input, scope);
|
|
475
|
+
const pattern = evaluate(arm.pattern, input, scope);
|
|
476
|
+
if (valuesEqual(input, pattern))
|
|
477
|
+
return evaluate(arm.result, input, scope);
|
|
478
|
+
}
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
// ── Member access / Method call / Function call / Index ──
|
|
482
|
+
function evalMemberAccess(expr, current, scope) {
|
|
483
|
+
const target = evaluate(expr.target, current, scope);
|
|
484
|
+
if (isObject(target))
|
|
485
|
+
return target[expr.memberName] ?? null;
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
function evalMethodCall(expr, current, scope) {
|
|
489
|
+
const target = evaluate(expr.target, current, scope);
|
|
490
|
+
const args = expr.arguments.map(a => evaluate(a, current, scope));
|
|
491
|
+
return callBuiltin(expr.methodName, target, args, scope);
|
|
492
|
+
}
|
|
493
|
+
function evalFunctionCall(expr, current, scope) {
|
|
494
|
+
// Check for memoized functions
|
|
495
|
+
const funcVal = scope.get(expr.functionName);
|
|
496
|
+
if (funcVal instanceof MemoizedFunction) {
|
|
497
|
+
const args = expr.arguments.map(a => evaluate(a, current, scope));
|
|
498
|
+
return funcVal.invoke(args, current);
|
|
499
|
+
}
|
|
500
|
+
const args = expr.arguments.map(a => evaluate(a, current, scope));
|
|
501
|
+
return callBuiltin(expr.functionName, current, args, scope);
|
|
502
|
+
}
|
|
503
|
+
function evalIndex(expr, current, scope) {
|
|
504
|
+
const target = evaluate(expr.target, current, scope);
|
|
505
|
+
if (expr.index === null)
|
|
506
|
+
return toArray(target);
|
|
507
|
+
const idx = evaluate(expr.index, current, scope);
|
|
508
|
+
return toArray(target)[idx] ?? null;
|
|
509
|
+
}
|
|
510
|
+
function evalInterpolation(expr, current, scope) {
|
|
511
|
+
return expr.parts.map(p => p.type === 'Text' ? p.text : valueToString(evaluate(p.expression, current, scope))).join('');
|
|
512
|
+
}
|
|
513
|
+
// ── Built-in Methods ──
|
|
514
|
+
function callBuiltin(name, target, args, _scope) {
|
|
515
|
+
const str = () => typeof target === 'string' ? target : valueToString(target);
|
|
516
|
+
const num = () => typeof target === 'number' ? target : Number(target) || 0;
|
|
517
|
+
const arr = () => toArray(target);
|
|
518
|
+
switch (name) {
|
|
519
|
+
// String
|
|
520
|
+
case 'toLower': {
|
|
521
|
+
if (args.length === 1 && typeof args[0] === 'number') {
|
|
522
|
+
const s = str(), pos = args[0] - 1;
|
|
523
|
+
return pos >= 0 && pos < s.length ? s.slice(0, pos) + s[pos].toLowerCase() + s.slice(pos + 1) : s;
|
|
524
|
+
}
|
|
525
|
+
return str().toLowerCase();
|
|
526
|
+
}
|
|
527
|
+
case 'toUpper': {
|
|
528
|
+
if (args.length === 1 && typeof args[0] === 'number') {
|
|
529
|
+
const s = str(), pos = args[0] - 1;
|
|
530
|
+
return pos >= 0 && pos < s.length ? s.slice(0, pos) + s[pos].toUpperCase() + s.slice(pos + 1) : s;
|
|
531
|
+
}
|
|
532
|
+
return str().toUpperCase();
|
|
533
|
+
}
|
|
534
|
+
case 'trim': return args.length > 0 ? trimChars(str(), valueToString(args[0])) : str().trim();
|
|
535
|
+
case 'trimStart': return args.length > 0 ? trimStartChars(str(), valueToString(args[0])) : str().trimStart();
|
|
536
|
+
case 'trimEnd': return args.length > 0 ? trimEndChars(str(), valueToString(args[0])) : str().trimEnd();
|
|
537
|
+
case 'left': {
|
|
538
|
+
const s = str(), n = args.length > 0 ? Number(args[0]) : 1;
|
|
539
|
+
return s.slice(0, Math.min(n, s.length));
|
|
540
|
+
}
|
|
541
|
+
case 'right': {
|
|
542
|
+
const s = str(), n = args.length > 0 ? Number(args[0]) : 1;
|
|
543
|
+
return s.slice(-Math.min(n, s.length));
|
|
544
|
+
}
|
|
545
|
+
case 'padLeft': return str().padStart(Number(args[0]), args.length > 1 ? valueToString(args[1]) : ' ');
|
|
546
|
+
case 'padRight': return str().padEnd(Number(args[0]), args.length > 1 ? valueToString(args[1]) : ' ');
|
|
547
|
+
case 'contains': return str().toLowerCase().includes(valueToString(args[0]).toLowerCase());
|
|
548
|
+
case 'startsWith': return str().toLowerCase().startsWith(valueToString(args[0]).toLowerCase());
|
|
549
|
+
case 'endsWith': return str().toLowerCase().endsWith(valueToString(args[0]).toLowerCase());
|
|
550
|
+
case 'replace': {
|
|
551
|
+
const s = str(), search = valueToString(args[0]), repl = args.length > 1 ? valueToString(args[1]) : '';
|
|
552
|
+
const caseInsensitive = args.length > 2 && isTruthy(args[2]);
|
|
553
|
+
if (caseInsensitive)
|
|
554
|
+
return s.replace(new RegExp(escapeRegex(search), 'gi'), repl);
|
|
555
|
+
return s.split(search).join(repl);
|
|
556
|
+
}
|
|
557
|
+
case 'substring': {
|
|
558
|
+
const s = str(), start = Number(args[0]);
|
|
559
|
+
return args.length > 1 ? s.substring(start, start + Number(args[1])) : s.substring(start);
|
|
560
|
+
}
|
|
561
|
+
case 'split': return str().split(valueToString(args[0]));
|
|
562
|
+
case 'length':
|
|
563
|
+
return isArray(target) ? target.length : str().length;
|
|
564
|
+
case 'toCharArray': return [...str()];
|
|
565
|
+
case 'regex': return [...str().matchAll(new RegExp(valueToString(args[0]), 'g'))].map(m => m[0]);
|
|
566
|
+
case 'urlDecode': return decodeURIComponent(str());
|
|
567
|
+
case 'urlEncode': return encodeURIComponent(str());
|
|
568
|
+
case 'sanitize': return sanitize(str());
|
|
569
|
+
case 'concat': return concatMethod(target, args);
|
|
570
|
+
// Membership
|
|
571
|
+
case 'in': {
|
|
572
|
+
const candidates = args.flatMap(a => isArray(a) ? a : [a]);
|
|
573
|
+
return candidates.some(c => valuesEqual(target, c));
|
|
574
|
+
}
|
|
575
|
+
// Object manipulation
|
|
576
|
+
case 'clone': return JSON.parse(JSON.stringify(target));
|
|
577
|
+
case 'keep': {
|
|
578
|
+
const names = new Set(args.map(a => valueToString(a)));
|
|
579
|
+
if (isObject(target))
|
|
580
|
+
return Object.fromEntries(Object.entries(target).filter(([k]) => names.has(k)));
|
|
581
|
+
if (isArray(target))
|
|
582
|
+
return target.map(item => isObject(item) ? Object.fromEntries(Object.entries(item).filter(([k]) => names.has(k))) : item);
|
|
583
|
+
return target;
|
|
584
|
+
}
|
|
585
|
+
case 'remove': {
|
|
586
|
+
const names = new Set(args.map(a => valueToString(a)));
|
|
587
|
+
if (isObject(target))
|
|
588
|
+
return Object.fromEntries(Object.entries(target).filter(([k]) => !names.has(k)));
|
|
589
|
+
if (isArray(target))
|
|
590
|
+
return target.map(item => isObject(item) ? Object.fromEntries(Object.entries(item).filter(([k]) => !names.has(k))) : item);
|
|
591
|
+
return target;
|
|
592
|
+
}
|
|
593
|
+
// Collection
|
|
594
|
+
case 'count': return isArray(target) ? target.length : 1;
|
|
595
|
+
case 'first': return isArray(target) ? target[0] ?? null : target;
|
|
596
|
+
case 'last': return isArray(target) ? target[target.length - 1] ?? null : target;
|
|
597
|
+
case 'sum': return arr().reduce((a, b) => a + Number(b), 0);
|
|
598
|
+
case 'min': return Math.min(...arr().map(Number));
|
|
599
|
+
case 'max': return Math.max(...arr().map(Number));
|
|
600
|
+
case 'index': return isArray(target) ? target.map((_, i) => i) : 0;
|
|
601
|
+
case 'take': return arr().slice(0, Number(args[0]));
|
|
602
|
+
case 'skip': return arr().slice(Number(args[0]));
|
|
603
|
+
// Null checks
|
|
604
|
+
case 'isNull': {
|
|
605
|
+
const empty = target === null || target === undefined;
|
|
606
|
+
return args.length > 0 ? (empty ? args[0] : target) : empty;
|
|
607
|
+
}
|
|
608
|
+
case 'isEmpty':
|
|
609
|
+
case 'isNullOrEmpty': {
|
|
610
|
+
const empty = target === null || target === undefined || target === '' || (isArray(target) && target.length === 0);
|
|
611
|
+
return args.length > 0 ? (empty ? args[0] : target) : empty;
|
|
612
|
+
}
|
|
613
|
+
case 'isNullOrWhiteSpace': {
|
|
614
|
+
const empty = target === null || target === undefined ||
|
|
615
|
+
(typeof target === 'string' && target.trim() === '') ||
|
|
616
|
+
(isArray(target) && target.length === 0);
|
|
617
|
+
return args.length > 0 ? (empty ? args[0] : target) : empty;
|
|
618
|
+
}
|
|
619
|
+
// Type conversion
|
|
620
|
+
case 'not': return !isTruthy(target);
|
|
621
|
+
case 'boolean': return args.length > 0 ? isTruthy(args[0]) : isTruthy(target);
|
|
622
|
+
case 'toString': {
|
|
623
|
+
if (args.length > 0 && typeof target === 'number') {
|
|
624
|
+
// Basic format support
|
|
625
|
+
return String(target);
|
|
626
|
+
}
|
|
627
|
+
return valueToString(target);
|
|
628
|
+
}
|
|
629
|
+
case 'toNumber': return typeof target === 'number' ? target : (parseFloat(str()) || 0);
|
|
630
|
+
case 'convertTo': return convertTo(target, args);
|
|
631
|
+
// Math
|
|
632
|
+
case 'round': return evalRound(num(), args);
|
|
633
|
+
case 'floor': return Math.floor(num());
|
|
634
|
+
case 'ceiling': return Math.ceil(num());
|
|
635
|
+
case 'truncate': return Math.trunc(num());
|
|
636
|
+
case 'abs': return Math.abs(num());
|
|
637
|
+
// DateTime
|
|
638
|
+
case 'add': return evalDateAdd(target, args);
|
|
639
|
+
case 'dateFormat':
|
|
640
|
+
case 'tryDateFormat': return evalDateFormat(target, args);
|
|
641
|
+
case 'toUnixTimeSeconds': return evalToUnixTime(target, args);
|
|
642
|
+
case 'now': return evalNow(args);
|
|
643
|
+
case 'utcNow':
|
|
644
|
+
case 'utcnow': return evalNow(args);
|
|
645
|
+
// Generators
|
|
646
|
+
case 'range': return Array.from({ length: Number(args[1]) }, (_, i) => i + Number(args[0]));
|
|
647
|
+
case 'newGuid':
|
|
648
|
+
case 'newguid': return crypto.randomUUID();
|
|
649
|
+
// Crypto
|
|
650
|
+
case 'hash': return evalHash(str(), args);
|
|
651
|
+
case 'rsaSign': return evalRsaSign(target, args);
|
|
652
|
+
default: throw new Error(`Unknown method '${name}'.`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
// ── Helper functions for built-ins ──
|
|
656
|
+
function escapeRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
657
|
+
function trimChars(s, chars) {
|
|
658
|
+
const re = new RegExp(`^[${escapeRegex(chars)}]+|[${escapeRegex(chars)}]+$`, 'g');
|
|
659
|
+
return s.replace(re, '');
|
|
660
|
+
}
|
|
661
|
+
function trimStartChars(s, chars) {
|
|
662
|
+
const re = new RegExp(`^[${escapeRegex(chars)}]+`);
|
|
663
|
+
return s.replace(re, '');
|
|
664
|
+
}
|
|
665
|
+
function trimEndChars(s, chars) {
|
|
666
|
+
const re = new RegExp(`[${escapeRegex(chars)}]+$`);
|
|
667
|
+
return s.replace(re, '');
|
|
668
|
+
}
|
|
669
|
+
function sanitize(s) {
|
|
670
|
+
if (!s)
|
|
671
|
+
return s;
|
|
672
|
+
const charMap = {
|
|
673
|
+
'ß': 'ss', 'Α': 'A', 'Β': 'B', 'Γ': 'G', 'Δ': 'D', 'Ε': 'E', 'Ζ': 'Z', 'Η': 'H', 'Θ': 'Th',
|
|
674
|
+
'Ι': 'I', 'Κ': 'K', 'Λ': 'L', 'Μ': 'M', 'Ν': 'N', 'Ξ': 'X', 'Ο': 'O', 'Π': 'P',
|
|
675
|
+
'Ρ': 'R', 'Σ': 'S', 'Τ': 'T', 'Υ': 'Y', 'Φ': 'Ph', 'Χ': 'Ch', 'Ψ': 'Ps', 'Ω': 'O',
|
|
676
|
+
'α': 'a', 'β': 'b', 'γ': 'g', 'δ': 'd', 'ε': 'e', 'ζ': 'z', 'η': 'h', 'θ': 'th',
|
|
677
|
+
'ι': 'i', 'κ': 'k', 'λ': 'l', 'μ': 'm', 'ν': 'n', 'ξ': 'x', 'ο': 'o', 'π': 'p',
|
|
678
|
+
'ρ': 'r', 'σ': 's', 'τ': 't', 'υ': 'y', 'φ': 'ph', 'χ': 'ch', 'ψ': 'ps', 'ω': 'o',
|
|
679
|
+
};
|
|
680
|
+
const normalized = s.normalize('NFD');
|
|
681
|
+
let result = '';
|
|
682
|
+
for (const c of normalized) {
|
|
683
|
+
if (charMap[c])
|
|
684
|
+
result += charMap[c];
|
|
685
|
+
else if (c.charCodeAt(0) > 0x02ff)
|
|
686
|
+
continue; // skip combining diacritical marks
|
|
687
|
+
else
|
|
688
|
+
result += c;
|
|
689
|
+
}
|
|
690
|
+
return result.normalize('NFC');
|
|
691
|
+
}
|
|
692
|
+
function concatMethod(target, args) {
|
|
693
|
+
const sep = args.length > 0 ? valueToString(args[0]) : '|';
|
|
694
|
+
const items = toArray(target).map(valueToString);
|
|
695
|
+
for (let i = 1; i < args.length; i++) {
|
|
696
|
+
const a = args[i];
|
|
697
|
+
if (isArray(a))
|
|
698
|
+
items.push(...a.map(valueToString));
|
|
699
|
+
else
|
|
700
|
+
items.push(valueToString(a));
|
|
701
|
+
}
|
|
702
|
+
return items.join(sep);
|
|
703
|
+
}
|
|
704
|
+
function convertTo(target, args) {
|
|
705
|
+
const typeName = (valueToString(args[0]) ?? '').toLowerCase();
|
|
706
|
+
const s = valueToString(target);
|
|
707
|
+
switch (typeName) {
|
|
708
|
+
case 'int32':
|
|
709
|
+
case 'int':
|
|
710
|
+
case 'integer': {
|
|
711
|
+
const n = parseFloat(s);
|
|
712
|
+
return isNaN(n) ? 0 : Math.trunc(n);
|
|
713
|
+
}
|
|
714
|
+
case 'int64':
|
|
715
|
+
case 'long': {
|
|
716
|
+
const n = parseFloat(s);
|
|
717
|
+
return isNaN(n) ? 0 : Math.trunc(n);
|
|
718
|
+
}
|
|
719
|
+
case 'double':
|
|
720
|
+
case 'float':
|
|
721
|
+
case 'decimal': {
|
|
722
|
+
const n = parseFloat(s);
|
|
723
|
+
return isNaN(n) ? 0 : n;
|
|
724
|
+
}
|
|
725
|
+
case 'boolean':
|
|
726
|
+
case 'bool': {
|
|
727
|
+
const n = parseFloat(s);
|
|
728
|
+
if (!isNaN(n))
|
|
729
|
+
return n !== 0;
|
|
730
|
+
if (s.toLowerCase() === 'true')
|
|
731
|
+
return true;
|
|
732
|
+
if (s.toLowerCase() === 'false')
|
|
733
|
+
return false;
|
|
734
|
+
return s.trim() !== '';
|
|
735
|
+
}
|
|
736
|
+
case 'string': return valueToString(target);
|
|
737
|
+
default: return target;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
function evalRound(value, args) {
|
|
741
|
+
let decimals = 0;
|
|
742
|
+
let mode = 'awayfromzero';
|
|
743
|
+
for (const arg of args) {
|
|
744
|
+
if (typeof arg === 'string')
|
|
745
|
+
mode = arg.toLowerCase();
|
|
746
|
+
else
|
|
747
|
+
decimals = Number(arg);
|
|
748
|
+
}
|
|
749
|
+
const factor = Math.pow(10, decimals);
|
|
750
|
+
if (mode === 'toeven') {
|
|
751
|
+
// Banker's rounding
|
|
752
|
+
const shifted = value * factor;
|
|
753
|
+
const rounded = Math.round(shifted);
|
|
754
|
+
if (Math.abs(shifted - Math.floor(shifted) - 0.5) < 1e-10) {
|
|
755
|
+
return (Math.floor(shifted) % 2 === 0 ? Math.floor(shifted) : Math.ceil(shifted)) / factor;
|
|
756
|
+
}
|
|
757
|
+
return rounded / factor;
|
|
758
|
+
}
|
|
759
|
+
// Away from zero (default)
|
|
760
|
+
return Math.round(value * factor + Number.EPSILON) / factor;
|
|
761
|
+
}
|
|
762
|
+
function evalDateAdd(target, args) {
|
|
763
|
+
const dateStr = valueToString(target);
|
|
764
|
+
const date = new Date(dateStr);
|
|
765
|
+
if (isNaN(date.getTime()) && typeof target === 'number') {
|
|
766
|
+
return String(target + Number(args[0]));
|
|
767
|
+
}
|
|
768
|
+
const tsStr = valueToString(args[0]);
|
|
769
|
+
const ms = parseTimeSpan(tsStr);
|
|
770
|
+
date.setTime(date.getTime() + ms);
|
|
771
|
+
return date.toISOString().replace('.000Z', 'Z');
|
|
772
|
+
}
|
|
773
|
+
function parseTimeSpan(s) {
|
|
774
|
+
// Format: [days.]hours:minutes:seconds
|
|
775
|
+
const parts = s.split(':');
|
|
776
|
+
let days = 0, hours = 0, minutes = 0, seconds = 0;
|
|
777
|
+
if (parts.length >= 3) {
|
|
778
|
+
const hourPart = parts[0];
|
|
779
|
+
if (hourPart.includes('.')) {
|
|
780
|
+
const [d, h] = hourPart.split('.');
|
|
781
|
+
days = parseInt(d);
|
|
782
|
+
hours = parseInt(h);
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
hours = parseInt(hourPart);
|
|
786
|
+
}
|
|
787
|
+
minutes = parseInt(parts[1]);
|
|
788
|
+
seconds = parseFloat(parts[2]);
|
|
789
|
+
}
|
|
790
|
+
return ((days * 24 + hours) * 3600 + minutes * 60 + seconds) * 1000;
|
|
791
|
+
}
|
|
792
|
+
function evalDateFormat(target, args) {
|
|
793
|
+
const dateStr = valueToString(target);
|
|
794
|
+
let date;
|
|
795
|
+
if (args.length >= 2) {
|
|
796
|
+
// Two args: inputFormat, outputFormat — just parse and format
|
|
797
|
+
date = new Date(dateStr);
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
date = new Date(dateStr);
|
|
801
|
+
}
|
|
802
|
+
if (isNaN(date.getTime()))
|
|
803
|
+
return dateStr;
|
|
804
|
+
const fmt = args.length >= 2 ? valueToString(args[1]) : valueToString(args[0]);
|
|
805
|
+
return formatDate(date, fmt);
|
|
806
|
+
}
|
|
807
|
+
function evalToUnixTime(target, args) {
|
|
808
|
+
let date;
|
|
809
|
+
if (args.length >= 1)
|
|
810
|
+
date = new Date(valueToString(args[0]));
|
|
811
|
+
else if (typeof target === 'string' && target)
|
|
812
|
+
date = new Date(target);
|
|
813
|
+
else
|
|
814
|
+
date = new Date();
|
|
815
|
+
return String(Math.floor(date.getTime() / 1000));
|
|
816
|
+
}
|
|
817
|
+
function evalNow(args) {
|
|
818
|
+
const fmt = args.length > 0 ? valueToString(args[0]) : 'yyyy-MM-ddTHH:mm:ssZ';
|
|
819
|
+
const date = new Date();
|
|
820
|
+
if (args.length >= 2) {
|
|
821
|
+
// Timezone — not easily doable in pure JS without Intl, use UTC
|
|
822
|
+
return formatDate(date, fmt);
|
|
823
|
+
}
|
|
824
|
+
return formatDate(date, fmt);
|
|
825
|
+
}
|
|
826
|
+
function formatDate(d, fmt) {
|
|
827
|
+
const pad = (n, w = 2) => String(n).padStart(w, '0');
|
|
828
|
+
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
829
|
+
return fmt
|
|
830
|
+
.replace('yyyy', String(d.getUTCFullYear()))
|
|
831
|
+
.replace('MMMM', months[d.getUTCMonth()])
|
|
832
|
+
.replace('MMM', months[d.getUTCMonth()].slice(0, 3))
|
|
833
|
+
.replace('MM', pad(d.getUTCMonth() + 1))
|
|
834
|
+
.replace('dd', pad(d.getUTCDate()))
|
|
835
|
+
.replace('HH', pad(d.getUTCHours()))
|
|
836
|
+
.replace('mm', pad(d.getUTCMinutes()))
|
|
837
|
+
.replace('ss', pad(d.getUTCSeconds()))
|
|
838
|
+
.replace('Z', 'Z');
|
|
839
|
+
}
|
|
840
|
+
// ── Crypto (uses node:crypto — documented browser limitation) ──
|
|
841
|
+
let _crypto = null;
|
|
842
|
+
function getNodeCrypto() {
|
|
843
|
+
if (!_crypto) {
|
|
844
|
+
try {
|
|
845
|
+
// Dynamic import for Node.js; will fail in browsers
|
|
846
|
+
_crypto = require('node:crypto');
|
|
847
|
+
}
|
|
848
|
+
catch {
|
|
849
|
+
throw new Error('Crypto functions (hash, rsaSign) require Node.js. Not available in browser.');
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return _crypto;
|
|
853
|
+
}
|
|
854
|
+
function evalHash(input, args) {
|
|
855
|
+
const len = args.length > 0 ? Number(args[0]) : 32;
|
|
856
|
+
const crypto = getNodeCrypto();
|
|
857
|
+
const fullHash = crypto.createHash('md5').update(input, 'utf8').digest('hex');
|
|
858
|
+
return fullHash.slice(0, Math.min(len, fullHash.length));
|
|
859
|
+
}
|
|
860
|
+
function evalRsaSign(target, args) {
|
|
861
|
+
const crypto = getNodeCrypto();
|
|
862
|
+
const data = args.length > 0 ? valueToString(args[0]) : valueToString(target);
|
|
863
|
+
const keyPem = args.length > 1 ? valueToString(args[1]) : valueToString(args[0]);
|
|
864
|
+
const keyBody = keyPem
|
|
865
|
+
.replace('-----BEGIN RSA PRIVATE KEY-----', '')
|
|
866
|
+
.replace('-----END RSA PRIVATE KEY-----', '')
|
|
867
|
+
.replace(/\n/g, '')
|
|
868
|
+
.replace(/\r/g, '');
|
|
869
|
+
const keyDer = Buffer.from(keyBody, 'base64');
|
|
870
|
+
const privateKey = crypto.createPrivateKey({ key: keyDer, format: 'der', type: 'pkcs1' });
|
|
871
|
+
const signature = crypto.sign('sha1', Buffer.from(data, 'ascii'), {
|
|
872
|
+
key: privateKey,
|
|
873
|
+
padding: crypto.constants.RSA_PKCS1_PADDING,
|
|
874
|
+
});
|
|
875
|
+
// Legacy compatibility: reverse the signature bytes
|
|
876
|
+
const reversed = Buffer.from(signature).reverse();
|
|
877
|
+
return reversed.toString('base64');
|
|
878
|
+
}
|
|
879
|
+
//# sourceMappingURL=evaluator.js.map
|