@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.
- package/README.md +112 -0
- package/dist/ast.d.ts +111 -0
- package/dist/ast.js +2 -0
- package/dist/builtins.d.ts +10 -0
- package/dist/builtins.js +1194 -0
- package/dist/evaluator.d.ts +13 -0
- package/dist/evaluator.js +586 -0
- package/dist/formatter.d.ts +7 -0
- package/dist/formatter.js +120 -0
- package/dist/highlighter.d.ts +14 -0
- package/dist/highlighter.js +149 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/lexer.d.ts +9 -0
- package/dist/lexer.js +161 -0
- package/dist/parser.d.ts +13 -0
- package/dist/parser.js +853 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +129 -0
- package/package.json +29 -0
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
package/dist/lexer.d.ts
ADDED
|
@@ -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
|
package/dist/parser.d.ts
ADDED
|
@@ -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
|