@jbrowse/jexl 3.3.0 → 4.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/Expression.js +1 -2
- package/dist/Expression.js.map +1 -1
- package/dist/Jexl.d.ts +2 -2
- package/dist/Lexer.d.ts +10 -8
- package/dist/Lexer.js +46 -35
- package/dist/Lexer.js.map +1 -1
- package/dist/evaluator/compile.d.ts +6 -4
- package/dist/evaluator/compile.js +42 -52
- package/dist/evaluator/compile.js.map +1 -1
- package/dist/grammar.js +1 -1
- package/dist/parser/Parser.d.ts +2 -9
- package/dist/parser/Parser.js +2 -12
- package/dist/parser/Parser.js.map +1 -1
- package/dist/parser/handlers.d.ts +6 -3
- package/dist/parser/handlers.js +45 -28
- package/dist/parser/handlers.js.map +1 -1
- package/dist/parser/states.js +3 -5
- package/dist/parser/states.js.map +1 -1
- package/dist/types.d.ts +0 -3
- package/esm/Expression.js +1 -2
- package/esm/Expression.js.map +1 -1
- package/esm/Jexl.d.ts +2 -2
- package/esm/Lexer.d.ts +10 -8
- package/esm/Lexer.js +46 -35
- package/esm/Lexer.js.map +1 -1
- package/esm/evaluator/compile.d.ts +6 -4
- package/esm/evaluator/compile.js +42 -52
- package/esm/evaluator/compile.js.map +1 -1
- package/esm/grammar.js +1 -1
- package/esm/parser/Parser.d.ts +2 -9
- package/esm/parser/Parser.js +2 -12
- package/esm/parser/Parser.js.map +1 -1
- package/esm/parser/handlers.d.ts +6 -3
- package/esm/parser/handlers.js +45 -28
- package/esm/parser/handlers.js.map +1 -1
- package/esm/parser/states.js +3 -5
- package/esm/parser/states.js.map +1 -1
- package/esm/types.d.ts +0 -3
- package/package.json +2 -1
- package/src/Expression.ts +1 -2
- package/src/Jexl.ts +2 -2
- package/src/Lexer.ts +47 -36
- package/src/evaluator/compile.ts +53 -68
- package/src/grammar.ts +1 -1
- package/src/parser/Parser.ts +2 -13
- package/src/parser/handlers.ts +48 -31
- package/src/parser/states.ts +3 -5
- package/src/types.ts +0 -3
- package/dist/evaluator/Evaluator.d.ts +0 -37
- package/dist/evaluator/Evaluator.js +0 -49
- package/dist/evaluator/Evaluator.js.map +0 -1
- package/esm/evaluator/Evaluator.d.ts +0 -37
- package/esm/evaluator/Evaluator.js +0 -47
- package/esm/evaluator/Evaluator.js.map +0 -1
- package/src/evaluator/Evaluator.ts +0 -58
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jbrowse/jexl",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "A fork of the jexl lang for jbrowse",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "vitest",
|
|
18
|
+
"bench": "vitest bench --run",
|
|
18
19
|
"coverage": "pnpm test --coverage",
|
|
19
20
|
"clean": "rimraf dist esm",
|
|
20
21
|
"format": "prettier --write .",
|
package/src/Expression.ts
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import Lexer from './Lexer.ts'
|
|
7
|
-
import Evaluator from './evaluator/Evaluator.ts'
|
|
8
7
|
import { compileAst } from './evaluator/compile.ts'
|
|
9
8
|
import Parser from './parser/Parser.ts'
|
|
10
9
|
|
|
@@ -71,7 +70,7 @@ class Expression {
|
|
|
71
70
|
if (!this._fn) {
|
|
72
71
|
return undefined
|
|
73
72
|
}
|
|
74
|
-
return this._fn(
|
|
73
|
+
return this._fn(context)
|
|
75
74
|
}
|
|
76
75
|
}
|
|
77
76
|
|
package/src/Jexl.ts
CHANGED
|
@@ -40,9 +40,9 @@ class Jexl {
|
|
|
40
40
|
* Adds a binary operator to Jexl at the specified precedence. The higher the
|
|
41
41
|
* precedence, the earlier the operator is applied in the order of operations.
|
|
42
42
|
* For example, * has a higher precedence than +, because multiplication comes
|
|
43
|
-
* before
|
|
43
|
+
* before addition.
|
|
44
44
|
*
|
|
45
|
-
* Please see grammar.
|
|
45
|
+
* Please see grammar.ts for a listing of all default operators and their
|
|
46
46
|
* precedence values in order to choose the appropriate precedence for the
|
|
47
47
|
* new operator.
|
|
48
48
|
* @param {string} operator The operator string to be added
|
package/src/Lexer.ts
CHANGED
|
@@ -6,13 +6,26 @@
|
|
|
6
6
|
import type { Grammar } from './grammar.ts'
|
|
7
7
|
import type { TemplatePart, Token } from './types.ts'
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
// what an identifier and a number look like, written once: the split regex
|
|
10
|
+
// below carves elements out of the expression with these, and _createToken
|
|
11
|
+
// then re-tests the elements it produced. Two spellings of either would
|
|
12
|
+
// disagree on some input, and each disagreement is an "Invalid expression
|
|
13
|
+
// token" for something the splitter was happy to produce.
|
|
14
|
+
const identChars = String.raw`a-zA-Zа-яА-Я_\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF$`
|
|
15
|
+
const identPattern = `[${identChars}][${identChars}0-9]*`
|
|
16
|
+
// unsigned: whether a leading '-' negates is decided separately, in getTokens
|
|
17
|
+
const numberPattern = String.raw`(?:(?:[0-9]*\.[0-9]+)|[0-9]+)`
|
|
18
|
+
|
|
19
|
+
const numericRegex = new RegExp(`^-?${numberPattern}$`)
|
|
20
|
+
const identRegex = new RegExp(`^${identPattern}$`)
|
|
12
21
|
const escEscRegex = /\\\\/g
|
|
13
22
|
// a string literal opens with one of exactly two quote characters, so the two
|
|
14
23
|
// unescaping regexes can just be named rather than built and cached per quote
|
|
15
24
|
const escQuoteRegex = { "'": /\\'/g, '"': /\\"/g }
|
|
25
|
+
// the escapes a template string's static text recognizes. One pass, so the
|
|
26
|
+
// backslash an escaped backslash yields can't be re-read as the start of the
|
|
27
|
+
// escape that follows it
|
|
28
|
+
const templateEscRegex = /\\([`$\\])/g
|
|
16
29
|
const whitespaceRegex = /^\s*$/
|
|
17
30
|
const preOpRegexElems = [
|
|
18
31
|
// Template strings
|
|
@@ -26,12 +39,7 @@ const preOpRegexElems = [
|
|
|
26
39
|
String.raw`\btrue\b`,
|
|
27
40
|
String.raw`\bfalse\b`
|
|
28
41
|
]
|
|
29
|
-
const postOpRegexElems = [
|
|
30
|
-
// Identifiers
|
|
31
|
-
'[a-zA-Zа-яА-Я_\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\\$][a-zA-Z0-9а-яА-Я_\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\\$]*',
|
|
32
|
-
// Numerics (without negative symbol)
|
|
33
|
-
String.raw`(?:(?:[0-9]*\.[0-9]+)|[0-9]+)`
|
|
34
|
-
]
|
|
42
|
+
const postOpRegexElems = [identPattern, numberPattern]
|
|
35
43
|
const unaryMinusToken = (): Token => ({
|
|
36
44
|
type: 'unaryOp',
|
|
37
45
|
value: '-',
|
|
@@ -49,14 +57,16 @@ const minusNegatesAfter = new Set([
|
|
|
49
57
|
])
|
|
50
58
|
|
|
51
59
|
/**
|
|
52
|
-
* Lexer
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
+
* Lexer handles the lexical parsing of a Jexl string. Its responsibility is to
|
|
61
|
+
* identify the "parts of speech" of a Jexl expression, and tokenize and label
|
|
62
|
+
* each, but to do only the most minimal syntax checking; the only errors the
|
|
63
|
+
* Lexer should be concerned with are if it's unable to identify the utility of
|
|
64
|
+
* any of its tokens. Errors stemming from these tokens not being in a sensible
|
|
65
|
+
* configuration should be left for the Parser to handle.
|
|
66
|
+
*
|
|
67
|
+
* An instance is bound to one grammar and memoizes the regex that splits an
|
|
68
|
+
* expression into elements, which is expensive to build; {@link #_clearCache}
|
|
69
|
+
* discards it when the grammar's elements change.
|
|
60
70
|
*/
|
|
61
71
|
class Lexer {
|
|
62
72
|
_grammar: Grammar
|
|
@@ -98,37 +108,43 @@ class Lexer {
|
|
|
98
108
|
*/
|
|
99
109
|
getTokens(elements: string[]) {
|
|
100
110
|
const tokens: Token[] = []
|
|
101
|
-
|
|
111
|
+
// a prefix minus, held back until the element it applies to is known. It
|
|
112
|
+
// carries its own raw so that whitespace arriving before that element ("-
|
|
113
|
+
// 1") accumulates on the minus rather than on the token before it, which
|
|
114
|
+
// is what lets the parser's error messages quote the expression verbatim.
|
|
115
|
+
let pendingMinus: Token | undefined
|
|
102
116
|
for (const element of elements) {
|
|
103
117
|
if (this._isWhitespace(element)) {
|
|
104
|
-
const last = tokens.at(-1)
|
|
118
|
+
const last = pendingMinus ?? tokens.at(-1)
|
|
105
119
|
if (last) {
|
|
106
120
|
last.raw += element
|
|
107
121
|
}
|
|
108
122
|
} else if (element === '-' && this._isNegative(tokens)) {
|
|
109
123
|
// a second prefix minus in a row ("- -x"): emit the pending one as a
|
|
110
124
|
// unary operator so this one can negate whatever comes next
|
|
111
|
-
if (
|
|
112
|
-
tokens.push(
|
|
125
|
+
if (pendingMinus) {
|
|
126
|
+
tokens.push(pendingMinus)
|
|
113
127
|
}
|
|
114
|
-
|
|
115
|
-
} else if (
|
|
116
|
-
negate = false
|
|
128
|
+
pendingMinus = unaryMinusToken()
|
|
129
|
+
} else if (pendingMinus) {
|
|
117
130
|
if (numericRegex.exec(element)) {
|
|
118
131
|
// fold the sign into the number, so "-1" stays a single literal
|
|
119
|
-
|
|
132
|
+
const token = this._createToken('-' + element)
|
|
133
|
+
token.raw = pendingMinus.raw + element
|
|
134
|
+
tokens.push(token)
|
|
120
135
|
} else {
|
|
121
136
|
// anything else gets a standalone prefix operator, letting "-x",
|
|
122
137
|
// "-(a + b)" and "-foo.bar" negate a computed value
|
|
123
|
-
tokens.push(
|
|
138
|
+
tokens.push(pendingMinus, this._createToken(element))
|
|
124
139
|
}
|
|
140
|
+
pendingMinus = undefined
|
|
125
141
|
} else {
|
|
126
142
|
tokens.push(this._createToken(element))
|
|
127
143
|
}
|
|
128
144
|
}
|
|
129
145
|
// Catch a - at the end of the string. Let the parser handle that issue.
|
|
130
|
-
if (
|
|
131
|
-
tokens.push(
|
|
146
|
+
if (pendingMinus) {
|
|
147
|
+
tokens.push(pendingMinus)
|
|
132
148
|
}
|
|
133
149
|
return tokens
|
|
134
150
|
}
|
|
@@ -256,10 +272,8 @@ class Lexer {
|
|
|
256
272
|
* @private
|
|
257
273
|
*/
|
|
258
274
|
_isNegative(tokens: Token[]) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
return minusNegatesAfter.has(tokens[tokens.length - 1]!.type)
|
|
275
|
+
const last = tokens.at(-1)
|
|
276
|
+
return !last || minusNegatesAfter.has(last.type)
|
|
263
277
|
}
|
|
264
278
|
|
|
265
279
|
/**
|
|
@@ -366,10 +380,7 @@ class Lexer {
|
|
|
366
380
|
}
|
|
367
381
|
|
|
368
382
|
_unescapeTemplateString(str: string) {
|
|
369
|
-
return str
|
|
370
|
-
.replaceAll('\\`', '`')
|
|
371
|
-
.replaceAll(String.raw`\$`, '$')
|
|
372
|
-
.replaceAll('\\\\', '\\')
|
|
383
|
+
return str.replaceAll(templateEscRegex, '$1')
|
|
373
384
|
}
|
|
374
385
|
}
|
|
375
386
|
|
package/src/evaluator/compile.ts
CHANGED
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
|
|
6
6
|
import type { Grammar } from '../grammar.ts'
|
|
7
7
|
import type { AstNode, AstNodeUnion, JexlValue } from '../types.ts'
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
/** The variables an expression is evaluated against. */
|
|
10
|
+
export type Context = Record<string, JexlValue>
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
|
-
* An AST node lowered to a closure. Calling it with
|
|
12
|
-
*
|
|
13
|
+
* An AST node lowered to a closure. Calling it with a context produces the
|
|
14
|
+
* node's value. The grammar is captured when the closure is built, so it is not
|
|
15
|
+
* a parameter.
|
|
13
16
|
*/
|
|
14
|
-
export type CompiledNode = (
|
|
17
|
+
export type CompiledNode = (context: Context) => JexlValue
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Writes a key that a plain assignment would mishandle. Storing to
|
|
@@ -20,11 +23,7 @@ export type CompiledNode = (ev: Evaluator) => JexlValue
|
|
|
20
23
|
* re-pointed the target's prototype). Defining the property instead matches
|
|
21
24
|
* what `JSON.parse('{"__proto__":1}')` produces: an ordinary own property.
|
|
22
25
|
*/
|
|
23
|
-
function defineOwn(
|
|
24
|
-
target: Record<string, JexlValue>,
|
|
25
|
-
key: string,
|
|
26
|
-
value: JexlValue
|
|
27
|
-
) {
|
|
26
|
+
function defineOwn(target: Context, key: string, value: JexlValue) {
|
|
28
27
|
Object.defineProperty(target, key, {
|
|
29
28
|
value,
|
|
30
29
|
writable: true,
|
|
@@ -33,11 +32,7 @@ function defineOwn(
|
|
|
33
32
|
})
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
function assignOwn(
|
|
37
|
-
target: Record<string, JexlValue>,
|
|
38
|
-
key: string,
|
|
39
|
-
value: JexlValue
|
|
40
|
-
) {
|
|
35
|
+
function assignOwn(target: Context, key: string, value: JexlValue) {
|
|
41
36
|
target[key] = value
|
|
42
37
|
}
|
|
43
38
|
|
|
@@ -88,21 +83,17 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
88
83
|
case 'Identifier': {
|
|
89
84
|
const name = node.value
|
|
90
85
|
if (!node.from) {
|
|
91
|
-
return
|
|
92
|
-
? (ev) => ev._relContext[name]
|
|
93
|
-
: (ev) => ev._context[name]
|
|
86
|
+
return (ctx) => ctx[name]
|
|
94
87
|
}
|
|
95
88
|
const from = compileAst(node.from, grammar)
|
|
96
|
-
return (
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
89
|
+
return (ctx) => {
|
|
90
|
+
const subject = from(ctx)
|
|
91
|
+
if (subject == null) {
|
|
99
92
|
return undefined
|
|
100
93
|
}
|
|
101
94
|
// an identifier chained off an array reads through its first element
|
|
102
|
-
const
|
|
103
|
-
return
|
|
104
|
-
? undefined
|
|
105
|
-
: (ctx as Record<string, JexlValue>)[name]
|
|
95
|
+
const target = Array.isArray(subject) ? subject[0] : subject
|
|
96
|
+
return target == null ? undefined : (target as Context)[name]
|
|
106
97
|
}
|
|
107
98
|
}
|
|
108
99
|
|
|
@@ -119,18 +110,18 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
119
110
|
if (evalOnDemand) {
|
|
120
111
|
// operands stay unevaluated behind an `eval` thunk, so operators such
|
|
121
112
|
// as && and || can short-circuit
|
|
122
|
-
return (
|
|
123
|
-
evalOnDemand({ eval: () => left(
|
|
113
|
+
return (ctx) =>
|
|
114
|
+
evalOnDemand({ eval: () => left(ctx) }, { eval: () => right(ctx) })
|
|
124
115
|
}
|
|
125
116
|
const fn = op.eval
|
|
126
117
|
if (!fn) {
|
|
127
|
-
return (
|
|
128
|
-
left(
|
|
129
|
-
right(
|
|
118
|
+
return (ctx) => {
|
|
119
|
+
left(ctx)
|
|
120
|
+
right(ctx)
|
|
130
121
|
return undefined
|
|
131
122
|
}
|
|
132
123
|
}
|
|
133
|
-
return (
|
|
124
|
+
return (ctx) => fn(left(ctx), right(ctx))
|
|
134
125
|
}
|
|
135
126
|
|
|
136
127
|
case 'UnaryExpression': {
|
|
@@ -144,12 +135,12 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
144
135
|
: undefined
|
|
145
136
|
if (!fn) {
|
|
146
137
|
// the operand is still evaluated for an unknown operator, as before
|
|
147
|
-
return (
|
|
148
|
-
right(
|
|
138
|
+
return (ctx) => {
|
|
139
|
+
right(ctx)
|
|
149
140
|
return undefined
|
|
150
141
|
}
|
|
151
142
|
}
|
|
152
|
-
return (
|
|
143
|
+
return (ctx) => fn(right(ctx))
|
|
153
144
|
}
|
|
154
145
|
|
|
155
146
|
case 'ConditionalExpression': {
|
|
@@ -160,28 +151,22 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
160
151
|
const alternate = node.alternate
|
|
161
152
|
? compileAst(node.alternate, grammar)
|
|
162
153
|
: undefined
|
|
163
|
-
return (
|
|
164
|
-
const res = test(
|
|
154
|
+
return (ctx) => {
|
|
155
|
+
const res = test(ctx)
|
|
165
156
|
if (res) {
|
|
166
157
|
// an omitted consequent ("a ?: b") yields the test result
|
|
167
|
-
return consequent ? consequent(
|
|
158
|
+
return consequent ? consequent(ctx) : res
|
|
168
159
|
}
|
|
169
|
-
return alternate ? alternate(
|
|
160
|
+
return alternate ? alternate(ctx) : undefined
|
|
170
161
|
}
|
|
171
162
|
}
|
|
172
163
|
|
|
173
164
|
case 'FilterExpression': {
|
|
174
|
-
if (node.relative) {
|
|
175
|
-
// thrown on evaluation rather than compilation, as before
|
|
176
|
-
return () => {
|
|
177
|
-
throw new Error('Relative filter expressions are not supported')
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
165
|
const subject = compileAst(node.subject, grammar)
|
|
181
166
|
const index = compileAst(node.expr, grammar)
|
|
182
|
-
return (
|
|
183
|
-
const subjectVal = subject(
|
|
184
|
-
const indexVal = index(
|
|
167
|
+
return (ctx) => {
|
|
168
|
+
const subjectVal = subject(ctx)
|
|
169
|
+
const indexVal = index(ctx)
|
|
185
170
|
if (subjectVal == null) {
|
|
186
171
|
return undefined
|
|
187
172
|
}
|
|
@@ -194,10 +179,10 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
194
179
|
case 'ArrayLiteral': {
|
|
195
180
|
const items = node.value.map((item) => compileAst(item, grammar))
|
|
196
181
|
const len = items.length
|
|
197
|
-
return (
|
|
182
|
+
return (ctx) => {
|
|
198
183
|
const out: JexlValue[] = new Array(len)
|
|
199
184
|
for (let i = 0; i < len; i++) {
|
|
200
|
-
out[i] = items[i]!(
|
|
185
|
+
out[i] = items[i]!(ctx)
|
|
201
186
|
}
|
|
202
187
|
return out
|
|
203
188
|
}
|
|
@@ -210,10 +195,10 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
210
195
|
const len = keys.length
|
|
211
196
|
// resolved once, here, so the common case keeps its plain store
|
|
212
197
|
const store = keys.includes('__proto__') ? defineOwn : assignOwn
|
|
213
|
-
return (
|
|
198
|
+
return (ctx) => {
|
|
214
199
|
const out: Record<string, JexlValue> = {}
|
|
215
200
|
for (let i = 0; i < len; i++) {
|
|
216
|
-
store(out, keys[i]!, values[i]!(
|
|
201
|
+
store(out, keys[i]!, values[i]!(ctx))
|
|
217
202
|
}
|
|
218
203
|
return out
|
|
219
204
|
}
|
|
@@ -221,18 +206,18 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
221
206
|
|
|
222
207
|
case 'TemplateLiteral': {
|
|
223
208
|
// every part renders to a string, so these are narrower than CompiledNode
|
|
224
|
-
const parts = node.parts.map((part): ((
|
|
209
|
+
const parts = node.parts.map((part): ((ctx: Context) => string) => {
|
|
225
210
|
if (part.type === 'static') {
|
|
226
211
|
const { value } = part
|
|
227
212
|
return () => value
|
|
228
213
|
}
|
|
229
214
|
const expr = compileAst(part.value, grammar)
|
|
230
|
-
return (
|
|
215
|
+
return (ctx) => stringify(expr(ctx))
|
|
231
216
|
})
|
|
232
|
-
return (
|
|
217
|
+
return (ctx) => {
|
|
233
218
|
let out = ''
|
|
234
219
|
for (const part of parts) {
|
|
235
|
-
out += part(
|
|
220
|
+
out += part(ctx)
|
|
236
221
|
}
|
|
237
222
|
return out
|
|
238
223
|
}
|
|
@@ -245,8 +230,8 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
245
230
|
// and `constructor` aren't callable as Jexl functions. Resolved per call
|
|
246
231
|
// rather than baked in, since functions are routinely registered after
|
|
247
232
|
// an expression has been compiled.
|
|
248
|
-
const lookup = (
|
|
249
|
-
const { functions } =
|
|
233
|
+
const lookup = () => {
|
|
234
|
+
const { functions } = grammar
|
|
250
235
|
if (!Object.hasOwn(functions, name)) {
|
|
251
236
|
throw new Error(`Jexl Function ${name} is not defined.`)
|
|
252
237
|
}
|
|
@@ -256,15 +241,15 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
256
241
|
// argument array and needs no spread
|
|
257
242
|
switch (args.length) {
|
|
258
243
|
case 0: {
|
|
259
|
-
return (
|
|
244
|
+
return () => lookup()()
|
|
260
245
|
}
|
|
261
246
|
case 1: {
|
|
262
247
|
const [a0] = args as [CompiledNode]
|
|
263
|
-
return (
|
|
248
|
+
return (ctx) => lookup()(a0(ctx))
|
|
264
249
|
}
|
|
265
250
|
case 2: {
|
|
266
251
|
const [a0, a1] = args as [CompiledNode, CompiledNode]
|
|
267
|
-
return (
|
|
252
|
+
return (ctx) => lookup()(a0(ctx), a1(ctx))
|
|
268
253
|
}
|
|
269
254
|
case 3: {
|
|
270
255
|
const [a0, a1, a2] = args as [
|
|
@@ -272,16 +257,16 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
272
257
|
CompiledNode,
|
|
273
258
|
CompiledNode
|
|
274
259
|
]
|
|
275
|
-
return (
|
|
260
|
+
return (ctx) => lookup()(a0(ctx), a1(ctx), a2(ctx))
|
|
276
261
|
}
|
|
277
262
|
default: {
|
|
278
263
|
const len = args.length
|
|
279
|
-
return (
|
|
264
|
+
return (ctx) => {
|
|
280
265
|
const vals: JexlValue[] = new Array(len)
|
|
281
266
|
for (let i = 0; i < len; i++) {
|
|
282
|
-
vals[i] = args[i]!(
|
|
267
|
+
vals[i] = args[i]!(ctx)
|
|
283
268
|
}
|
|
284
|
-
return lookup(
|
|
269
|
+
return lookup()(...vals)
|
|
285
270
|
}
|
|
286
271
|
}
|
|
287
272
|
}
|
|
@@ -290,10 +275,10 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
290
275
|
case 'SequenceExpression': {
|
|
291
276
|
const exprs = node.expressions.map((expr) => compileAst(expr, grammar))
|
|
292
277
|
const len = exprs.length
|
|
293
|
-
return (
|
|
278
|
+
return (ctx) => {
|
|
294
279
|
let last: JexlValue
|
|
295
280
|
for (let i = 0; i < len; i++) {
|
|
296
|
-
last = exprs[i]!(
|
|
281
|
+
last = exprs[i]!(ctx)
|
|
297
282
|
}
|
|
298
283
|
return last
|
|
299
284
|
}
|
|
@@ -303,9 +288,9 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
303
288
|
const name = node.left.value
|
|
304
289
|
const right = compileAst(node.right!, grammar)
|
|
305
290
|
const store = name === '__proto__' ? defineOwn : assignOwn
|
|
306
|
-
return (
|
|
307
|
-
const value = right(
|
|
308
|
-
store(
|
|
291
|
+
return (ctx) => {
|
|
292
|
+
const value = right(ctx)
|
|
293
|
+
store(ctx, name, value)
|
|
309
294
|
return value
|
|
310
295
|
}
|
|
311
296
|
}
|
package/src/grammar.ts
CHANGED
|
@@ -231,7 +231,7 @@ export const getGrammar = (): Grammar => ({
|
|
|
231
231
|
|
|
232
232
|
/**
|
|
233
233
|
* A map of function names to javascript functions. A Jexl function
|
|
234
|
-
* takes zero
|
|
234
|
+
* takes zero or more arguments:
|
|
235
235
|
*
|
|
236
236
|
* - {*} ...args: A variable number of arguments passed to this function.
|
|
237
237
|
* All of these are pre-evaluated to their actual values before calling
|
package/src/parser/Parser.ts
CHANGED
|
@@ -32,7 +32,7 @@ const tokenHandlers: Record<
|
|
|
32
32
|
/**
|
|
33
33
|
* The Parser is a state machine that converts tokens from the {@link Lexer}
|
|
34
34
|
* into an Abstract Syntax Tree (AST), capable of being evaluated in any
|
|
35
|
-
* context by
|
|
35
|
+
* context by {@link compileAst}. The Parser expects that all tokens
|
|
36
36
|
* provided to it are legal and typed properly according to the grammar, but
|
|
37
37
|
* accepts that the tokens may still be in an invalid order or in some other
|
|
38
38
|
* unparsable configuration that requires it to throw an Error.
|
|
@@ -52,13 +52,11 @@ class Parser {
|
|
|
52
52
|
_state: string
|
|
53
53
|
_tree: AstNode | null
|
|
54
54
|
_exprStr: string
|
|
55
|
-
_relative: boolean
|
|
56
55
|
_stopMap: Record<string, string>
|
|
57
56
|
_cursor?: AstNode | null
|
|
58
57
|
_subParser?: Parser
|
|
59
58
|
_parentStop?: boolean
|
|
60
59
|
_nextIdentEncapsulate?: boolean
|
|
61
|
-
_nextIdentRelative?: boolean
|
|
62
60
|
_curObjKey?: string
|
|
63
61
|
_sequenceExpressions?: AstNode[]
|
|
64
62
|
|
|
@@ -73,7 +71,6 @@ class Parser {
|
|
|
73
71
|
this._state = 'expectOperand'
|
|
74
72
|
this._tree = null
|
|
75
73
|
this._exprStr = prefix || ''
|
|
76
|
-
this._relative = false
|
|
77
74
|
this._stopMap = stopMap || {}
|
|
78
75
|
}
|
|
79
76
|
|
|
@@ -143,7 +140,7 @@ class Parser {
|
|
|
143
140
|
/**
|
|
144
141
|
* Marks this Parser instance as completed and retrieves the full AST.
|
|
145
142
|
* @returns {{}|null} a full expression tree, ready for evaluation by the
|
|
146
|
-
* {@link
|
|
143
|
+
* {@link compileAst} function, or null if no tokens were passed to
|
|
147
144
|
* the parser before complete was called
|
|
148
145
|
* @throws {Error} if the parser is not in a state where it's legal to end
|
|
149
146
|
* the expression, indicating that the expression is incomplete
|
|
@@ -179,14 +176,6 @@ class Parser {
|
|
|
179
176
|
return this._cursor ? this._tree : null
|
|
180
177
|
}
|
|
181
178
|
|
|
182
|
-
/**
|
|
183
|
-
* Indicates whether the expression tree contains a relative path identifier.
|
|
184
|
-
* @returns {boolean} true if a relative identifier exists false otherwise.
|
|
185
|
-
*/
|
|
186
|
-
isRelative() {
|
|
187
|
-
return this._relative
|
|
188
|
-
}
|
|
189
|
-
|
|
190
179
|
/**
|
|
191
180
|
* Ends a subexpression by completing the subParser and passing its result
|
|
192
181
|
* to the subHandler configured in the current state.
|