@bpmnkit/feel 0.0.8

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.
@@ -0,0 +1,120 @@
1
+ const DEFAULTS = {
2
+ indent: " ",
3
+ maxLineLength: 80,
4
+ };
5
+ export function formatFeel(node, opts) {
6
+ const o = { ...DEFAULTS, ...opts };
7
+ return fmt(node, o, 0);
8
+ }
9
+ function fmt(node, o, depth) {
10
+ const ind = o.indent.repeat(depth);
11
+ const ind1 = o.indent.repeat(depth + 1);
12
+ switch (node.kind) {
13
+ case "null":
14
+ return "null";
15
+ case "boolean":
16
+ return String(node.value);
17
+ case "number":
18
+ return String(node.value);
19
+ case "string":
20
+ return `"${node.value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
21
+ case "temporal":
22
+ return node.raw;
23
+ case "name":
24
+ return node.name;
25
+ case "unary-minus":
26
+ return `-${fmt(node.operand, o, depth)}`;
27
+ case "binary": {
28
+ const l = fmt(node.left, o, depth);
29
+ const r = fmt(node.right, o, depth);
30
+ return `${l} ${node.op} ${r}`;
31
+ }
32
+ case "path":
33
+ return `${fmt(node.base, o, depth)}.${node.key}`;
34
+ case "filter":
35
+ return `${fmt(node.base, o, depth)}[${fmt(node.condition, o, depth)}]`;
36
+ case "range": {
37
+ const s = fmt(node.low, o, depth);
38
+ const e = fmt(node.high, o, depth);
39
+ const open = node.startIncluded ? "[" : "(";
40
+ const close = node.endIncluded ? "]" : ")";
41
+ return `${open}${s}..${e}${close}`;
42
+ }
43
+ case "list": {
44
+ if (node.items.length === 0)
45
+ return "[]";
46
+ const inline = `[${node.items.map((i) => fmt(i, o, depth)).join(", ")}]`;
47
+ if (inline.length <= o.maxLineLength - depth * o.indent.length)
48
+ return inline;
49
+ const items = node.items.map((i) => `${ind1}${fmt(i, o, depth + 1)}`).join(",\n");
50
+ return `[\n${items}\n${ind}]`;
51
+ }
52
+ case "context": {
53
+ if (node.entries.length === 0)
54
+ return "{}";
55
+ const inline = `{${node.entries.map((e) => `${e.key}: ${fmt(e.value, o, depth)}`).join(", ")}}`;
56
+ if (inline.length <= o.maxLineLength - depth * o.indent.length)
57
+ return inline;
58
+ const entries = node.entries
59
+ .map((e) => `${ind1}${e.key}: ${fmt(e.value, o, depth + 1)}`)
60
+ .join(",\n");
61
+ return `{\n${entries}\n${ind}}`;
62
+ }
63
+ case "call": {
64
+ const args = node.args.map((a) => fmt(a, o, depth)).join(", ");
65
+ return `${node.callee}(${args})`;
66
+ }
67
+ case "call-named": {
68
+ const args = node.args.map((a) => `${a.name}: ${fmt(a.value, o, depth)}`).join(", ");
69
+ return `${node.callee}(${args})`;
70
+ }
71
+ case "if": {
72
+ const cond = fmt(node.condition, o, depth);
73
+ const thn = fmt(node.then, o, depth);
74
+ const els = fmt(node.else, o, depth);
75
+ const inline = `if ${cond} then ${thn} else ${els}`;
76
+ if (inline.length <= o.maxLineLength - depth * o.indent.length)
77
+ return inline;
78
+ return `if ${cond}\n${ind1}then ${thn}\n${ind1}else ${els}`;
79
+ }
80
+ case "for": {
81
+ const bindings = node.bindings
82
+ .map((b) => `${b.name} in ${fmt(b.domain, o, depth)}`)
83
+ .join(", ");
84
+ const body = fmt(node.body, o, depth);
85
+ const inline = `for ${bindings} return ${body}`;
86
+ if (inline.length <= o.maxLineLength - depth * o.indent.length)
87
+ return inline;
88
+ return `for ${bindings}\n${ind1}return ${body}`;
89
+ }
90
+ case "some":
91
+ case "every": {
92
+ const bindings = node.bindings
93
+ .map((b) => `${b.name} in ${fmt(b.domain, o, depth)}`)
94
+ .join(", ");
95
+ const sat = fmt(node.satisfies, o, depth);
96
+ const inline = `${node.kind} ${bindings} satisfies ${sat}`;
97
+ if (inline.length <= o.maxLineLength - depth * o.indent.length)
98
+ return inline;
99
+ return `${node.kind} ${bindings}\n${ind1}satisfies ${sat}`;
100
+ }
101
+ case "between":
102
+ return `${fmt(node.value, o, depth)} between ${fmt(node.low, o, depth)} and ${fmt(node.high, o, depth)}`;
103
+ case "in-test":
104
+ return `${fmt(node.value, o, depth)} in ${fmt(node.test, o, depth)}`;
105
+ case "instance-of":
106
+ return `${fmt(node.value, o, depth)} instance of ${node.typeName}`;
107
+ case "function-def": {
108
+ const params = node.params.join(", ");
109
+ const body = fmt(node.body, o, depth);
110
+ return `function(${params}) ${body}`;
111
+ }
112
+ case "unary-test-list":
113
+ return node.tests.map((t) => fmt(t, o, depth)).join(", ");
114
+ case "unary-not":
115
+ return `not(${node.tests.map((t) => fmt(t, o, depth)).join(", ")})`;
116
+ case "any-input":
117
+ return "-";
118
+ }
119
+ }
120
+ //# sourceMappingURL=formatter.js.map
@@ -0,0 +1,14 @@
1
+ export type HighlightKind = "keyword" | "operator" | "literal-number" | "literal-string" | "literal-temporal" | "literal-bool" | "literal-null" | "literal-range" | "builtin" | "variable" | "comment" | "punctuation" | "plain";
2
+ export interface AnnotatedToken {
3
+ kind: HighlightKind;
4
+ value: string;
5
+ start: number;
6
+ end: number;
7
+ }
8
+ /** Annotate token stream with semantic highlight kinds. */
9
+ export declare function annotate(input: string): AnnotatedToken[];
10
+ /** Render annotated tokens to HTML with span wrappers. */
11
+ export declare function highlightToHtml(input: string): string;
12
+ /** Backward-compatible alias. */
13
+ export declare const highlightFeel: typeof highlightToHtml;
14
+ //# sourceMappingURL=highlighter.d.ts.map
@@ -0,0 +1,149 @@
1
+ import { builtinNames } from "./builtins.js";
2
+ import { tokenize } from "./lexer.js";
3
+ const BUILTINS = new Set(builtinNames());
4
+ // Multi-word built-in names for lookahead annotation
5
+ const BUILTIN_PREFIXES = (() => {
6
+ const s = new Set();
7
+ for (const name of BUILTINS) {
8
+ const parts = name.split(" ");
9
+ for (let i = 1; i < parts.length; i++) {
10
+ s.add(parts.slice(0, i).join(" "));
11
+ }
12
+ }
13
+ return s;
14
+ })();
15
+ function classifyToken(tok, allTokens, idx) {
16
+ switch (tok.kind) {
17
+ case "number":
18
+ return { kind: "literal-number" };
19
+ case "string":
20
+ return { kind: "literal-string" };
21
+ case "temporal":
22
+ return { kind: "literal-temporal" };
23
+ case "comment":
24
+ return { kind: "comment" };
25
+ case "whitespace":
26
+ return { kind: "plain" };
27
+ case "unknown":
28
+ return { kind: "plain" };
29
+ case "backtick":
30
+ return { kind: "variable" };
31
+ case "op":
32
+ case "punct":
33
+ return { kind: tok.kind === "punct" ? "punctuation" : "operator" };
34
+ case "keyword": {
35
+ if (tok.value === "true" || tok.value === "false")
36
+ return { kind: "literal-bool" };
37
+ if (tok.value === "null")
38
+ return { kind: "literal-null" };
39
+ return { kind: "keyword" };
40
+ }
41
+ case "name": {
42
+ // Try to match multi-word builtin
43
+ let name = tok.value;
44
+ let lookahead = idx + 1;
45
+ while (BUILTIN_PREFIXES.has(name)) {
46
+ // Skip whitespace
47
+ while (lookahead < allTokens.length && allTokens[lookahead]?.kind === "whitespace") {
48
+ lookahead++;
49
+ }
50
+ const next = allTokens[lookahead];
51
+ if (!next || (next.kind !== "name" && next.kind !== "keyword"))
52
+ break;
53
+ const extended = `${name} ${next.value}`;
54
+ if (BUILTINS.has(extended) || BUILTIN_PREFIXES.has(extended)) {
55
+ name = extended;
56
+ lookahead++;
57
+ }
58
+ else {
59
+ break;
60
+ }
61
+ }
62
+ if (BUILTINS.has(name))
63
+ return { kind: "builtin", name };
64
+ return { kind: "variable" };
65
+ }
66
+ }
67
+ }
68
+ /** Annotate token stream with semantic highlight kinds. */
69
+ export function annotate(input) {
70
+ const tokens = tokenize(input);
71
+ const result = [];
72
+ let i = 0;
73
+ while (i < tokens.length) {
74
+ const tok = tokens[i];
75
+ if (!tok) {
76
+ i++;
77
+ continue;
78
+ }
79
+ // Try multi-word built-in name
80
+ if (tok.kind === "name") {
81
+ let name = tok.value;
82
+ let end = i + 1;
83
+ let endPos = tok.end;
84
+ // Greedy multi-word match
85
+ let lookahead = end;
86
+ while (BUILTIN_PREFIXES.has(name)) {
87
+ const wsStart = lookahead;
88
+ while (lookahead < tokens.length && tokens[lookahead]?.kind === "whitespace")
89
+ lookahead++;
90
+ const next = tokens[lookahead];
91
+ if (!next || (next.kind !== "name" && next.kind !== "keyword")) {
92
+ lookahead = wsStart;
93
+ break;
94
+ }
95
+ const extended = `${name} ${next.value}`;
96
+ if (BUILTINS.has(extended) || BUILTIN_PREFIXES.has(extended)) {
97
+ name = extended;
98
+ lookahead++;
99
+ end = lookahead;
100
+ endPos = next.end;
101
+ }
102
+ else {
103
+ lookahead = wsStart;
104
+ break;
105
+ }
106
+ }
107
+ if (BUILTINS.has(name) && end > i + 1) {
108
+ // Multi-word builtin: emit all tokens as builtin
109
+ for (let j = i; j < end; j++) {
110
+ const t = tokens[j];
111
+ if (!t)
112
+ continue;
113
+ result.push({
114
+ kind: t.kind === "whitespace" ? "plain" : "builtin",
115
+ value: t.value,
116
+ start: t.start,
117
+ end: t.end,
118
+ });
119
+ }
120
+ i = end;
121
+ continue;
122
+ }
123
+ }
124
+ const { kind } = classifyToken(tok, tokens, i);
125
+ result.push({ kind, value: tok.value, start: tok.start, end: tok.end });
126
+ i++;
127
+ }
128
+ return result;
129
+ }
130
+ function escapeHtml(s) {
131
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
132
+ }
133
+ /** Render annotated tokens to HTML with span wrappers. */
134
+ export function highlightToHtml(input) {
135
+ if (!input.trim())
136
+ return escapeHtml(input) || '<span class="feel-empty">-</span>';
137
+ const tokens = annotate(input);
138
+ return tokens
139
+ .map((t) => {
140
+ const escaped = escapeHtml(t.value);
141
+ if (t.kind === "plain")
142
+ return escaped;
143
+ return `<span class="feel-${t.kind}">${escaped}</span>`;
144
+ })
145
+ .join("");
146
+ }
147
+ /** Backward-compatible alias. */
148
+ export const highlightFeel = highlightToHtml;
149
+ //# sourceMappingURL=highlighter.js.map
@@ -0,0 +1,14 @@
1
+ export type { FeelValue, FeelDate, FeelTime, FeelDateTime, FeelDayTimeDuration, FeelYearsMonthsDuration, FeelRange, FeelContext, FeelFunction, } from "./types.js";
2
+ export { isFeelDate, isFeelTime, isFeelDateTime, isFeelDayTimeDuration, isFeelYearsMonthsDuration, isFeelDuration, isFeelList, isFeelContext, isFeelRange, isFeelFunction, getProperty, } from "./types.js";
3
+ export { tokenize } from "./lexer.js";
4
+ export type { FeelToken, FeelTokenKind } from "./lexer.js";
5
+ export type { FeelNode, BinaryOp } from "./ast.js";
6
+ export { parseExpression, parseUnaryTests } from "./parser.js";
7
+ export type { ParseResult, ParseError } from "./parser.js";
8
+ export { evaluate, evaluateUnaryTests, evaluateUnaryTest } from "./evaluator.js";
9
+ export type { EvalContext } from "./evaluator.js";
10
+ export { formatFeel } from "./formatter.js";
11
+ export type { FormatOptions } from "./formatter.js";
12
+ export { annotate, highlightToHtml, highlightFeel } from "./highlighter.js";
13
+ export type { AnnotatedToken, HighlightKind } from "./highlighter.js";
14
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ export { isFeelDate, isFeelTime, isFeelDateTime, isFeelDayTimeDuration, isFeelYearsMonthsDuration, isFeelDuration, isFeelList, isFeelContext, isFeelRange, isFeelFunction, getProperty, } from "./types.js";
2
+ // Lexer
3
+ export { tokenize } from "./lexer.js";
4
+ // Parser
5
+ export { parseExpression, parseUnaryTests } from "./parser.js";
6
+ // Evaluator
7
+ export { evaluate, evaluateUnaryTests, evaluateUnaryTest } from "./evaluator.js";
8
+ // Formatter
9
+ export { formatFeel } from "./formatter.js";
10
+ // Highlighter
11
+ export { annotate, highlightToHtml, highlightFeel } from "./highlighter.js";
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,9 @@
1
+ export type FeelTokenKind = "number" | "string" | "temporal" | "name" | "keyword" | "op" | "punct" | "comment" | "whitespace" | "backtick" | "unknown";
2
+ export interface FeelToken {
3
+ kind: FeelTokenKind;
4
+ value: string;
5
+ start: number;
6
+ end: number;
7
+ }
8
+ export declare function tokenize(input: string): FeelToken[];
9
+ //# sourceMappingURL=lexer.d.ts.map
package/dist/lexer.js ADDED
@@ -0,0 +1,161 @@
1
+ const KEYWORDS = new Set([
2
+ "true",
3
+ "false",
4
+ "null",
5
+ "if",
6
+ "then",
7
+ "else",
8
+ "for",
9
+ "in",
10
+ "return",
11
+ "some",
12
+ "every",
13
+ "satisfies",
14
+ "function",
15
+ "external",
16
+ "not",
17
+ "and",
18
+ "or",
19
+ "between",
20
+ "instance",
21
+ "of",
22
+ ]);
23
+ export function tokenize(input) {
24
+ const tokens = [];
25
+ let i = 0;
26
+ const len = input.length;
27
+ const ch = (offset = 0) => input.charAt(i + offset);
28
+ const slice = (start, end) => input.slice(start, end);
29
+ while (i < len) {
30
+ const start = i;
31
+ // Line comment
32
+ if (ch() === "/" && ch(1) === "/") {
33
+ i += 2;
34
+ while (i < len && ch() !== "\n")
35
+ i++;
36
+ tokens.push({ kind: "comment", value: slice(start, i), start, end: i });
37
+ continue;
38
+ }
39
+ // Block comment
40
+ if (ch() === "/" && ch(1) === "*") {
41
+ i += 2;
42
+ while (i < len && !(ch() === "*" && ch(1) === "/"))
43
+ i++;
44
+ i += 2;
45
+ tokens.push({ kind: "comment", value: slice(start, i), start, end: i });
46
+ continue;
47
+ }
48
+ // Temporal literal @"..."
49
+ if (ch() === "@" && ch(1) === '"') {
50
+ i += 2;
51
+ while (i < len && ch() !== '"') {
52
+ if (ch() === "\\")
53
+ i++;
54
+ i++;
55
+ }
56
+ i++; // closing "
57
+ tokens.push({ kind: "temporal", value: slice(start, i), start, end: i });
58
+ continue;
59
+ }
60
+ // String literal
61
+ if (ch() === '"') {
62
+ i++;
63
+ while (i < len && ch() !== '"') {
64
+ if (ch() === "\\")
65
+ i++;
66
+ i++;
67
+ }
68
+ i++; // closing "
69
+ tokens.push({ kind: "string", value: slice(start, i), start, end: i });
70
+ continue;
71
+ }
72
+ // Backtick name
73
+ if (ch() === "`") {
74
+ i++;
75
+ while (i < len && ch() !== "`")
76
+ i++;
77
+ i++; // closing `
78
+ tokens.push({
79
+ kind: "backtick",
80
+ value: slice(start + 1, i - 1),
81
+ start,
82
+ end: i,
83
+ });
84
+ continue;
85
+ }
86
+ // Whitespace
87
+ if (ch() === " " || ch() === "\t" || ch() === "\n" || ch() === "\r") {
88
+ while (i < len && (ch() === " " || ch() === "\t" || ch() === "\n" || ch() === "\r"))
89
+ i++;
90
+ tokens.push({ kind: "whitespace", value: slice(start, i), start, end: i });
91
+ continue;
92
+ }
93
+ // Two-char operators (check before single-char)
94
+ const two = slice(i, i + 2);
95
+ if (two === "**" ||
96
+ two === ">=" ||
97
+ two === "<=" ||
98
+ two === "!=" ||
99
+ two === "->" ||
100
+ two === "..") {
101
+ tokens.push({ kind: "op", value: two, start, end: i + 2 });
102
+ i += 2;
103
+ continue;
104
+ }
105
+ // "==" is not standard FEEL but users familiar with JS/Java write it; treat as "=".
106
+ if (two === "==") {
107
+ tokens.push({ kind: "op", value: "=", start, end: i + 2 });
108
+ i += 2;
109
+ continue;
110
+ }
111
+ // Single-char operators
112
+ if ("+-*/=<>?".includes(ch())) {
113
+ tokens.push({ kind: "op", value: ch(), start, end: i + 1 });
114
+ i++;
115
+ continue;
116
+ }
117
+ // Punctuation
118
+ if ("()[]{},:".includes(ch())) {
119
+ tokens.push({ kind: "punct", value: ch(), start, end: i + 1 });
120
+ i++;
121
+ continue;
122
+ }
123
+ // Dot (not ..)
124
+ if (ch() === ".") {
125
+ tokens.push({ kind: "punct", value: ".", start, end: i + 1 });
126
+ i++;
127
+ continue;
128
+ }
129
+ // Number (only consume one decimal point, and only if followed by a digit)
130
+ if (ch() >= "0" && ch() <= "9") {
131
+ while (i < len && ch() >= "0" && ch() <= "9")
132
+ i++;
133
+ // Consume decimal fraction only if next char is '.' followed by a digit (not '..')
134
+ if (i < len && ch() === "." && i + 1 < len && ch(1) >= "0" && ch(1) <= "9") {
135
+ i++; // consume the '.'
136
+ while (i < len && ch() >= "0" && ch() <= "9")
137
+ i++;
138
+ }
139
+ tokens.push({ kind: "number", value: slice(start, i), start, end: i });
140
+ continue;
141
+ }
142
+ // Identifier / keyword
143
+ if ((ch() >= "a" && ch() <= "z") || (ch() >= "A" && ch() <= "Z") || ch() === "_") {
144
+ while (i < len &&
145
+ ((ch() >= "a" && ch() <= "z") ||
146
+ (ch() >= "A" && ch() <= "Z") ||
147
+ (ch() >= "0" && ch() <= "9") ||
148
+ ch() === "_"))
149
+ i++;
150
+ const word = slice(start, i);
151
+ const kind = KEYWORDS.has(word) ? "keyword" : "name";
152
+ tokens.push({ kind, value: word, start, end: i });
153
+ continue;
154
+ }
155
+ // Unknown character
156
+ tokens.push({ kind: "unknown", value: ch(), start, end: i + 1 });
157
+ i++;
158
+ }
159
+ return tokens;
160
+ }
161
+ //# sourceMappingURL=lexer.js.map
@@ -0,0 +1,13 @@
1
+ import type { FeelNode } from "./ast.js";
2
+ export interface ParseError {
3
+ message: string;
4
+ start: number;
5
+ end: number;
6
+ }
7
+ export interface ParseResult {
8
+ ast: FeelNode | null;
9
+ errors: ParseError[];
10
+ }
11
+ export declare function parseExpression(input: string): ParseResult;
12
+ export declare function parseUnaryTests(input: string): ParseResult;
13
+ //# sourceMappingURL=parser.d.ts.map