@bpmnkit/feel 0.0.19 → 0.0.21
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 +4 -1
- package/dist/builtins.js +55 -35
- package/dist/highlighter.js +22 -9
- package/dist/lexer.js +80 -49
- package/dist/parser.js +25 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://github.com/bpmnkit/monorepo)
|
|
10
10
|
[](https://github.com/bpmnkit/monorepo)
|
|
11
11
|
|
|
12
|
-
[Website](https://bpmnkit.com) · [Documentation](https://
|
|
12
|
+
[Website](https://bpmnkit.com) · [Documentation](https://bpmnkit.com/docs) · [GitHub](https://github.com/bpmnkit/monorepo) · [Changelog](https://github.com/bpmnkit/monorepo/blob/main/packages/feel/CHANGELOG.md)
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
15
|
---
|
|
@@ -106,15 +106,18 @@ interface ParseResult {
|
|
|
106
106
|
| [`@bpmnkit/plugins`](https://www.npmjs.com/package/@bpmnkit/plugins) | 22 composable canvas plugins |
|
|
107
107
|
| [`@bpmnkit/api`](https://www.npmjs.com/package/@bpmnkit/api) | Camunda 8 REST API TypeScript client |
|
|
108
108
|
| [`@bpmnkit/ascii`](https://www.npmjs.com/package/@bpmnkit/ascii) | Render BPMN diagrams as Unicode ASCII art |
|
|
109
|
+
| [`@bpmnkit/docspack`](https://www.npmjs.com/package/@bpmnkit/docspack) | BPMN Kit docs as an offline docspack package for AI agents |
|
|
109
110
|
| [`@bpmnkit/ui`](https://www.npmjs.com/package/@bpmnkit/ui) | Shared design tokens and UI components |
|
|
110
111
|
| [`@bpmnkit/profiles`](https://www.npmjs.com/package/@bpmnkit/profiles) | Shared auth, profile storage, and client factories for CLI & proxy |
|
|
111
112
|
| [`@bpmnkit/operate`](https://www.npmjs.com/package/@bpmnkit/operate) | Monitoring & operations frontend for Camunda clusters |
|
|
112
113
|
| [`@bpmnkit/connector-gen`](https://www.npmjs.com/package/@bpmnkit/connector-gen) | Generate connector templates from OpenAPI specs |
|
|
114
|
+
| [`@bpmnkit/connectors`](https://www.npmjs.com/package/@bpmnkit/connectors) | Camunda 8 OOTB connector catalog and deterministic template application |
|
|
113
115
|
| [`@bpmnkit/cli`](https://www.npmjs.com/package/@bpmnkit/cli) | Camunda 8 command-line interface (casen) |
|
|
114
116
|
| [`@bpmnkit/proxy`](https://www.npmjs.com/package/@bpmnkit/proxy) | Local AI bridge and Camunda API proxy server |
|
|
115
117
|
| [`@bpmnkit/patterns`](https://www.npmjs.com/package/@bpmnkit/patterns) | Domain process patterns for BPMNKit AIKit |
|
|
116
118
|
| [`@bpmnkit/reebe-wasm`](https://www.npmjs.com/package/@bpmnkit/reebe-wasm) | WebAssembly BPMN engine for browser simulation |
|
|
117
119
|
| [`@bpmnkit/worker-client`](https://www.npmjs.com/package/@bpmnkit/worker-client) | Thin Zeebe REST client for standalone workers |
|
|
120
|
+
| [`@bpmnkit/user-tasks`](https://www.npmjs.com/package/@bpmnkit/user-tasks) | Embeddable user task widget for Camunda 8 |
|
|
118
121
|
| [`@bpmnkit/cli-sdk`](https://www.npmjs.com/package/@bpmnkit/cli-sdk) | Plugin authoring SDK for the casen CLI |
|
|
119
122
|
| [`@bpmnkit/create-casen-plugin`](https://www.npmjs.com/package/@bpmnkit/create-casen-plugin) | Scaffold a new casen CLI plugin in seconds |
|
|
120
123
|
| [`@bpmnkit/casen-report`](https://www.npmjs.com/package/@bpmnkit/casen-report) | HTML reports from Camunda 8 incident and SLA data |
|
package/dist/builtins.js
CHANGED
|
@@ -319,18 +319,39 @@ reg("ends with", (str, match) => {
|
|
|
319
319
|
return null;
|
|
320
320
|
return s.endsWith(m);
|
|
321
321
|
});
|
|
322
|
+
// Patterns in FEEL come from static expression text, so the same few regexes
|
|
323
|
+
// are compiled over and over inside loops and decision tables; keep them.
|
|
324
|
+
const REGEX_CACHE_LIMIT = 256;
|
|
325
|
+
const regexCache = new Map();
|
|
326
|
+
/** Compiled regex for `pattern`/`flags`, or null when the pattern is invalid. */
|
|
327
|
+
function cachedRegExp(pattern, flags) {
|
|
328
|
+
const key = `${flags}/${pattern}`;
|
|
329
|
+
const hit = regexCache.get(key);
|
|
330
|
+
if (hit !== undefined)
|
|
331
|
+
return hit;
|
|
332
|
+
let re;
|
|
333
|
+
try {
|
|
334
|
+
re = new RegExp(pattern, flags);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
re = null;
|
|
338
|
+
}
|
|
339
|
+
if (regexCache.size >= REGEX_CACHE_LIMIT)
|
|
340
|
+
regexCache.clear();
|
|
341
|
+
regexCache.set(key, re);
|
|
342
|
+
return re;
|
|
343
|
+
}
|
|
322
344
|
reg("matches", (str, pattern, flags) => {
|
|
323
345
|
const s = toStr(str);
|
|
324
346
|
const p = toStr(pattern);
|
|
325
347
|
if (s === null || p === null)
|
|
326
348
|
return null;
|
|
327
349
|
const f = flags !== undefined && flags !== null ? (toStr(flags) ?? "") : "";
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
catch {
|
|
350
|
+
const re = cachedRegExp(p, f);
|
|
351
|
+
if (re === null)
|
|
332
352
|
return null;
|
|
333
|
-
|
|
353
|
+
re.lastIndex = 0;
|
|
354
|
+
return re.test(s);
|
|
334
355
|
});
|
|
335
356
|
reg("replace", (str, pattern, replacement, flags) => {
|
|
336
357
|
const s = toStr(str);
|
|
@@ -339,24 +360,19 @@ reg("replace", (str, pattern, replacement, flags) => {
|
|
|
339
360
|
if (s === null || p === null || r === null)
|
|
340
361
|
return null;
|
|
341
362
|
const f = flags !== undefined && flags !== null ? (toStr(flags) ?? "g") : "g";
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
catch {
|
|
363
|
+
const re = cachedRegExp(p, f.includes("g") ? f : `${f}g`);
|
|
364
|
+
if (re === null)
|
|
346
365
|
return null;
|
|
347
|
-
|
|
366
|
+
re.lastIndex = 0;
|
|
367
|
+
return s.replace(re, r);
|
|
348
368
|
});
|
|
349
369
|
reg("split", (str, delimiter) => {
|
|
350
370
|
const s = toStr(str);
|
|
351
371
|
const d = toStr(delimiter);
|
|
352
372
|
if (s === null || d === null)
|
|
353
373
|
return null;
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
catch {
|
|
358
|
-
return s.split(d);
|
|
359
|
-
}
|
|
374
|
+
const re = cachedRegExp(d, "");
|
|
375
|
+
return re === null ? s.split(d) : s.split(re);
|
|
360
376
|
});
|
|
361
377
|
reg("string join", (...args) => {
|
|
362
378
|
const flat = flattenToList(args);
|
|
@@ -713,45 +729,40 @@ reg("index of", (list, match) => {
|
|
|
713
729
|
}
|
|
714
730
|
return result;
|
|
715
731
|
});
|
|
732
|
+
// Set membership is SameValueZero, exactly what Array#includes used here, so
|
|
733
|
+
// de-duplication keeps its semantics while dropping from O(n²) to O(n).
|
|
716
734
|
reg("union", (...args) => {
|
|
717
|
-
const
|
|
735
|
+
const seen = new Set();
|
|
718
736
|
for (const v of args) {
|
|
719
737
|
if (isFeelList(v)) {
|
|
720
|
-
for (const item of v)
|
|
721
|
-
|
|
722
|
-
result.push(item);
|
|
723
|
-
}
|
|
738
|
+
for (const item of v)
|
|
739
|
+
seen.add(item);
|
|
724
740
|
}
|
|
725
|
-
else
|
|
726
|
-
|
|
741
|
+
else {
|
|
742
|
+
seen.add(v);
|
|
727
743
|
}
|
|
728
744
|
}
|
|
729
|
-
return
|
|
745
|
+
return [...seen];
|
|
730
746
|
});
|
|
731
747
|
reg("distinct values", (list) => {
|
|
732
748
|
if (!isFeelList(list))
|
|
733
749
|
return null;
|
|
734
|
-
|
|
735
|
-
for (const v of list) {
|
|
736
|
-
if (!result.includes(v))
|
|
737
|
-
result.push(v);
|
|
738
|
-
}
|
|
739
|
-
return result;
|
|
750
|
+
return [...new Set(list)];
|
|
740
751
|
});
|
|
741
752
|
reg("flatten", (list) => {
|
|
742
753
|
if (!isFeelList(list))
|
|
743
754
|
return null;
|
|
755
|
+
const result = [];
|
|
744
756
|
const flat = (arr) => {
|
|
745
|
-
const result = [];
|
|
746
757
|
for (const v of arr) {
|
|
747
758
|
if (isFeelList(v))
|
|
748
|
-
|
|
759
|
+
flat(v);
|
|
749
760
|
else
|
|
750
761
|
result.push(v);
|
|
751
762
|
}
|
|
752
|
-
return result;
|
|
753
763
|
};
|
|
754
|
-
|
|
764
|
+
flat(list);
|
|
765
|
+
return result;
|
|
755
766
|
});
|
|
756
767
|
reg("sort", (list, fn) => {
|
|
757
768
|
if (!isFeelList(list))
|
|
@@ -1178,12 +1189,21 @@ reg("coincides", (a, b) => {
|
|
|
1178
1189
|
// -------------------------------------------------------------------------
|
|
1179
1190
|
// Exports
|
|
1180
1191
|
// -------------------------------------------------------------------------
|
|
1192
|
+
// One FeelFunction wrapper per built-in, created on first use and shared: name
|
|
1193
|
+
// resolution runs for every identifier the evaluator meets, so allocating a
|
|
1194
|
+
// wrapper and closure per lookup showed up on every loop iteration.
|
|
1195
|
+
const builtinWrappers = new Map();
|
|
1181
1196
|
/** Look up a built-in function by name. Returns undefined if not found. */
|
|
1182
1197
|
export function getBuiltin(name) {
|
|
1198
|
+
const cached = builtinWrappers.get(name);
|
|
1199
|
+
if (cached)
|
|
1200
|
+
return cached;
|
|
1183
1201
|
const fn = builtinMap.get(name);
|
|
1184
1202
|
if (!fn)
|
|
1185
1203
|
return undefined;
|
|
1186
|
-
|
|
1204
|
+
const wrapper = { type: "function", call: (args) => fn(...args) };
|
|
1205
|
+
builtinWrappers.set(name, wrapper);
|
|
1206
|
+
return wrapper;
|
|
1187
1207
|
}
|
|
1188
1208
|
/** All built-in names. */
|
|
1189
1209
|
export function builtinNames() {
|
package/dist/highlighter.js
CHANGED
|
@@ -120,6 +120,16 @@ export function annotate(input) {
|
|
|
120
120
|
i = end;
|
|
121
121
|
continue;
|
|
122
122
|
}
|
|
123
|
+
// The greedy match above already decided whether this word is a
|
|
124
|
+
// built-in; re-running the lookahead in classifyToken would repeat it.
|
|
125
|
+
result.push({
|
|
126
|
+
kind: BUILTINS.has(name) ? "builtin" : "variable",
|
|
127
|
+
value: tok.value,
|
|
128
|
+
start: tok.start,
|
|
129
|
+
end: tok.end,
|
|
130
|
+
});
|
|
131
|
+
i++;
|
|
132
|
+
continue;
|
|
123
133
|
}
|
|
124
134
|
const { kind } = classifyToken(tok, tokens, i);
|
|
125
135
|
result.push({ kind, value: tok.value, start: tok.start, end: tok.end });
|
|
@@ -127,22 +137,25 @@ export function annotate(input) {
|
|
|
127
137
|
}
|
|
128
138
|
return result;
|
|
129
139
|
}
|
|
140
|
+
const HTML_ESCAPE_RE = /[&<>]/;
|
|
141
|
+
const HTML_ESCAPE_ALL_RE = /[&<>]/g;
|
|
142
|
+
const HTML_ESCAPES = { "&": "&", "<": "<", ">": ">" };
|
|
130
143
|
function escapeHtml(s) {
|
|
131
|
-
|
|
144
|
+
// Most tokens contain nothing to escape; one test beats three replace passes.
|
|
145
|
+
if (!HTML_ESCAPE_RE.test(s))
|
|
146
|
+
return s;
|
|
147
|
+
return s.replace(HTML_ESCAPE_ALL_RE, (ch) => HTML_ESCAPES[ch] ?? ch);
|
|
132
148
|
}
|
|
133
149
|
/** Render annotated tokens to HTML with span wrappers. */
|
|
134
150
|
export function highlightToHtml(input) {
|
|
135
151
|
if (!input.trim())
|
|
136
152
|
return escapeHtml(input) || '<span class="feel-empty">-</span>';
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
.map((t) => {
|
|
153
|
+
let html = "";
|
|
154
|
+
for (const t of annotate(input)) {
|
|
140
155
|
const escaped = escapeHtml(t.value);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
})
|
|
145
|
-
.join("");
|
|
156
|
+
html += t.kind === "plain" ? escaped : `<span class="feel-${t.kind}">${escaped}</span>`;
|
|
157
|
+
}
|
|
158
|
+
return html;
|
|
146
159
|
}
|
|
147
160
|
/** Backward-compatible alias. */
|
|
148
161
|
export const highlightFeel = highlightToHtml;
|
package/dist/lexer.js
CHANGED
|
@@ -20,140 +20,171 @@ 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 MINUS = 0x2d;
|
|
40
|
+
const EQ = 0x3d;
|
|
41
|
+
const UNDERSCORE = 0x5f;
|
|
42
|
+
function isDigit(c) {
|
|
43
|
+
return c >= 0x30 && c <= 0x39;
|
|
44
|
+
}
|
|
45
|
+
function isLetter(c) {
|
|
46
|
+
return (c >= 0x61 && c <= 0x7a) || (c >= 0x41 && c <= 0x5a);
|
|
47
|
+
}
|
|
48
|
+
function isWhitespace(c) {
|
|
49
|
+
return c === SPACE || c === TAB || c === LF || c === CR;
|
|
50
|
+
}
|
|
51
|
+
const SINGLE_OPS = new Set("+-*/=<>?".split("").map((c) => c.charCodeAt(0)));
|
|
52
|
+
const PUNCT = new Set("()[]{},:".split("").map((c) => c.charCodeAt(0)));
|
|
23
53
|
export function tokenize(input) {
|
|
24
54
|
const tokens = [];
|
|
25
55
|
let i = 0;
|
|
26
56
|
const len = input.length;
|
|
27
|
-
const ch = (offset = 0) => input.charAt(i + offset);
|
|
28
|
-
const slice = (start, end) => input.slice(start, end);
|
|
29
57
|
while (i < len) {
|
|
30
58
|
const start = i;
|
|
59
|
+
const c = input.charCodeAt(i);
|
|
60
|
+
// NaN past the end never equals any code, so lookahead needs no bounds check.
|
|
61
|
+
const next = input.charCodeAt(i + 1);
|
|
31
62
|
// Line comment
|
|
32
|
-
if (
|
|
63
|
+
if (c === SLASH && next === SLASH) {
|
|
33
64
|
i += 2;
|
|
34
|
-
while (i < len &&
|
|
65
|
+
while (i < len && input.charCodeAt(i) !== LF)
|
|
35
66
|
i++;
|
|
36
|
-
tokens.push({ kind: "comment", value: slice(start, i), start, end: i });
|
|
67
|
+
tokens.push({ kind: "comment", value: input.slice(start, i), start, end: i });
|
|
37
68
|
continue;
|
|
38
69
|
}
|
|
39
70
|
// Block comment
|
|
40
|
-
if (
|
|
71
|
+
if (c === SLASH && next === STAR) {
|
|
41
72
|
i += 2;
|
|
42
|
-
while (i < len && !(
|
|
73
|
+
while (i < len && !(input.charCodeAt(i) === STAR && input.charCodeAt(i + 1) === SLASH))
|
|
43
74
|
i++;
|
|
44
75
|
i += 2;
|
|
45
|
-
tokens.push({ kind: "comment", value: slice(start, i), start, end: i });
|
|
76
|
+
tokens.push({ kind: "comment", value: input.slice(start, i), start, end: i });
|
|
46
77
|
continue;
|
|
47
78
|
}
|
|
48
79
|
// Temporal literal @"..."
|
|
49
|
-
if (
|
|
80
|
+
if (c === AT && next === DQUOTE) {
|
|
50
81
|
i += 2;
|
|
51
|
-
while (i < len &&
|
|
52
|
-
if (
|
|
82
|
+
while (i < len && input.charCodeAt(i) !== DQUOTE) {
|
|
83
|
+
if (input.charCodeAt(i) === BACKSLASH)
|
|
53
84
|
i++;
|
|
54
85
|
i++;
|
|
55
86
|
}
|
|
56
87
|
i++; // closing "
|
|
57
|
-
tokens.push({ kind: "temporal", value: slice(start, i), start, end: i });
|
|
88
|
+
tokens.push({ kind: "temporal", value: input.slice(start, i), start, end: i });
|
|
58
89
|
continue;
|
|
59
90
|
}
|
|
60
91
|
// String literal
|
|
61
|
-
if (
|
|
92
|
+
if (c === DQUOTE) {
|
|
62
93
|
i++;
|
|
63
|
-
while (i < len &&
|
|
64
|
-
if (
|
|
94
|
+
while (i < len && input.charCodeAt(i) !== DQUOTE) {
|
|
95
|
+
if (input.charCodeAt(i) === BACKSLASH)
|
|
65
96
|
i++;
|
|
66
97
|
i++;
|
|
67
98
|
}
|
|
68
99
|
i++; // closing "
|
|
69
|
-
tokens.push({ kind: "string", value: slice(start, i), start, end: i });
|
|
100
|
+
tokens.push({ kind: "string", value: input.slice(start, i), start, end: i });
|
|
70
101
|
continue;
|
|
71
102
|
}
|
|
72
103
|
// Backtick name
|
|
73
|
-
if (
|
|
104
|
+
if (c === BACKTICK) {
|
|
74
105
|
i++;
|
|
75
|
-
while (i < len &&
|
|
106
|
+
while (i < len && input.charCodeAt(i) !== BACKTICK)
|
|
76
107
|
i++;
|
|
77
108
|
i++; // closing `
|
|
78
109
|
tokens.push({
|
|
79
110
|
kind: "backtick",
|
|
80
|
-
value: slice(start + 1, i - 1),
|
|
111
|
+
value: input.slice(start + 1, i - 1),
|
|
81
112
|
start,
|
|
82
113
|
end: i,
|
|
83
114
|
});
|
|
84
115
|
continue;
|
|
85
116
|
}
|
|
86
117
|
// Whitespace
|
|
87
|
-
if (
|
|
88
|
-
while (i < len && (
|
|
118
|
+
if (isWhitespace(c)) {
|
|
119
|
+
while (i < len && isWhitespace(input.charCodeAt(i)))
|
|
89
120
|
i++;
|
|
90
|
-
tokens.push({ kind: "whitespace", value: slice(start, i), start, end: i });
|
|
121
|
+
tokens.push({ kind: "whitespace", value: input.slice(start, i), start, end: i });
|
|
91
122
|
continue;
|
|
92
123
|
}
|
|
93
124
|
// 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 });
|
|
125
|
+
if ((c === STAR && next === STAR) ||
|
|
126
|
+
(c === GT && next === EQ) ||
|
|
127
|
+
(c === LT && next === EQ) ||
|
|
128
|
+
(c === BANG && next === EQ) ||
|
|
129
|
+
(c === MINUS && next === GT) ||
|
|
130
|
+
(c === DOT && next === DOT)) {
|
|
131
|
+
tokens.push({ kind: "op", value: input.slice(i, i + 2), start, end: i + 2 });
|
|
102
132
|
i += 2;
|
|
103
133
|
continue;
|
|
104
134
|
}
|
|
105
135
|
// "==" is not standard FEEL but users familiar with JS/Java write it; treat as "=".
|
|
106
|
-
if (
|
|
136
|
+
if (c === EQ && next === EQ) {
|
|
107
137
|
tokens.push({ kind: "op", value: "=", start, end: i + 2 });
|
|
108
138
|
i += 2;
|
|
109
139
|
continue;
|
|
110
140
|
}
|
|
111
141
|
// Single-char operators
|
|
112
|
-
if (
|
|
113
|
-
tokens.push({ kind: "op", value:
|
|
142
|
+
if (SINGLE_OPS.has(c)) {
|
|
143
|
+
tokens.push({ kind: "op", value: input[i], start, end: i + 1 });
|
|
114
144
|
i++;
|
|
115
145
|
continue;
|
|
116
146
|
}
|
|
117
147
|
// Punctuation
|
|
118
|
-
if (
|
|
119
|
-
tokens.push({ kind: "punct", value:
|
|
148
|
+
if (PUNCT.has(c)) {
|
|
149
|
+
tokens.push({ kind: "punct", value: input[i], start, end: i + 1 });
|
|
120
150
|
i++;
|
|
121
151
|
continue;
|
|
122
152
|
}
|
|
123
153
|
// Dot (not ..)
|
|
124
|
-
if (
|
|
154
|
+
if (c === DOT) {
|
|
125
155
|
tokens.push({ kind: "punct", value: ".", start, end: i + 1 });
|
|
126
156
|
i++;
|
|
127
157
|
continue;
|
|
128
158
|
}
|
|
129
159
|
// Number (only consume one decimal point, and only if followed by a digit)
|
|
130
|
-
if (
|
|
131
|
-
while (i < len &&
|
|
160
|
+
if (isDigit(c)) {
|
|
161
|
+
while (i < len && isDigit(input.charCodeAt(i)))
|
|
132
162
|
i++;
|
|
133
163
|
// Consume decimal fraction only if next char is '.' followed by a digit (not '..')
|
|
134
|
-
if (i < len &&
|
|
164
|
+
if (i + 1 < len && input.charCodeAt(i) === DOT && isDigit(input.charCodeAt(i + 1))) {
|
|
135
165
|
i++; // consume the '.'
|
|
136
|
-
while (i < len &&
|
|
166
|
+
while (i < len && isDigit(input.charCodeAt(i)))
|
|
137
167
|
i++;
|
|
138
168
|
}
|
|
139
|
-
tokens.push({ kind: "number", value: slice(start, i), start, end: i });
|
|
169
|
+
tokens.push({ kind: "number", value: input.slice(start, i), start, end: i });
|
|
140
170
|
continue;
|
|
141
171
|
}
|
|
142
172
|
// Identifier / keyword
|
|
143
|
-
if ((
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
173
|
+
if (isLetter(c) || c === UNDERSCORE) {
|
|
174
|
+
i++;
|
|
175
|
+
while (i < len) {
|
|
176
|
+
const w = input.charCodeAt(i);
|
|
177
|
+
if (!isLetter(w) && !isDigit(w) && w !== UNDERSCORE)
|
|
178
|
+
break;
|
|
149
179
|
i++;
|
|
150
|
-
|
|
180
|
+
}
|
|
181
|
+
const word = input.slice(start, i);
|
|
151
182
|
const kind = KEYWORDS.has(word) ? "keyword" : "name";
|
|
152
183
|
tokens.push({ kind, value: word, start, end: i });
|
|
153
184
|
continue;
|
|
154
185
|
}
|
|
155
186
|
// Unknown character
|
|
156
|
-
tokens.push({ kind: "unknown", value:
|
|
187
|
+
tokens.push({ kind: "unknown", value: input[i], start, end: i + 1 });
|
|
157
188
|
i++;
|
|
158
189
|
}
|
|
159
190
|
return tokens;
|
package/dist/parser.js
CHANGED
|
@@ -854,18 +854,40 @@ class Parser {
|
|
|
854
854
|
}
|
|
855
855
|
}
|
|
856
856
|
}
|
|
857
|
+
// Expression text is static model content that engines and decision tables
|
|
858
|
+
// evaluate over and over, so parse results are memoized. Results are treated
|
|
859
|
+
// as immutable by every consumer; the caches are bounded and simply reset when
|
|
860
|
+
// full.
|
|
861
|
+
const PARSE_CACHE_LIMIT = 2048;
|
|
862
|
+
const expressionCache = new Map();
|
|
863
|
+
const unaryTestsCache = new Map();
|
|
864
|
+
function remember(cache, input, result) {
|
|
865
|
+
if (cache.size >= PARSE_CACHE_LIMIT)
|
|
866
|
+
cache.clear();
|
|
867
|
+
cache.set(input, result);
|
|
868
|
+
return result;
|
|
869
|
+
}
|
|
857
870
|
export function parseExpression(input) {
|
|
871
|
+
const cached = expressionCache.get(input);
|
|
872
|
+
if (cached !== undefined)
|
|
873
|
+
return cached;
|
|
858
874
|
const p = new Parser(input);
|
|
859
875
|
const ast = p.parseExpression(0);
|
|
860
876
|
p.checkDone();
|
|
861
|
-
return { ast, errors: p.errors };
|
|
877
|
+
return remember(expressionCache, input, { ast, errors: p.errors });
|
|
862
878
|
}
|
|
863
879
|
export function parseUnaryTests(input) {
|
|
880
|
+
const cached = unaryTestsCache.get(input);
|
|
881
|
+
if (cached !== undefined)
|
|
882
|
+
return cached;
|
|
864
883
|
if (input.trim() === "-") {
|
|
865
|
-
return
|
|
884
|
+
return remember(unaryTestsCache, input, {
|
|
885
|
+
ast: { kind: "any-input", start: 0, end: input.length },
|
|
886
|
+
errors: [],
|
|
887
|
+
});
|
|
866
888
|
}
|
|
867
889
|
const p = new Parser(input);
|
|
868
890
|
const ast = p.parseUnaryTests();
|
|
869
|
-
return { ast, errors: p.errors };
|
|
891
|
+
return remember(unaryTestsCache, input, { ast, errors: p.errors });
|
|
870
892
|
}
|
|
871
893
|
//# sourceMappingURL=parser.js.map
|