@bpmnkit/feel 0.0.21 → 1.0.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.
@@ -6,7 +6,10 @@ export interface EvalContext {
6
6
  input?: FeelValue;
7
7
  }
8
8
  export declare function evaluate(node: FeelNode, ctx: EvalContext): FeelValue;
9
- /** Evaluate a unary test against an input value. Returns boolean. */
9
+ /**
10
+ * Evaluates a unary test against an input value. A decision table's rule
11
+ * either matches or it does not, so an unknown answer is not a match.
12
+ */
10
13
  export declare function evaluateUnaryTest(node: FeelNode, input: FeelValue, ctx: EvalContext): boolean;
11
14
  /** Evaluate a full unary-test node (the root returned by parseUnaryTests). */
12
15
  export declare function evaluateUnaryTests(node: FeelNode, input: FeelValue, ctx: EvalContext): boolean;
package/dist/evaluator.js CHANGED
@@ -1,5 +1,5 @@
1
- import { compareValues, getBuiltin, parseTemporal } from "./builtins.js";
2
- import { getProperty, isFeelContext, isFeelDate, isFeelDateTime, isFeelDayTimeDuration, isFeelList, isFeelRange, isFeelTime, isFeelYearsMonthsDuration, } from "./types.js";
1
+ import { compareValues, getBuiltin, orderNamedArgs, parseTemporal } from "./builtins.js";
2
+ import { getProperty, isFeelContext, isFeelDate, isFeelDateTime, isFeelDayTimeDuration, isFeelFunction, isFeelList, isFeelRange, isFeelTime, isFeelYearsMonthsDuration, } from "./types.js";
3
3
  function lookupVar(ctx, name) {
4
4
  if (name === "?")
5
5
  return ctx.input ?? null;
@@ -13,16 +13,63 @@ function lookupVar(ctx, name) {
13
13
  function childCtx(parent, vars = {}) {
14
14
  return { vars, parent, input: parent.input };
15
15
  }
16
+ /**
17
+ * The scope a filter condition runs in. The element is bound to `item`, and
18
+ * when it is a context its entries are in scope directly, so a list of
19
+ * records filters on their fields: `[{a: 1}, {a: 2}][a >= 2]`. An entry
20
+ * called `item` is the element's own, not the element.
21
+ */
22
+ function filterCtx(parent, item) {
23
+ const vars = { item };
24
+ if (isFeelContext(item))
25
+ Object.assign(vars, item);
26
+ return childCtx(parent, vars);
27
+ }
28
+ /**
29
+ * Expands the domain of a `for`/`some`/`every` binding into the values to
30
+ * iterate. A numeric or date range yields its whole span, counting down when
31
+ * it runs backwards; anything else iterates as a single-element list. A range
32
+ * over values that cannot be stepped through has no iteration, which is null.
33
+ */
34
+ function iterationValues(domain) {
35
+ if (isFeelList(domain))
36
+ return domain;
37
+ if (!isFeelRange(domain))
38
+ return [domain];
39
+ const { start, end } = domain;
40
+ if (typeof start === "number" && typeof end === "number") {
41
+ return span(start, end).map((n) => n);
42
+ }
43
+ if (isFeelDate(start) && isFeelDate(end)) {
44
+ return span(dateToEpochDays(start), dateToEpochDays(end)).map((d) => epochDaysToDate(d));
45
+ }
46
+ return null;
47
+ }
48
+ /** Every integer from `from` to `to`, in whichever direction that runs. */
49
+ function span(from, to) {
50
+ const step = from <= to ? 1 : -1;
51
+ const values = [];
52
+ for (let v = from; step > 0 ? v <= to : v >= to; v += step)
53
+ values.push(v);
54
+ return values;
55
+ }
16
56
  // -------------------------------------------------------------------------
17
57
  // Arithmetic helpers for temporal types
18
58
  // -------------------------------------------------------------------------
59
+ /**
60
+ * Shifts a date by whole months. The day is clamped to the length of the
61
+ * month it lands in, so 2020-01-31 plus P1M is 2020-02-29 rather than a
62
+ * February 31st that no calendar has.
63
+ */
64
+ function shiftMonths(date, months) {
65
+ const total = date.month + months;
66
+ const year = date.year + Math.floor((total - 1) / 12);
67
+ const month = ((((total - 1) % 12) + 12) % 12) + 1;
68
+ return { type: "date", year, month, day: Math.min(date.day, DAYS_IN_MONTH_TABLE(year, month)) };
69
+ }
19
70
  function addDuration(date, dur) {
20
71
  if (isFeelDate(date) && isFeelYearsMonthsDuration(dur)) {
21
- let m = date.month + dur.months;
22
- let y = date.year;
23
- y += Math.floor((m - 1) / 12);
24
- m = ((m - 1 + 1200) % 12) + 1;
25
- return { type: "date", year: y, month: m, day: date.day };
72
+ return shiftMonths(date, dur.months);
26
73
  }
27
74
  if (isFeelDate(date) && isFeelDayTimeDuration(dur)) {
28
75
  const EPOCH = dateToEpochDays(date);
@@ -34,16 +81,7 @@ function addDuration(date, dur) {
34
81
  return epochSecondsToDateTime(totalSec, date.time.offsetSeconds, date.time.timezone);
35
82
  }
36
83
  if (isFeelDateTime(date) && isFeelYearsMonthsDuration(dur)) {
37
- const d = date.date;
38
- let m = d.month + dur.months;
39
- let y = d.year;
40
- y += Math.floor((m - 1) / 12);
41
- m = ((m - 1 + 1200) % 12) + 1;
42
- return {
43
- type: "date-time",
44
- date: { type: "date", year: y, month: m, day: d.day },
45
- time: date.time,
46
- };
84
+ return { type: "date-time", date: shiftMonths(date.date, dur.months), time: date.time };
47
85
  }
48
86
  if (isFeelDayTimeDuration(date) && isFeelDayTimeDuration(dur)) {
49
87
  return { type: "days-time-duration", seconds: date.seconds + dur.seconds };
@@ -178,9 +216,16 @@ export function evaluate(node, ctx) {
178
216
  case "list":
179
217
  return node.items.map((item) => evaluate(item, ctx));
180
218
  case "context": {
219
+ // Entries are evaluated in order in a scope that already holds the
220
+ // preceding ones, so `{a: 1, b: a + 1}` resolves `a` in `b`.
181
221
  const result = {};
222
+ const entryCtx = childCtx(ctx, result);
182
223
  for (const entry of node.entries) {
183
- result[entry.key] = evaluate(entry.value, ctx);
224
+ // A key given twice names two different values, which is not a
225
+ // context at all.
226
+ if (entry.key in result)
227
+ return null;
228
+ result[entry.key] = evaluate(entry.value, entryCtx);
184
229
  }
185
230
  return result;
186
231
  }
@@ -206,71 +251,53 @@ export function evaluate(node, ctx) {
206
251
  return getProperty(base, node.key);
207
252
  }
208
253
  case "filter": {
209
- const base = evaluate(node.base, ctx);
210
- if (!isFeelList(base)) {
211
- if (base === null)
212
- return [];
213
- // single value
214
- const result = evaluate(node.condition, childCtx(ctx, { item: base }));
215
- return typeof result === "number" ? [base] : result ? [base] : [];
216
- }
254
+ const value = evaluate(node.base, ctx);
255
+ if (value === null)
256
+ return [];
257
+ // A value that is not a list is filtered as a list holding just it,
258
+ // so `true[1]` is true and `true[0]` is null.
259
+ const base = isFeelList(value) ? value : [value];
217
260
  // Numeric index filter
218
- const first = evaluate(node.condition, childCtx(ctx, { item: base[0] ?? null }));
261
+ const first = evaluate(node.condition, filterCtx(ctx, base[0] ?? null));
219
262
  if (typeof first === "number") {
220
263
  const idx = first > 0 ? first - 1 : base.length + first;
221
264
  const val = base[Math.floor(idx)];
222
265
  return val !== undefined ? val : null;
223
266
  }
224
- return base.filter((item) => {
225
- const r = evaluate(node.condition, childCtx(ctx, { item }));
226
- return r === true || (r !== false && r !== null);
227
- });
267
+ return base.filter((item) => evaluate(node.condition, filterCtx(ctx, item)) === true);
228
268
  }
229
269
  case "call":
230
270
  return evalCall(node.callee, node.args, ctx);
271
+ case "call-expr": {
272
+ const target = evaluate(node.target, ctx);
273
+ if (!isFeelFunction(target))
274
+ return null;
275
+ return target.call(node.args.map((a) => evaluate(a, ctx)));
276
+ }
231
277
  case "call-named": {
278
+ const argNames = node.args.map((a) => a.name);
279
+ const values = node.args.map((a) => evaluate(a.value, ctx));
232
280
  const builtin = getBuiltin(node.callee);
233
- if (!builtin) {
234
- const fn = lookupVar(ctx, node.callee);
235
- if (fn === null || typeof fn !== "object" || !("call" in fn))
281
+ if (builtin) {
282
+ const order = orderNamedArgs(node.callee, argNames);
283
+ if (order === null)
236
284
  return null;
237
- const args = node.args.map((a) => evaluate(a.value, ctx));
238
- return fn.call(args);
285
+ return builtin.call(order.map((idx) => values[idx] ?? null));
239
286
  }
240
- // Map named args to positional (built-ins don't declare param names in registry)
241
- const args = node.args.map((a) => evaluate(a.value, ctx));
242
- return builtin.call(args);
287
+ const fn = lookupVar(ctx, node.callee);
288
+ if (!isFeelFunction(fn))
289
+ return null;
290
+ return fn.call(orderArgs(fn.paramNames, argNames, values));
243
291
  }
244
292
  case "if": {
245
293
  const cond = evaluate(node.condition, ctx);
246
294
  return cond === true ? evaluate(node.then, ctx) : evaluate(node.else, ctx);
247
295
  }
248
- case "for": {
249
- const domains = [];
250
- for (const binding of node.bindings) {
251
- const d = evaluate(binding.domain, ctx);
252
- domains.push(isFeelList(d) ? d : [d]);
253
- }
254
- const partial = [];
255
- const results = evalCartesian(node.bindings, domains, 0, ctx, node, partial);
256
- return results;
257
- }
258
- case "some": {
259
- const domains = [];
260
- for (const binding of node.bindings) {
261
- const d = evaluate(binding.domain, ctx);
262
- domains.push(isFeelList(d) ? d : [d]);
263
- }
264
- return evalQuantifier("some", node.bindings, domains, 0, ctx, node.satisfies);
265
- }
266
- case "every": {
267
- const domains = [];
268
- for (const binding of node.bindings) {
269
- const d = evaluate(binding.domain, ctx);
270
- domains.push(isFeelList(d) ? d : [d]);
271
- }
272
- return evalQuantifier("every", node.bindings, domains, 0, ctx, node.satisfies);
273
- }
296
+ case "for":
297
+ return evalFor(node.bindings, node.body, ctx);
298
+ case "some":
299
+ case "every":
300
+ return evalQuantifier(node.kind, node.bindings, node.satisfies, ctx);
274
301
  case "between": {
275
302
  const val = evaluate(node.value, ctx);
276
303
  const low = evaluate(node.low, ctx);
@@ -281,11 +308,11 @@ export function evaluate(node, ctx) {
281
308
  return null;
282
309
  return cmpLow >= 0 && cmpHigh <= 0;
283
310
  }
284
- case "in-test": {
285
- const val = evaluate(node.value, ctx);
286
- const test = evaluate(node.test, ctx);
287
- return testIncludes(test, val);
288
- }
311
+ case "in-test":
312
+ // The right-hand side is a unary test, evaluated with the left-hand
313
+ // value as its implicit input. Unlike a decision table's test, this
314
+ // one keeps an unknown answer unknown.
315
+ return unaryTestValue(node.test, evaluate(node.value, ctx), ctx);
289
316
  case "instance-of": {
290
317
  const val = evaluate(node.value, ctx);
291
318
  return checkInstanceOf(val, node.typeName);
@@ -333,9 +360,8 @@ function evalBinary(op, leftNode, rightNode, ctx) {
333
360
  const r = evaluate(rightNode, ctx);
334
361
  if (r === false)
335
362
  return false;
336
- if (l === null || r === null)
337
- return null;
338
- return true;
363
+ // Anything that is not a boolean leaves the result unknown.
364
+ return l === true && r === true ? true : null;
339
365
  }
340
366
  if (op === "or") {
341
367
  const l = evaluate(leftNode, ctx);
@@ -344,16 +370,21 @@ function evalBinary(op, leftNode, rightNode, ctx) {
344
370
  const r = evaluate(rightNode, ctx);
345
371
  if (r === true)
346
372
  return true;
347
- if (l === null || r === null)
348
- return null;
349
- return false;
373
+ return l === false && r === false ? false : null;
350
374
  }
351
375
  const left = evaluate(leftNode, ctx);
352
376
  const right = evaluate(rightNode, ctx);
353
- if (op === "=")
354
- return deepEqual(left, right);
355
- if (op === "!=")
356
- return !deepEqual(left, right);
377
+ if (op === "=" || op === "!=") {
378
+ // Comparing values of different types says nothing, so it is null
379
+ // rather than false. Comparing against null stays a real answer.
380
+ if (left !== null && right !== null && typeTag(left) !== typeTag(right))
381
+ return null;
382
+ // Temporal values are equal when they name the same point, however
383
+ // each was written: 12:00-01:00 and 17:00+04:00 are one instant.
384
+ const instant = isTemporal(left) ? compareValues(left, right) : null;
385
+ const equal = instant !== null ? instant === 0 : deepEqual(left, right);
386
+ return op === "=" ? equal : !equal;
387
+ }
357
388
  if (left === null || right === null)
358
389
  return null;
359
390
  if (op === "+" || op === "-") {
@@ -409,6 +440,19 @@ function evalBinary(op, leftNode, rightNode, ctx) {
409
440
  return cmp >= 0;
410
441
  return null;
411
442
  }
443
+ /**
444
+ * Orders the arguments of a named invocation of a user-defined function. A
445
+ * function that declares no parameter names, or that is handed a name it does
446
+ * not declare, gets nulls rather than a silently mis-ordered argument list.
447
+ */
448
+ function orderArgs(paramNames, argNames, values) {
449
+ if (!paramNames)
450
+ return [];
451
+ return paramNames.map((param) => {
452
+ const idx = argNames.indexOf(param);
453
+ return idx >= 0 ? (values[idx] ?? null) : null;
454
+ });
455
+ }
412
456
  function evalCall(callee, argNodes, ctx) {
413
457
  const builtin = getBuiltin(callee);
414
458
  if (builtin) {
@@ -421,63 +465,100 @@ function evalCall(callee, argNodes, ctx) {
421
465
  const args = argNodes.map((a) => evaluate(a, ctx));
422
466
  return fn.call(args);
423
467
  }
424
- function evalCartesian(bindings, domains, idx, ctx, forNode, partial) {
425
- if (idx === bindings.length) {
426
- const vars = { partial: [...partial] };
427
- const binding = bindings[idx - 1];
428
- if (binding)
429
- vars[binding.name] = partial[partial.length - 1] ?? null;
430
- const result = evaluate(forNode.body, childCtx(ctx, vars));
431
- partial.push(result);
432
- return [result];
433
- }
434
- const binding = bindings[idx];
435
- if (!binding)
436
- return [];
437
- const domain = domains[idx] ?? [];
468
+ /**
469
+ * Evaluates a `for`. Each binding's domain is evaluated with the bindings to
470
+ * its left already in scope, so `for x in xs, y in x` works, and the body sees
471
+ * the results produced so far as `partial`.
472
+ */
473
+ function evalFor(bindings, body, ctx) {
438
474
  const results = [];
439
- for (const val of domain) {
440
- const vars = { partial: [...partial] };
441
- vars[binding.name] = val;
442
- // Propagate earlier bindings too
443
- for (let i = 0; i < idx; i++) {
444
- const b = bindings[i];
445
- if (b)
446
- vars[b.name] = partial[i] ?? null;
475
+ let failed = false;
476
+ const iterate = (idx, scope) => {
477
+ if (failed)
478
+ return;
479
+ if (idx === bindings.length) {
480
+ results.push(evaluate(body, childCtx(scope, { partial: [...results] })));
481
+ return;
447
482
  }
448
- const sub = evalCartesian(bindings, domains, idx + 1, childCtx(ctx, vars), forNode, [
449
- ...partial,
450
- val,
451
- ]);
452
- results.push(...sub);
453
- }
454
- return results;
483
+ const binding = bindings[idx];
484
+ if (!binding)
485
+ return;
486
+ const domain = iterationValues(evaluate(binding.domain, scope));
487
+ if (domain === null) {
488
+ failed = true;
489
+ return;
490
+ }
491
+ for (const value of domain) {
492
+ iterate(idx + 1, childCtx(scope, { [binding.name]: value }));
493
+ }
494
+ };
495
+ iterate(0, ctx);
496
+ return failed ? null : results;
455
497
  }
456
- function evalQuantifier(kind, bindings, domains, idx, ctx, satisfies) {
457
- if (idx === bindings.length) {
458
- return evaluate(satisfies, ctx);
459
- }
460
- const binding = bindings[idx];
461
- if (!binding)
462
- return kind === "every";
463
- const domain = domains[idx] ?? [];
464
- if (domain.length === 0)
465
- return kind === "every";
466
- let hasNull = false;
467
- for (const val of domain) {
468
- const vars = {};
469
- vars[binding.name] = val;
470
- const r = evalQuantifier(kind, bindings, domains, idx + 1, childCtx(ctx, vars), satisfies);
471
- if (kind === "some" && r === true)
472
- return true;
473
- if (kind === "every" && r === false)
474
- return false;
475
- if (r === null)
476
- hasNull = true;
477
- }
498
+ /**
499
+ * Evaluates `some`/`every`. A definite answer wins over an unknown one: one
500
+ * true satisfies `some` whatever else the domain holds, and one false settles
501
+ * `every`. Otherwise an unknown anywhere makes the whole answer unknown.
502
+ */
503
+ function evalQuantifier(kind, bindings, satisfies, ctx) {
504
+ let sawTrue = false;
505
+ let sawFalse = false;
506
+ let sawUnknown = false;
507
+ let failed = false;
508
+ const visit = (idx, scope) => {
509
+ if (failed)
510
+ return;
511
+ if (idx === bindings.length) {
512
+ const result = asBoolean(evaluate(satisfies, scope));
513
+ if (result === true)
514
+ sawTrue = true;
515
+ else if (result === false)
516
+ sawFalse = true;
517
+ else
518
+ sawUnknown = true;
519
+ return;
520
+ }
521
+ const binding = bindings[idx];
522
+ if (!binding)
523
+ return;
524
+ const domain = iterationValues(evaluate(binding.domain, scope));
525
+ if (domain === null) {
526
+ failed = true;
527
+ return;
528
+ }
529
+ for (const value of domain) {
530
+ visit(idx + 1, childCtx(scope, { [binding.name]: value }));
531
+ }
532
+ };
533
+ visit(0, ctx);
534
+ if (failed)
535
+ return null;
478
536
  if (kind === "some")
479
- return hasNull ? null : false;
480
- return hasNull ? null : true;
537
+ return sawTrue ? true : sawUnknown ? null : false;
538
+ return sawFalse ? false : sawUnknown ? null : true;
539
+ }
540
+ function asBoolean(v) {
541
+ return typeof v === "boolean" ? v : null;
542
+ }
543
+ /** True for the values that name a point in time or a length of it. */
544
+ function isTemporal(v) {
545
+ return (isFeelDate(v) ||
546
+ isFeelTime(v) ||
547
+ isFeelDateTime(v) ||
548
+ isFeelDayTimeDuration(v) ||
549
+ isFeelYearsMonthsDuration(v));
550
+ }
551
+ /** The FEEL type of a value, for deciding whether two values are comparable. */
552
+ function typeTag(v) {
553
+ if (v === null)
554
+ return "null";
555
+ if (Array.isArray(v))
556
+ return "list";
557
+ const t = typeof v;
558
+ if (t !== "object")
559
+ return t;
560
+ const tagged = v.type;
561
+ return typeof tagged === "string" ? tagged : "context";
481
562
  }
482
563
  function deepEqual(a, b) {
483
564
  if (a === b)
@@ -511,6 +592,10 @@ function deepEqual(a, b) {
511
592
  return false;
512
593
  }
513
594
  function testIncludes(test, val) {
595
+ // Two lists are compared, not searched: [1,2,3] is a member of
596
+ // [[1,2,3,4], [1,2,3]] rather than of its elements.
597
+ if (isFeelList(test) && isFeelList(val))
598
+ return deepEqual(test, val);
514
599
  if (isFeelRange(test)) {
515
600
  const cmpStart = compareValues(val, test.start);
516
601
  const cmpEnd = compareValues(val, test.end);
@@ -531,6 +616,10 @@ function testIncludes(test, val) {
531
616
  return deepEqual(val, test);
532
617
  }
533
618
  function checkInstanceOf(val, typeName) {
619
+ // null is not an instance of anything, Any included: it is the absence of
620
+ // a value rather than a value of some type.
621
+ if (val === null)
622
+ return typeName === "null" || typeName === "Null";
534
623
  switch (typeName) {
535
624
  case "number":
536
625
  return typeof val === "number";
@@ -545,8 +634,10 @@ function checkInstanceOf(val, typeName) {
545
634
  case "date and time":
546
635
  return isFeelDateTime(val);
547
636
  case "days and time duration":
637
+ case "dayTimeDuration":
548
638
  return isFeelDayTimeDuration(val);
549
639
  case "years and months duration":
640
+ case "yearMonthDuration":
550
641
  return isFeelYearsMonthsDuration(val);
551
642
  case "list":
552
643
  return Array.isArray(val);
@@ -555,25 +646,39 @@ function checkInstanceOf(val, typeName) {
555
646
  case "function":
556
647
  return typeof val === "object" && val !== null && "call" in val;
557
648
  case "Any":
649
+ case "any":
558
650
  return true;
559
651
  case "null":
560
- return val === null;
652
+ case "Null":
653
+ return false;
561
654
  default:
562
655
  return false;
563
656
  }
564
657
  }
565
- /** Evaluate a unary test against an input value. Returns boolean. */
566
- export function evaluateUnaryTest(node, input, ctx) {
658
+ /**
659
+ * Evaluates a unary test, keeping an unknown answer unknown. A range whose
660
+ * bound is null, or an input of null, says nothing about membership.
661
+ */
662
+ function unaryTestValue(node, input, ctx) {
567
663
  const withInput = { ...ctx, input };
568
664
  const result = evaluate(node, withInput);
569
665
  if (typeof result === "boolean")
570
666
  return result;
571
667
  // Range result in unary-test context → membership test
572
668
  if (isFeelRange(result))
573
- return testIncludes(result, input) === true;
669
+ return testIncludes(result, input);
574
670
  // List result → any element matches
575
- if (isFeelList(result))
576
- return result.some((r) => testIncludes(r, input) === true);
671
+ if (isFeelList(result)) {
672
+ let unknown = false;
673
+ for (const item of result) {
674
+ const match = testIncludes(item, input);
675
+ if (match === true)
676
+ return true;
677
+ if (match === null)
678
+ unknown = true;
679
+ }
680
+ return unknown ? null : false;
681
+ }
577
682
  // A plain expression in unary test mode is an equality test.
578
683
  // When the result is null: only match if the node itself is the null literal
579
684
  // (null arithmetic in comparisons also yields null but must not match anything).
@@ -581,6 +686,13 @@ export function evaluateUnaryTest(node, input, ctx) {
581
686
  return deepEqual(result, input);
582
687
  return node.kind === "null" ? input === null : false;
583
688
  }
689
+ /**
690
+ * Evaluates a unary test against an input value. A decision table's rule
691
+ * either matches or it does not, so an unknown answer is not a match.
692
+ */
693
+ export function evaluateUnaryTest(node, input, ctx) {
694
+ return unaryTestValue(node, input, ctx) === true;
695
+ }
584
696
  /** Evaluate a full unary-test node (the root returned by parseUnaryTests). */
585
697
  export function evaluateUnaryTests(node, input, ctx) {
586
698
  return evaluateUnaryTest(node, input, ctx);
package/dist/formatter.js CHANGED
@@ -6,6 +6,17 @@ export function formatFeel(node, opts) {
6
6
  const o = { ...DEFAULTS, ...opts };
7
7
  return fmt(node, o, 0);
8
8
  }
9
+ const STRING_ESCAPES = {
10
+ "\\": "\\\\",
11
+ '"': '\\"',
12
+ "\n": "\\n",
13
+ "\r": "\\r",
14
+ "\t": "\\t",
15
+ };
16
+ /** Re-escapes a string literal's value, which the parser stores decoded. */
17
+ function escapeString(value) {
18
+ return value.replace(/[\\"\n\r\t]/g, (c) => STRING_ESCAPES[c] ?? c);
19
+ }
9
20
  function fmt(node, o, depth) {
10
21
  const ind = o.indent.repeat(depth);
11
22
  const ind1 = o.indent.repeat(depth + 1);
@@ -17,7 +28,7 @@ function fmt(node, o, depth) {
17
28
  case "number":
18
29
  return String(node.value);
19
30
  case "string":
20
- return `"${node.value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
31
+ return `"${escapeString(node.value)}"`;
21
32
  case "temporal":
22
33
  return node.raw;
23
34
  case "name":
@@ -64,6 +75,10 @@ function fmt(node, o, depth) {
64
75
  const args = node.args.map((a) => fmt(a, o, depth)).join(", ");
65
76
  return `${node.callee}(${args})`;
66
77
  }
78
+ case "call-expr": {
79
+ const args = node.args.map((a) => fmt(a, o, depth)).join(", ");
80
+ return `${fmt(node.target, o, depth)}(${args})`;
81
+ }
67
82
  case "call-named": {
68
83
  const args = node.args.map((a) => `${a.name}: ${fmt(a.value, o, depth)}`).join(", ");
69
84
  return `${node.callee}(${args})`;
package/dist/index.d.ts CHANGED
@@ -4,11 +4,12 @@ export { tokenize } from "./lexer.js";
4
4
  export type { FeelToken, FeelTokenKind } from "./lexer.js";
5
5
  export type { FeelNode, BinaryOp } from "./ast.js";
6
6
  export { parseExpression, parseUnaryTests } from "./parser.js";
7
- export type { ParseResult, ParseError } from "./parser.js";
7
+ export type { ParseResult, ParseError, ParseOptions } from "./parser.js";
8
8
  export { evaluate, evaluateUnaryTests, evaluateUnaryTest } from "./evaluator.js";
9
9
  export type { EvalContext } from "./evaluator.js";
10
10
  export { formatFeel } from "./formatter.js";
11
11
  export type { FormatOptions } from "./formatter.js";
12
+ export { builtinNames, getBuiltin } from "./builtins.js";
12
13
  export { annotate, highlightToHtml, highlightFeel } from "./highlighter.js";
13
14
  export type { AnnotatedToken, HighlightKind } from "./highlighter.js";
14
15
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -7,6 +7,8 @@ export { parseExpression, parseUnaryTests } from "./parser.js";
7
7
  export { evaluate, evaluateUnaryTests, evaluateUnaryTest } from "./evaluator.js";
8
8
  // Formatter
9
9
  export { formatFeel } from "./formatter.js";
10
+ // Built-ins
11
+ export { builtinNames, getBuiltin } from "./builtins.js";
10
12
  // Highlighter
11
13
  export { annotate, highlightToHtml, highlightFeel } from "./highlighter.js";
12
14
  //# sourceMappingURL=index.js.map
package/dist/lexer.d.ts CHANGED
@@ -6,4 +6,11 @@ export interface FeelToken {
6
6
  end: number;
7
7
  }
8
8
  export declare function tokenize(input: string): FeelToken[];
9
+ /**
10
+ * Decodes the escape sequences of a FEEL string literal body (the text between
11
+ * the quotes). Recognizes \' \" \\ \n \r \t, \uXXXX and the extended
12
+ * \UXXXXXX form; an unrecognized sequence is left as written, since dropping
13
+ * the backslash would silently alter the author's data.
14
+ */
15
+ export declare function unescapeString(body: string): string;
9
16
  //# sourceMappingURL=lexer.d.ts.map