@bpmnkit/feel 0.0.20 → 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/README.md +3 -1
- package/dist/ast.d.ts +4 -0
- package/dist/builtins.d.ts +7 -0
- package/dist/builtins.js +824 -190
- package/dist/evaluator.d.ts +4 -1
- package/dist/evaluator.js +251 -139
- package/dist/formatter.js +16 -1
- package/dist/highlighter.js +22 -9
- package/dist/index.d.ts +1 -1
- package/dist/lexer.d.ts +7 -0
- package/dist/lexer.js +169 -51
- package/dist/parser.d.ts +11 -2
- package/dist/parser.js +333 -94
- package/dist/types.js +35 -2
- package/package.json +5 -2
package/dist/lexer.js
CHANGED
|
@@ -20,142 +20,260 @@ const KEYWORDS = new Set([
|
|
|
20
20
|
"instance",
|
|
21
21
|
"of",
|
|
22
22
|
]);
|
|
23
|
+
// Character codes; comparing codes avoids allocating a one-character string
|
|
24
|
+
// for every position the scanner visits.
|
|
25
|
+
const SLASH = 0x2f;
|
|
26
|
+
const STAR = 0x2a;
|
|
27
|
+
const AT = 0x40;
|
|
28
|
+
const DQUOTE = 0x22;
|
|
29
|
+
const BACKSLASH = 0x5c;
|
|
30
|
+
const BACKTICK = 0x60;
|
|
31
|
+
const SPACE = 0x20;
|
|
32
|
+
const TAB = 0x09;
|
|
33
|
+
const LF = 0x0a;
|
|
34
|
+
const CR = 0x0d;
|
|
35
|
+
const DOT = 0x2e;
|
|
36
|
+
const GT = 0x3e;
|
|
37
|
+
const LT = 0x3c;
|
|
38
|
+
const BANG = 0x21;
|
|
39
|
+
const PLUS = 0x2b;
|
|
40
|
+
const MINUS = 0x2d;
|
|
41
|
+
const EQ = 0x3d;
|
|
42
|
+
const UNDERSCORE = 0x5f;
|
|
43
|
+
function isDigit(c) {
|
|
44
|
+
return c >= 0x30 && c <= 0x39;
|
|
45
|
+
}
|
|
46
|
+
function isLetter(c) {
|
|
47
|
+
if ((c >= 0x61 && c <= 0x7a) || (c >= 0x41 && c <= 0x5a))
|
|
48
|
+
return true;
|
|
49
|
+
// FEEL names are not limited to ASCII: anything above the ASCII range is
|
|
50
|
+
// a name character, which covers accented letters and emoji alike.
|
|
51
|
+
return c > 0x7f;
|
|
52
|
+
}
|
|
53
|
+
function isWhitespace(c) {
|
|
54
|
+
return c === SPACE || c === TAB || c === LF || c === CR;
|
|
55
|
+
}
|
|
56
|
+
const SINGLE_OPS = new Set("+-*/=<>?".split("").map((c) => c.charCodeAt(0)));
|
|
57
|
+
const PUNCT = new Set("()[]{},:".split("").map((c) => c.charCodeAt(0)));
|
|
58
|
+
/** End of an exponent starting at `i` ("e4", "e+4", "e-4"), or `i` if none. */
|
|
59
|
+
function readExponent(input, i) {
|
|
60
|
+
const c = input.charCodeAt(i);
|
|
61
|
+
if (c !== 0x65 && c !== 0x45)
|
|
62
|
+
return i;
|
|
63
|
+
let j = i + 1;
|
|
64
|
+
const sign = input.charCodeAt(j);
|
|
65
|
+
if (sign === PLUS || sign === MINUS)
|
|
66
|
+
j++;
|
|
67
|
+
if (!isDigit(input.charCodeAt(j)))
|
|
68
|
+
return i;
|
|
69
|
+
while (j < input.length && isDigit(input.charCodeAt(j)))
|
|
70
|
+
j++;
|
|
71
|
+
return j;
|
|
72
|
+
}
|
|
23
73
|
export function tokenize(input) {
|
|
24
74
|
const tokens = [];
|
|
25
75
|
let i = 0;
|
|
26
76
|
const len = input.length;
|
|
27
|
-
const ch = (offset = 0) => input.charAt(i + offset);
|
|
28
|
-
const slice = (start, end) => input.slice(start, end);
|
|
29
77
|
while (i < len) {
|
|
30
78
|
const start = i;
|
|
79
|
+
const c = input.charCodeAt(i);
|
|
80
|
+
// NaN past the end never equals any code, so lookahead needs no bounds check.
|
|
81
|
+
const next = input.charCodeAt(i + 1);
|
|
31
82
|
// Line comment
|
|
32
|
-
if (
|
|
83
|
+
if (c === SLASH && next === SLASH) {
|
|
33
84
|
i += 2;
|
|
34
|
-
while (i < len &&
|
|
85
|
+
while (i < len && input.charCodeAt(i) !== LF)
|
|
35
86
|
i++;
|
|
36
|
-
tokens.push({ kind: "comment", value: slice(start, i), start, end: i });
|
|
87
|
+
tokens.push({ kind: "comment", value: input.slice(start, i), start, end: i });
|
|
37
88
|
continue;
|
|
38
89
|
}
|
|
39
90
|
// Block comment
|
|
40
|
-
if (
|
|
91
|
+
if (c === SLASH && next === STAR) {
|
|
41
92
|
i += 2;
|
|
42
|
-
while (i < len && !(
|
|
93
|
+
while (i < len && !(input.charCodeAt(i) === STAR && input.charCodeAt(i + 1) === SLASH))
|
|
43
94
|
i++;
|
|
44
95
|
i += 2;
|
|
45
|
-
tokens.push({ kind: "comment", value: slice(start, i), start, end: i });
|
|
96
|
+
tokens.push({ kind: "comment", value: input.slice(start, i), start, end: i });
|
|
46
97
|
continue;
|
|
47
98
|
}
|
|
48
99
|
// Temporal literal @"..."
|
|
49
|
-
if (
|
|
100
|
+
if (c === AT && next === DQUOTE) {
|
|
50
101
|
i += 2;
|
|
51
|
-
while (i < len &&
|
|
52
|
-
if (
|
|
102
|
+
while (i < len && input.charCodeAt(i) !== DQUOTE) {
|
|
103
|
+
if (input.charCodeAt(i) === BACKSLASH)
|
|
53
104
|
i++;
|
|
54
105
|
i++;
|
|
55
106
|
}
|
|
56
107
|
i++; // closing "
|
|
57
|
-
tokens.push({ kind: "temporal", value: slice(start, i), start, end: i });
|
|
108
|
+
tokens.push({ kind: "temporal", value: input.slice(start, i), start, end: i });
|
|
58
109
|
continue;
|
|
59
110
|
}
|
|
60
111
|
// String literal
|
|
61
|
-
if (
|
|
112
|
+
if (c === DQUOTE) {
|
|
62
113
|
i++;
|
|
63
|
-
while (i < len &&
|
|
64
|
-
if (
|
|
114
|
+
while (i < len && input.charCodeAt(i) !== DQUOTE) {
|
|
115
|
+
if (input.charCodeAt(i) === BACKSLASH)
|
|
65
116
|
i++;
|
|
66
117
|
i++;
|
|
67
118
|
}
|
|
68
119
|
i++; // closing "
|
|
69
|
-
tokens.push({ kind: "string", value: slice(start, i), start, end: i });
|
|
120
|
+
tokens.push({ kind: "string", value: input.slice(start, i), start, end: i });
|
|
70
121
|
continue;
|
|
71
122
|
}
|
|
72
123
|
// Backtick name
|
|
73
|
-
if (
|
|
124
|
+
if (c === BACKTICK) {
|
|
74
125
|
i++;
|
|
75
|
-
while (i < len &&
|
|
126
|
+
while (i < len && input.charCodeAt(i) !== BACKTICK)
|
|
76
127
|
i++;
|
|
77
128
|
i++; // closing `
|
|
78
129
|
tokens.push({
|
|
79
130
|
kind: "backtick",
|
|
80
|
-
value: slice(start + 1, i - 1),
|
|
131
|
+
value: input.slice(start + 1, i - 1),
|
|
81
132
|
start,
|
|
82
133
|
end: i,
|
|
83
134
|
});
|
|
84
135
|
continue;
|
|
85
136
|
}
|
|
86
137
|
// Whitespace
|
|
87
|
-
if (
|
|
88
|
-
while (i < len && (
|
|
138
|
+
if (isWhitespace(c)) {
|
|
139
|
+
while (i < len && isWhitespace(input.charCodeAt(i)))
|
|
89
140
|
i++;
|
|
90
|
-
tokens.push({ kind: "whitespace", value: slice(start, i), start, end: i });
|
|
141
|
+
tokens.push({ kind: "whitespace", value: input.slice(start, i), start, end: i });
|
|
91
142
|
continue;
|
|
92
143
|
}
|
|
93
144
|
// Two-char operators (check before single-char)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
tokens.push({ kind: "op", value: two, start, end: i + 2 });
|
|
145
|
+
if ((c === STAR && next === STAR) ||
|
|
146
|
+
(c === GT && next === EQ) ||
|
|
147
|
+
(c === LT && next === EQ) ||
|
|
148
|
+
(c === BANG && next === EQ) ||
|
|
149
|
+
(c === MINUS && next === GT) ||
|
|
150
|
+
(c === DOT && next === DOT)) {
|
|
151
|
+
tokens.push({ kind: "op", value: input.slice(i, i + 2), start, end: i + 2 });
|
|
102
152
|
i += 2;
|
|
103
153
|
continue;
|
|
104
154
|
}
|
|
105
155
|
// "==" is not standard FEEL but users familiar with JS/Java write it; treat as "=".
|
|
106
|
-
if (
|
|
156
|
+
if (c === EQ && next === EQ) {
|
|
107
157
|
tokens.push({ kind: "op", value: "=", start, end: i + 2 });
|
|
108
158
|
i += 2;
|
|
109
159
|
continue;
|
|
110
160
|
}
|
|
111
161
|
// Single-char operators
|
|
112
|
-
if (
|
|
113
|
-
tokens.push({ kind: "op", value:
|
|
162
|
+
if (SINGLE_OPS.has(c)) {
|
|
163
|
+
tokens.push({ kind: "op", value: input[i], start, end: i + 1 });
|
|
114
164
|
i++;
|
|
115
165
|
continue;
|
|
116
166
|
}
|
|
117
167
|
// Punctuation
|
|
118
|
-
if (
|
|
119
|
-
tokens.push({ kind: "punct", value:
|
|
168
|
+
if (PUNCT.has(c)) {
|
|
169
|
+
tokens.push({ kind: "punct", value: input[i], start, end: i + 1 });
|
|
120
170
|
i++;
|
|
121
171
|
continue;
|
|
122
172
|
}
|
|
123
|
-
// Dot (not ..)
|
|
124
|
-
if (
|
|
173
|
+
// Dot (not ".." and not the start of a number like ".872")
|
|
174
|
+
if (c === DOT && !isDigit(next)) {
|
|
125
175
|
tokens.push({ kind: "punct", value: ".", start, end: i + 1 });
|
|
126
176
|
i++;
|
|
127
177
|
continue;
|
|
128
178
|
}
|
|
129
|
-
// Number
|
|
130
|
-
|
|
131
|
-
|
|
179
|
+
// Number: digits, an optional fraction, an optional exponent. A leading
|
|
180
|
+
// "." is allowed (".872"), and ".." is never part of a number.
|
|
181
|
+
if (isDigit(c) || (c === DOT && isDigit(next))) {
|
|
182
|
+
while (i < len && isDigit(input.charCodeAt(i)))
|
|
132
183
|
i++;
|
|
133
184
|
// Consume decimal fraction only if next char is '.' followed by a digit (not '..')
|
|
134
|
-
if (i < len &&
|
|
185
|
+
if (i + 1 < len && input.charCodeAt(i) === DOT && isDigit(input.charCodeAt(i + 1))) {
|
|
135
186
|
i++; // consume the '.'
|
|
136
|
-
while (i < len &&
|
|
187
|
+
while (i < len && isDigit(input.charCodeAt(i)))
|
|
137
188
|
i++;
|
|
138
189
|
}
|
|
139
|
-
|
|
190
|
+
const exponent = readExponent(input, i);
|
|
191
|
+
if (exponent > i)
|
|
192
|
+
i = exponent;
|
|
193
|
+
tokens.push({ kind: "number", value: input.slice(start, i), start, end: i });
|
|
140
194
|
continue;
|
|
141
195
|
}
|
|
142
196
|
// Identifier / keyword
|
|
143
|
-
if ((
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
197
|
+
if (isLetter(c) || c === UNDERSCORE) {
|
|
198
|
+
i++;
|
|
199
|
+
while (i < len) {
|
|
200
|
+
const w = input.charCodeAt(i);
|
|
201
|
+
if (!isLetter(w) && !isDigit(w) && w !== UNDERSCORE)
|
|
202
|
+
break;
|
|
149
203
|
i++;
|
|
150
|
-
|
|
204
|
+
}
|
|
205
|
+
const word = input.slice(start, i);
|
|
151
206
|
const kind = KEYWORDS.has(word) ? "keyword" : "name";
|
|
152
207
|
tokens.push({ kind, value: word, start, end: i });
|
|
153
208
|
continue;
|
|
154
209
|
}
|
|
155
210
|
// Unknown character
|
|
156
|
-
tokens.push({ kind: "unknown", value:
|
|
211
|
+
tokens.push({ kind: "unknown", value: input[i], start, end: i + 1 });
|
|
157
212
|
i++;
|
|
158
213
|
}
|
|
159
214
|
return tokens;
|
|
160
215
|
}
|
|
216
|
+
const SIMPLE_ESCAPES = {
|
|
217
|
+
"'": "'",
|
|
218
|
+
'"': '"',
|
|
219
|
+
"\\": "\\",
|
|
220
|
+
n: "\n",
|
|
221
|
+
r: "\r",
|
|
222
|
+
t: "\t",
|
|
223
|
+
};
|
|
224
|
+
/** Decodes a \u/\U escape at `i`, or returns null when it is not a valid one. */
|
|
225
|
+
function readCodePoint(body, i) {
|
|
226
|
+
const kind = body[i + 1];
|
|
227
|
+
const maxDigits = kind === "u" ? 4 : 6;
|
|
228
|
+
const digits = /^[0-9a-fA-F]+/.exec(body.slice(i + 2, i + 2 + maxDigits))?.[0] ?? "";
|
|
229
|
+
// \u takes exactly four digits; \U takes as many as still form a code point,
|
|
230
|
+
// so "\U101EF0" is \U101EF followed by a literal "0".
|
|
231
|
+
const minLength = kind === "u" ? 4 : 1;
|
|
232
|
+
for (let len = digits.length; len >= minLength; len--) {
|
|
233
|
+
if (kind === "u" && len !== 4)
|
|
234
|
+
break;
|
|
235
|
+
const code = Number.parseInt(digits.slice(0, len), 16);
|
|
236
|
+
if (code <= 0x10ffff)
|
|
237
|
+
return { text: String.fromCodePoint(code), length: 2 + len };
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Decodes the escape sequences of a FEEL string literal body (the text between
|
|
243
|
+
* the quotes). Recognizes \' \" \\ \n \r \t, \uXXXX and the extended
|
|
244
|
+
* \UXXXXXX form; an unrecognized sequence is left as written, since dropping
|
|
245
|
+
* the backslash would silently alter the author's data.
|
|
246
|
+
*/
|
|
247
|
+
export function unescapeString(body) {
|
|
248
|
+
if (!body.includes("\\"))
|
|
249
|
+
return body;
|
|
250
|
+
let out = "";
|
|
251
|
+
let i = 0;
|
|
252
|
+
while (i < body.length) {
|
|
253
|
+
const c = body[i];
|
|
254
|
+
if (c !== "\\" || i + 1 >= body.length) {
|
|
255
|
+
out += c;
|
|
256
|
+
i++;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const simple = SIMPLE_ESCAPES[body[i + 1]];
|
|
260
|
+
if (simple !== undefined) {
|
|
261
|
+
out += simple;
|
|
262
|
+
i += 2;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const next = body[i + 1];
|
|
266
|
+
if (next === "u" || next === "U") {
|
|
267
|
+
const decoded = readCodePoint(body, i);
|
|
268
|
+
if (decoded) {
|
|
269
|
+
out += decoded.text;
|
|
270
|
+
i += decoded.length;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
out += c;
|
|
275
|
+
i++;
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
161
279
|
//# sourceMappingURL=lexer.js.map
|
package/dist/parser.d.ts
CHANGED
|
@@ -8,6 +8,15 @@ export interface ParseResult {
|
|
|
8
8
|
ast: FeelNode | null;
|
|
9
9
|
errors: ParseError[];
|
|
10
10
|
}
|
|
11
|
-
export
|
|
12
|
-
|
|
11
|
+
export interface ParseOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Names that are in scope where the expression is evaluated. FEEL names may
|
|
14
|
+
* contain spaces, so `a b + 1` can only be read as a reference to `a b`
|
|
15
|
+
* when the parser is told that `a b` exists. Without this, only multi-word
|
|
16
|
+
* built-in names are recognized.
|
|
17
|
+
*/
|
|
18
|
+
names?: Iterable<string>;
|
|
19
|
+
}
|
|
20
|
+
export declare function parseExpression(input: string, options?: ParseOptions): ParseResult;
|
|
21
|
+
export declare function parseUnaryTests(input: string, options?: ParseOptions): ParseResult;
|
|
13
22
|
//# sourceMappingURL=parser.d.ts.map
|