@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/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 (ch() === "/" && ch(1) === "/") {
83
+ if (c === SLASH && next === SLASH) {
33
84
  i += 2;
34
- while (i < len && ch() !== "\n")
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 (ch() === "/" && ch(1) === "*") {
91
+ if (c === SLASH && next === STAR) {
41
92
  i += 2;
42
- while (i < len && !(ch() === "*" && ch(1) === "/"))
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 (ch() === "@" && ch(1) === '"') {
100
+ if (c === AT && next === DQUOTE) {
50
101
  i += 2;
51
- while (i < len && ch() !== '"') {
52
- if (ch() === "\\")
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 (ch() === '"') {
112
+ if (c === DQUOTE) {
62
113
  i++;
63
- while (i < len && ch() !== '"') {
64
- if (ch() === "\\")
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 (ch() === "`") {
124
+ if (c === BACKTICK) {
74
125
  i++;
75
- while (i < len && ch() !== "`")
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 (ch() === " " || ch() === "\t" || ch() === "\n" || ch() === "\r") {
88
- while (i < len && (ch() === " " || ch() === "\t" || ch() === "\n" || ch() === "\r"))
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
- 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 });
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 (two === "==") {
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 ("+-*/=<>?".includes(ch())) {
113
- tokens.push({ kind: "op", value: ch(), start, end: i + 1 });
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 ("()[]{},:".includes(ch())) {
119
- tokens.push({ kind: "punct", value: ch(), start, end: i + 1 });
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 (ch() === ".") {
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 (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")
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 && ch() === "." && i + 1 < len && ch(1) >= "0" && ch(1) <= "9") {
185
+ if (i + 1 < len && input.charCodeAt(i) === DOT && isDigit(input.charCodeAt(i + 1))) {
135
186
  i++; // consume the '.'
136
- while (i < len && ch() >= "0" && ch() <= "9")
187
+ while (i < len && isDigit(input.charCodeAt(i)))
137
188
  i++;
138
189
  }
139
- tokens.push({ kind: "number", value: slice(start, i), start, end: i });
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 ((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() === "_"))
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
- const word = slice(start, i);
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: ch(), start, end: i + 1 });
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 declare function parseExpression(input: string): ParseResult;
12
- export declare function parseUnaryTests(input: string): ParseResult;
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