@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.
package/dist/lexer.js CHANGED
@@ -36,6 +36,7 @@ const DOT = 0x2e;
36
36
  const GT = 0x3e;
37
37
  const LT = 0x3c;
38
38
  const BANG = 0x21;
39
+ const PLUS = 0x2b;
39
40
  const MINUS = 0x2d;
40
41
  const EQ = 0x3d;
41
42
  const UNDERSCORE = 0x5f;
@@ -43,13 +44,32 @@ function isDigit(c) {
43
44
  return c >= 0x30 && c <= 0x39;
44
45
  }
45
46
  function isLetter(c) {
46
- return (c >= 0x61 && c <= 0x7a) || (c >= 0x41 && c <= 0x5a);
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;
47
52
  }
48
53
  function isWhitespace(c) {
49
54
  return c === SPACE || c === TAB || c === LF || c === CR;
50
55
  }
51
56
  const SINGLE_OPS = new Set("+-*/=<>?".split("").map((c) => c.charCodeAt(0)));
52
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
+ }
53
73
  export function tokenize(input) {
54
74
  const tokens = [];
55
75
  let i = 0;
@@ -150,14 +170,15 @@ export function tokenize(input) {
150
170
  i++;
151
171
  continue;
152
172
  }
153
- // Dot (not ..)
154
- if (c === DOT) {
173
+ // Dot (not ".." and not the start of a number like ".872")
174
+ if (c === DOT && !isDigit(next)) {
155
175
  tokens.push({ kind: "punct", value: ".", start, end: i + 1 });
156
176
  i++;
157
177
  continue;
158
178
  }
159
- // Number (only consume one decimal point, and only if followed by a digit)
160
- if (isDigit(c)) {
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))) {
161
182
  while (i < len && isDigit(input.charCodeAt(i)))
162
183
  i++;
163
184
  // Consume decimal fraction only if next char is '.' followed by a digit (not '..')
@@ -166,6 +187,9 @@ export function tokenize(input) {
166
187
  while (i < len && isDigit(input.charCodeAt(i)))
167
188
  i++;
168
189
  }
190
+ const exponent = readExponent(input, i);
191
+ if (exponent > i)
192
+ i = exponent;
169
193
  tokens.push({ kind: "number", value: input.slice(start, i), start, end: i });
170
194
  continue;
171
195
  }
@@ -189,4 +213,67 @@ export function tokenize(input) {
189
213
  }
190
214
  return tokens;
191
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
+ }
192
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