@jbrowse/jexl 4.0.1 → 5.0.1
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 +163 -20
- package/dist/Expression.d.ts +16 -0
- package/dist/Expression.js +28 -4
- package/dist/Expression.js.map +1 -1
- package/dist/Jexl.d.ts +20 -8
- package/dist/Jexl.js +34 -15
- package/dist/Jexl.js.map +1 -1
- package/dist/Lexer.d.ts +4 -3
- package/dist/Lexer.js +29 -21
- package/dist/Lexer.js.map +1 -1
- package/dist/analyze.d.ts +84 -0
- package/dist/analyze.js +259 -0
- package/dist/analyze.js.map +1 -0
- package/dist/check.d.ts +109 -0
- package/dist/check.js +1267 -0
- package/dist/check.js.map +1 -0
- package/dist/collections.d.ts +3 -0
- package/dist/collections.js +140 -0
- package/dist/collections.js.map +1 -0
- package/dist/conditions.d.ts +62 -0
- package/dist/conditions.js +207 -0
- package/dist/conditions.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -0
- package/dist/evaluator/compile.d.ts +10 -7
- package/dist/evaluator/compile.js +292 -99
- package/dist/evaluator/compile.js.map +1 -1
- package/dist/grammar.d.ts +22 -9
- package/dist/grammar.js +83 -55
- package/dist/grammar.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/dist/operators.d.ts +34 -0
- package/dist/operators.js +131 -0
- package/dist/operators.js.map +1 -0
- package/dist/parser/Parser.d.ts +48 -91
- package/dist/parser/Parser.js +347 -221
- package/dist/parser/Parser.js.map +1 -1
- package/dist/types.d.ts +11 -5
- package/esm/Expression.d.ts +16 -0
- package/esm/Expression.js +28 -4
- package/esm/Expression.js.map +1 -1
- package/esm/Jexl.d.ts +20 -8
- package/esm/Jexl.js +34 -15
- package/esm/Jexl.js.map +1 -1
- package/esm/Lexer.d.ts +4 -3
- package/esm/Lexer.js +29 -21
- package/esm/Lexer.js.map +1 -1
- package/esm/analyze.d.ts +84 -0
- package/esm/analyze.js +256 -0
- package/esm/analyze.js.map +1 -0
- package/esm/check.d.ts +109 -0
- package/esm/check.js +1260 -0
- package/esm/check.js.map +1 -0
- package/esm/collections.d.ts +3 -0
- package/esm/collections.js +137 -0
- package/esm/collections.js.map +1 -0
- package/esm/conditions.d.ts +62 -0
- package/esm/conditions.js +200 -0
- package/esm/conditions.js.map +1 -0
- package/esm/errors.d.ts +5 -0
- package/esm/errors.js +14 -0
- package/esm/errors.js.map +1 -0
- package/esm/evaluator/compile.d.ts +10 -7
- package/esm/evaluator/compile.js +292 -99
- package/esm/evaluator/compile.js.map +1 -1
- package/esm/grammar.d.ts +22 -9
- package/esm/grammar.js +79 -53
- package/esm/grammar.js.map +1 -1
- package/esm/index.d.ts +9 -0
- package/esm/index.js +4 -0
- package/esm/index.js.map +1 -1
- package/esm/operators.d.ts +34 -0
- package/esm/operators.js +123 -0
- package/esm/operators.js.map +1 -0
- package/esm/parser/Parser.d.ts +48 -91
- package/esm/parser/Parser.js +349 -192
- package/esm/parser/Parser.js.map +1 -1
- package/esm/types.d.ts +11 -5
- package/package.json +1 -1
- package/src/Expression.ts +34 -4
- package/src/Jexl.ts +52 -16
- package/src/Lexer.ts +32 -21
- package/src/analyze.ts +378 -0
- package/src/check.ts +1682 -0
- package/src/collections.ts +148 -0
- package/src/conditions.ts +262 -0
- package/src/errors.ts +15 -0
- package/src/evaluator/compile.ts +351 -102
- package/src/grammar.ts +137 -55
- package/src/index.ts +15 -0
- package/src/operators.ts +151 -0
- package/src/parser/Parser.ts +389 -199
- package/src/types.ts +13 -3
- package/dist/parser/handlers.d.ts +0 -115
- package/dist/parser/handlers.js +0 -352
- package/dist/parser/handlers.js.map +0 -1
- package/dist/parser/states.d.ts +0 -47
- package/dist/parser/states.js +0 -176
- package/dist/parser/states.js.map +0 -1
- package/esm/parser/handlers.d.ts +0 -115
- package/esm/parser/handlers.js +0 -328
- package/esm/parser/handlers.js.map +0 -1
- package/esm/parser/states.d.ts +0 -47
- package/esm/parser/states.js +0 -140
- package/esm/parser/states.js.map +0 -1
- package/src/parser/handlers.ts +0 -372
- package/src/parser/states.ts +0 -159
package/esm/types.d.ts
CHANGED
|
@@ -4,20 +4,21 @@ export interface TemplatePart {
|
|
|
4
4
|
}
|
|
5
5
|
export type JexlValue = string | number | boolean | null | undefined | JexlValue[] | {
|
|
6
6
|
[key: string]: JexlValue;
|
|
7
|
-
};
|
|
7
|
+
} | JexlFunction;
|
|
8
|
+
/** What a lambda evaluates to, for a registered function to call. */
|
|
9
|
+
export type JexlFunction = (...args: JexlValue[]) => JexlValue;
|
|
8
10
|
export interface Token {
|
|
9
11
|
type: string;
|
|
10
|
-
value: string | number | boolean | TemplatePart[];
|
|
12
|
+
value: string | number | boolean | null | TemplatePart[];
|
|
11
13
|
raw: string;
|
|
12
14
|
}
|
|
13
15
|
export interface AstNode {
|
|
14
16
|
type: string;
|
|
15
|
-
_parent?: AstNode;
|
|
16
17
|
right?: AstNode;
|
|
17
18
|
}
|
|
18
19
|
export interface Literal extends AstNode {
|
|
19
20
|
type: 'Literal';
|
|
20
|
-
value: string | number | boolean;
|
|
21
|
+
value: string | number | boolean | null;
|
|
21
22
|
}
|
|
22
23
|
export interface Identifier extends AstNode {
|
|
23
24
|
type: 'Identifier';
|
|
@@ -79,7 +80,12 @@ export interface AssignmentExpression extends AstNode {
|
|
|
79
80
|
operator: '=';
|
|
80
81
|
left: Identifier;
|
|
81
82
|
}
|
|
82
|
-
export
|
|
83
|
+
export interface Lambda extends AstNode {
|
|
84
|
+
type: 'Lambda';
|
|
85
|
+
params: string[];
|
|
86
|
+
body: AstNode;
|
|
87
|
+
}
|
|
88
|
+
export type AstNodeUnion = Literal | Identifier | BinaryExpression | UnaryExpression | ArrayLiteral | ObjectLiteral | TemplateLiteral | FunctionCall | FilterExpression | ConditionalExpression | SequenceExpression | AssignmentExpression | Lambda;
|
|
83
89
|
export type NodeByType<T extends AstNodeUnion['type']> = Extract<AstNodeUnion, {
|
|
84
90
|
type: T;
|
|
85
91
|
}>;
|
package/package.json
CHANGED
package/src/Expression.ts
CHANGED
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import Lexer from './Lexer.ts'
|
|
7
|
+
import { analyze } from './analyze.ts'
|
|
8
|
+
import { check } from './check.ts'
|
|
7
9
|
import { compileAst } from './evaluator/compile.ts'
|
|
8
10
|
import Parser from './parser/Parser.ts'
|
|
9
11
|
|
|
12
|
+
import type { AnalyzeOptions } from './analyze.ts'
|
|
13
|
+
import type { CheckOptions } from './check.ts'
|
|
10
14
|
import type { CompiledNode } from './evaluator/compile.ts'
|
|
11
15
|
import type { Grammar } from './grammar.ts'
|
|
12
16
|
import type { AstNode } from './types.ts'
|
|
@@ -45,10 +49,7 @@ class Expression {
|
|
|
45
49
|
* @returns {Expression} this Expression instance, for convenience
|
|
46
50
|
*/
|
|
47
51
|
compile() {
|
|
48
|
-
|
|
49
|
-
const tokens = this._lexer.tokenize(this._exprStr)
|
|
50
|
-
parser.addTokens(tokens)
|
|
51
|
-
this._ast = parser.complete()
|
|
52
|
+
this._ast = new Parser(this._grammar, this._lexer).parse(this._exprStr)
|
|
52
53
|
// lower the tree to closures once, here, so that eval() is just a call
|
|
53
54
|
this._fn = this._ast ? compileAst(this._ast, this._grammar) : null
|
|
54
55
|
this._compiled = true
|
|
@@ -72,6 +73,35 @@ class Expression {
|
|
|
72
73
|
}
|
|
73
74
|
return this._fn(context)
|
|
74
75
|
}
|
|
76
|
+
|
|
77
|
+
/** The expression text this Expression was made from. */
|
|
78
|
+
get source() {
|
|
79
|
+
return this._exprStr
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The parsed tree, or null for an expression with no tokens. */
|
|
83
|
+
get ast() {
|
|
84
|
+
if (!this._compiled) {
|
|
85
|
+
this.compile()
|
|
86
|
+
}
|
|
87
|
+
return this._ast
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Lists what the expression reads from its context, without evaluating it.
|
|
92
|
+
* See {@link analyze}.
|
|
93
|
+
*/
|
|
94
|
+
analyze(options?: AnalyzeOptions) {
|
|
95
|
+
return analyze(this.ast, options)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Checks the expression against a host's fields and functions, without
|
|
100
|
+
* evaluating it. See {@link check}.
|
|
101
|
+
*/
|
|
102
|
+
check(options?: CheckOptions) {
|
|
103
|
+
return check(this.ast, options)
|
|
104
|
+
}
|
|
75
105
|
}
|
|
76
106
|
|
|
77
107
|
export default Expression
|
package/src/Jexl.ts
CHANGED
|
@@ -10,14 +10,29 @@ import { getGrammar } from './grammar.ts'
|
|
|
10
10
|
import type {
|
|
11
11
|
BinaryOpEval,
|
|
12
12
|
BinaryOpEvalOnDemand,
|
|
13
|
+
GetMember,
|
|
13
14
|
Grammar,
|
|
14
15
|
GrammarElement,
|
|
15
16
|
GrammarFn,
|
|
16
17
|
UnaryOpEval,
|
|
17
|
-
UncheckedFn
|
|
18
|
+
UncheckedFn,
|
|
19
|
+
VariableReader
|
|
18
20
|
} from './grammar.ts'
|
|
19
21
|
import type { JexlValue } from './types.ts'
|
|
20
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Hooks for a host whose values keep their fields behind an accessor. Both are
|
|
25
|
+
* fixed at construction, since a compiled expression binds them.
|
|
26
|
+
*/
|
|
27
|
+
export interface JexlOptions {
|
|
28
|
+
/** Resolves `a.b` and `a[k]`. */
|
|
29
|
+
getMember?: GetMember
|
|
30
|
+
/** Resolves a bare name, such as a field of the current row. */
|
|
31
|
+
variableReader?: VariableReader
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const MAX_COMPILED = 10_000
|
|
35
|
+
|
|
21
36
|
/**
|
|
22
37
|
* Jexl is the Javascript Expression Language, capable of parsing and
|
|
23
38
|
* evaluating basic to complex expression strings, combined with advanced
|
|
@@ -29,9 +44,10 @@ class Jexl {
|
|
|
29
44
|
// shared by every Expression this instance creates, so that the expensive
|
|
30
45
|
// element-splitting regex is built once rather than per compile
|
|
31
46
|
_lexer: Lexer
|
|
47
|
+
_compiled = new Map<string, Expression>()
|
|
32
48
|
|
|
33
|
-
constructor() {
|
|
34
|
-
this._grammar = getGrammar()
|
|
49
|
+
constructor({ getMember, variableReader }: JexlOptions = {}) {
|
|
50
|
+
this._grammar = { ...getGrammar(), getMember, variableReader }
|
|
35
51
|
this._lexer = new Lexer(this._grammar)
|
|
36
52
|
this.expr = this.expr.bind(this)
|
|
37
53
|
}
|
|
@@ -73,6 +89,10 @@ class Jexl {
|
|
|
73
89
|
fn: UncheckedFn | BinaryOpEvalOnDemand,
|
|
74
90
|
manualEval?: boolean
|
|
75
91
|
) {
|
|
92
|
+
// replacing `-` keeps its prefix form, which is a separate operation
|
|
93
|
+
const previous = this._grammar.elements[operator]
|
|
94
|
+
const unaryEval =
|
|
95
|
+
previous?.type === 'binaryOp' ? previous.unaryEval : undefined
|
|
76
96
|
// the overloads above pair `fn` with `manualEval`; the implementation
|
|
77
97
|
// signature can't express that correlation, hence the assertions
|
|
78
98
|
this._addGrammarElement(
|
|
@@ -81,12 +101,14 @@ class Jexl {
|
|
|
81
101
|
? {
|
|
82
102
|
type: 'binaryOp',
|
|
83
103
|
precedence,
|
|
84
|
-
evalOnDemand: fn as BinaryOpEvalOnDemand
|
|
104
|
+
evalOnDemand: fn as BinaryOpEvalOnDemand,
|
|
105
|
+
unaryEval
|
|
85
106
|
}
|
|
86
107
|
: {
|
|
87
108
|
type: 'binaryOp',
|
|
88
109
|
precedence,
|
|
89
|
-
eval: fn as unknown as BinaryOpEval
|
|
110
|
+
eval: fn as unknown as BinaryOpEval,
|
|
111
|
+
unaryEval
|
|
90
112
|
}
|
|
91
113
|
)
|
|
92
114
|
}
|
|
@@ -132,16 +154,21 @@ class Jexl {
|
|
|
132
154
|
}
|
|
133
155
|
|
|
134
156
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* @param {string} expression The Jexl expression to be compiled
|
|
140
|
-
* @returns {Expression} The compiled Expression object
|
|
157
|
+
* Compiles an expression string, or returns the Expression this instance
|
|
158
|
+
* already compiled from the same string. Adding or removing an operator
|
|
159
|
+
* empties the cache, since a compiled expression binds its operators;
|
|
160
|
+
* functions are looked up per call and leave it alone.
|
|
141
161
|
*/
|
|
142
162
|
compile(expression: string) {
|
|
143
|
-
|
|
144
|
-
|
|
163
|
+
let compiled = this._compiled.get(expression)
|
|
164
|
+
if (!compiled) {
|
|
165
|
+
compiled = this.createExpression(expression).compile()
|
|
166
|
+
if (this._compiled.size >= MAX_COMPILED) {
|
|
167
|
+
this._compiled.delete(this._compiled.keys().next().value!)
|
|
168
|
+
}
|
|
169
|
+
this._compiled.set(expression, compiled)
|
|
170
|
+
}
|
|
171
|
+
return compiled
|
|
145
172
|
}
|
|
146
173
|
|
|
147
174
|
/**
|
|
@@ -154,6 +181,11 @@ class Jexl {
|
|
|
154
181
|
return new Expression(this._grammar, expression, this._lexer)
|
|
155
182
|
}
|
|
156
183
|
|
|
184
|
+
/** Parses an expression string, or throws a JexlSyntaxError. */
|
|
185
|
+
parse(expression: string) {
|
|
186
|
+
return this.compile(expression).ast
|
|
187
|
+
}
|
|
188
|
+
|
|
157
189
|
/**
|
|
158
190
|
* Retrieves a previously set expression function.
|
|
159
191
|
* @param {string} name The name of the expression function
|
|
@@ -172,8 +204,7 @@ class Jexl {
|
|
|
172
204
|
* @throws {*} on error
|
|
173
205
|
*/
|
|
174
206
|
eval(expression: string, context = {}) {
|
|
175
|
-
|
|
176
|
-
return exprObj.eval(context)
|
|
207
|
+
return this.compile(expression).eval(context)
|
|
177
208
|
}
|
|
178
209
|
|
|
179
210
|
/**
|
|
@@ -210,7 +241,7 @@ class Jexl {
|
|
|
210
241
|
const elem = this._grammar.elements[operator]
|
|
211
242
|
if (elem?.type === 'binaryOp' || elem?.type === 'unaryOp') {
|
|
212
243
|
Reflect.deleteProperty(this._grammar.elements, operator)
|
|
213
|
-
this.
|
|
244
|
+
this._grammarChanged()
|
|
214
245
|
}
|
|
215
246
|
}
|
|
216
247
|
|
|
@@ -223,7 +254,12 @@ class Jexl {
|
|
|
223
254
|
*/
|
|
224
255
|
_addGrammarElement(str: string, obj: GrammarElement) {
|
|
225
256
|
this._grammar.elements[str] = obj
|
|
257
|
+
this._grammarChanged()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_grammarChanged() {
|
|
226
261
|
this._lexer._clearCache()
|
|
262
|
+
this._compiled.clear()
|
|
227
263
|
}
|
|
228
264
|
}
|
|
229
265
|
|
package/src/Lexer.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Copyright 2020 Tom Shawver
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { JexlSyntaxError } from './errors.ts'
|
|
7
|
+
|
|
6
8
|
import type { Grammar } from './grammar.ts'
|
|
7
9
|
import type { TemplatePart, Token } from './types.ts'
|
|
8
10
|
|
|
@@ -12,9 +14,13 @@ import type { TemplatePart, Token } from './types.ts'
|
|
|
12
14
|
// disagree on some input, and each disagreement is an "Invalid expression
|
|
13
15
|
// token" for something the splitter was happy to produce.
|
|
14
16
|
const identChars = String.raw`a-zA-Zа-яА-Я_\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF$`
|
|
15
|
-
const
|
|
17
|
+
const identPart = `[${identChars}0-9]`
|
|
18
|
+
const identPattern = `[${identChars}]${identPart}*`
|
|
19
|
+
// a word operator such as `in`, but not part of a longer name. \b would do only
|
|
20
|
+
// for ASCII names, and split `in$` or `inà` into the operator and a remainder
|
|
21
|
+
const wholeWord = (word: string) => `(?<!${identPart})${word}(?!${identPart})`
|
|
16
22
|
// unsigned: whether a leading '-' negates is decided separately, in getTokens
|
|
17
|
-
const numberPattern = String.raw`(?:(?:[0-9]*\.[0-9]+)|[0-9]+)
|
|
23
|
+
const numberPattern = String.raw`(?:(?:[0-9]*\.[0-9]+)|[0-9]+)(?:[eE][+-]?[0-9]+)?`
|
|
18
24
|
|
|
19
25
|
const numericRegex = new RegExp(`^-?${numberPattern}$`)
|
|
20
26
|
const identRegex = new RegExp(`^${identPattern}$`)
|
|
@@ -35,11 +41,10 @@ const preOpRegexElems = [
|
|
|
35
41
|
String.raw`"(?:(?:\\")|[^"])*"`,
|
|
36
42
|
// Whitespace
|
|
37
43
|
String.raw`\s+`,
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
String.raw`\bfalse\b`
|
|
44
|
+
// ahead of the grammar's '.', so that '.5' is a number rather than a dot
|
|
45
|
+
numberPattern
|
|
41
46
|
]
|
|
42
|
-
const postOpRegexElems = [identPattern
|
|
47
|
+
const postOpRegexElems = [identPattern]
|
|
43
48
|
const unaryMinusToken = (): Token => ({
|
|
44
49
|
type: 'unaryOp',
|
|
45
50
|
value: '-',
|
|
@@ -53,7 +58,8 @@ const minusNegatesAfter = new Set([
|
|
|
53
58
|
'question',
|
|
54
59
|
'colon',
|
|
55
60
|
'comma',
|
|
56
|
-
'semicolon'
|
|
61
|
+
'semicolon',
|
|
62
|
+
'arrow'
|
|
57
63
|
])
|
|
58
64
|
|
|
59
65
|
/**
|
|
@@ -113,6 +119,7 @@ class Lexer {
|
|
|
113
119
|
// 1") accumulates on the minus rather than on the token before it, which
|
|
114
120
|
// is what lets the parser's error messages quote the expression verbatim.
|
|
115
121
|
let pendingMinus: Token | undefined
|
|
122
|
+
let offset = 0
|
|
116
123
|
for (const element of elements) {
|
|
117
124
|
if (this._isWhitespace(element)) {
|
|
118
125
|
const last = pendingMinus ?? tokens.at(-1)
|
|
@@ -129,18 +136,19 @@ class Lexer {
|
|
|
129
136
|
} else if (pendingMinus) {
|
|
130
137
|
if (numericRegex.exec(element)) {
|
|
131
138
|
// fold the sign into the number, so "-1" stays a single literal
|
|
132
|
-
const token = this._createToken('-' + element)
|
|
139
|
+
const token = this._createToken('-' + element, offset)
|
|
133
140
|
token.raw = pendingMinus.raw + element
|
|
134
141
|
tokens.push(token)
|
|
135
142
|
} else {
|
|
136
143
|
// anything else gets a standalone prefix operator, letting "-x",
|
|
137
144
|
// "-(a + b)" and "-foo.bar" negate a computed value
|
|
138
|
-
tokens.push(pendingMinus, this._createToken(element))
|
|
145
|
+
tokens.push(pendingMinus, this._createToken(element, offset))
|
|
139
146
|
}
|
|
140
147
|
pendingMinus = undefined
|
|
141
148
|
} else {
|
|
142
|
-
tokens.push(this._createToken(element))
|
|
149
|
+
tokens.push(this._createToken(element, offset))
|
|
143
150
|
}
|
|
151
|
+
offset += element.length
|
|
144
152
|
}
|
|
145
153
|
// Catch a - at the end of the string. Let the parser handle that issue.
|
|
146
154
|
if (pendingMinus) {
|
|
@@ -191,7 +199,7 @@ class Lexer {
|
|
|
191
199
|
* @throws {Error} if the provided string is not a valid expression element.
|
|
192
200
|
* @private
|
|
193
201
|
*/
|
|
194
|
-
_createToken(element: string): Token {
|
|
202
|
+
_createToken(element: string, offset = 0): Token {
|
|
195
203
|
const token: Token = {
|
|
196
204
|
type: 'literal',
|
|
197
205
|
value: element,
|
|
@@ -199,7 +207,7 @@ class Lexer {
|
|
|
199
207
|
}
|
|
200
208
|
if (element.startsWith('`')) {
|
|
201
209
|
token.type = 'templateString'
|
|
202
|
-
token.value = this._parseTemplateString(element)
|
|
210
|
+
token.value = this._parseTemplateString(element, offset)
|
|
203
211
|
return token
|
|
204
212
|
} else if (element.startsWith('"') || element.startsWith("'")) {
|
|
205
213
|
token.value = this._unquote(element)
|
|
@@ -207,30 +215,30 @@ class Lexer {
|
|
|
207
215
|
token.value = parseFloat(element)
|
|
208
216
|
} else if (element === 'true' || element === 'false') {
|
|
209
217
|
token.value = element === 'true'
|
|
218
|
+
} else if (element === 'null') {
|
|
219
|
+
token.value = null
|
|
210
220
|
} else if (Object.hasOwn(this._grammar.elements, element)) {
|
|
211
221
|
token.type = this._grammar.elements[element]!.type
|
|
212
222
|
} else if (identRegex.exec(element)) {
|
|
213
223
|
token.type = 'identifier'
|
|
214
224
|
} else {
|
|
215
|
-
throw new
|
|
225
|
+
throw new JexlSyntaxError(`Invalid expression token: ${element}`, offset)
|
|
216
226
|
}
|
|
217
227
|
return token
|
|
218
228
|
}
|
|
219
229
|
|
|
220
230
|
/**
|
|
221
231
|
* Escapes a string so that it can be treated as a string literal within a
|
|
222
|
-
* regular expression.
|
|
232
|
+
* regular expression. A word such as `in` also stops matching inside a
|
|
233
|
+
* longer name.
|
|
223
234
|
* @param {string} str The string to be escaped
|
|
224
235
|
* @returns {string} the RegExp-escaped string.
|
|
225
236
|
* @see https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions
|
|
226
237
|
* @private
|
|
227
238
|
*/
|
|
228
239
|
_escapeRegExp(str: string) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
str = String.raw`\b` + str + String.raw`\b`
|
|
232
|
-
}
|
|
233
|
-
return str
|
|
240
|
+
const escaped = str.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)
|
|
241
|
+
return identRegex.exec(str) ? wholeWord(escaped) : escaped
|
|
234
242
|
}
|
|
235
243
|
|
|
236
244
|
/**
|
|
@@ -307,7 +315,7 @@ class Lexer {
|
|
|
307
315
|
.replaceAll(escEscRegex, '\\')
|
|
308
316
|
}
|
|
309
317
|
|
|
310
|
-
_parseTemplateString(str: string) {
|
|
318
|
+
_parseTemplateString(str: string, offset = 0) {
|
|
311
319
|
const parts: TemplatePart[] = []
|
|
312
320
|
let current = 1
|
|
313
321
|
let staticStart = 1
|
|
@@ -355,7 +363,10 @@ class Lexer {
|
|
|
355
363
|
}
|
|
356
364
|
|
|
357
365
|
if (braceDepth !== 0) {
|
|
358
|
-
throw new
|
|
366
|
+
throw new JexlSyntaxError(
|
|
367
|
+
`Unclosed interpolation in template string: ${str}`,
|
|
368
|
+
offset + interpStart - '${'.length
|
|
369
|
+
)
|
|
359
370
|
}
|
|
360
371
|
|
|
361
372
|
parts.push({
|