@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/src/parser/Parser.ts
CHANGED
|
@@ -3,253 +3,443 @@
|
|
|
3
3
|
* Copyright 2020 Tom Shawver
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
import { states } from './states.ts'
|
|
6
|
+
import { JexlSyntaxError } from '../errors.ts'
|
|
8
7
|
|
|
9
8
|
import type Lexer from '../Lexer.ts'
|
|
10
9
|
import type { Grammar } from '../grammar.ts'
|
|
11
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
AstNode,
|
|
12
|
+
AstNodeUnion,
|
|
13
|
+
ConditionalExpression,
|
|
14
|
+
Lambda,
|
|
15
|
+
Literal,
|
|
16
|
+
ObjectLiteral,
|
|
17
|
+
TemplateLiteral,
|
|
18
|
+
TemplatePart,
|
|
19
|
+
Token
|
|
20
|
+
} from '../types.ts'
|
|
12
21
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
dot: handlers.dot,
|
|
25
|
-
identifier: handlers.identifier,
|
|
26
|
-
literal: handlers.literal,
|
|
27
|
-
semicolon: handlers.semicolon,
|
|
28
|
-
templateString: handlers.templateString,
|
|
29
|
-
unaryOp: handlers.unaryOp
|
|
30
|
-
}
|
|
22
|
+
const logical = new Set(['&&', '||'])
|
|
23
|
+
|
|
24
|
+
const omittedAlternateBefore = new Set([
|
|
25
|
+
'closeParen',
|
|
26
|
+
'closeBracket',
|
|
27
|
+
'closeCurl',
|
|
28
|
+
'comma',
|
|
29
|
+
'semicolon'
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
export { JexlSyntaxError } from '../errors.ts'
|
|
31
33
|
|
|
32
34
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* unparsable configuration that requires it to throw an Error.
|
|
39
|
-
* @param {{}} grammar The grammar object to use to parse Jexl strings
|
|
40
|
-
* @param {string} [prefix] A string prefix to prepend to the expression string
|
|
41
|
-
* for error messaging purposes. This is useful for when a new Parser is
|
|
42
|
-
* instantiated to parse an subexpression, as the parent Parser's
|
|
43
|
-
* expression string thus far can be passed for a more user-friendly
|
|
44
|
-
* error message.
|
|
45
|
-
* @param {{}} [stopMap] A mapping of token types to any truthy value. When the
|
|
46
|
-
* token type is encountered, the parser will return the mapped value
|
|
47
|
-
* instead of boolean false.
|
|
35
|
+
* Converts the tokens from the {@link Lexer} into an Abstract Syntax Tree, for
|
|
36
|
+
* {@link compileAst} to lower to closures. A Pratt parser: each level of the
|
|
37
|
+
* grammar is a method, from `_sequence` (loosest) down to `_primary`, and
|
|
38
|
+
* binary operators are climbed by the precedence the grammar gives them, so
|
|
39
|
+
* operators a host registers slot in without the parser knowing them.
|
|
48
40
|
*/
|
|
49
41
|
class Parser {
|
|
50
42
|
_grammar: Grammar
|
|
51
43
|
_lexer: Lexer
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
_subParser?: Parser
|
|
58
|
-
_parentStop?: boolean
|
|
59
|
-
_nextIdentEncapsulate?: boolean
|
|
60
|
-
_curObjKey?: string
|
|
61
|
-
_sequenceExpressions?: AstNode[]
|
|
44
|
+
_tokens: Token[] = []
|
|
45
|
+
_pos = 0
|
|
46
|
+
_offset: number
|
|
47
|
+
_grouped?: WeakSet<AstNode>
|
|
48
|
+
_lambdaDepth = 0
|
|
62
49
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
) {
|
|
50
|
+
/**
|
|
51
|
+
* @param offset where the tokens start in the source, so that an error
|
|
52
|
+
* inside a template interpolation reports its place in the whole
|
|
53
|
+
*/
|
|
54
|
+
constructor(grammar: Grammar, lexer: Lexer, offset = 0) {
|
|
69
55
|
this._grammar = grammar
|
|
70
56
|
this._lexer = lexer
|
|
71
|
-
this.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
57
|
+
this._offset = offset
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
parse(source: string) {
|
|
61
|
+
const start = this._offset
|
|
62
|
+
this._offset += source.length - source.trimStart().length
|
|
63
|
+
try {
|
|
64
|
+
this.addTokens(this._lexer.tokenize(source))
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error instanceof JexlSyntaxError) {
|
|
67
|
+
error.offset += start
|
|
68
|
+
}
|
|
69
|
+
throw error
|
|
70
|
+
}
|
|
71
|
+
return this.complete()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
addTokens(tokens: Token[]) {
|
|
75
|
+
this._tokens = this._tokens.concat(tokens)
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* @param {{type: <string>}} token A token object, as provided by the
|
|
81
|
-
* {@link Lexer#tokenize} function.
|
|
82
|
-
* @throws {Error} if a token is added when the Parser has been marked as
|
|
83
|
-
* complete by {@link #complete}, or if an unexpected token type is added.
|
|
84
|
-
* @returns {boolean|*} the stopState value if this parser encountered a token
|
|
85
|
-
* in the stopState mapb false if tokens can continue.
|
|
79
|
+
* @returns the expression tree, or null for an expression with no tokens
|
|
80
|
+
* @throws {JexlSyntaxError} if the tokens do not form exactly one expression
|
|
86
81
|
*/
|
|
87
|
-
|
|
88
|
-
if (this.
|
|
89
|
-
|
|
82
|
+
complete(): AstNodeUnion | null {
|
|
83
|
+
if (this._tokens.length === 0) {
|
|
84
|
+
return null
|
|
90
85
|
}
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
86
|
+
const ast = this._sequence()
|
|
87
|
+
if (this._pos < this._tokens.length) {
|
|
88
|
+
throw this._unexpected(this._pos)
|
|
89
|
+
}
|
|
90
|
+
return ast
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_sequence(): AstNodeUnion {
|
|
94
|
+
const first = this._assignment()
|
|
95
|
+
if (this._peek()?.type !== 'semicolon') {
|
|
96
|
+
return first
|
|
97
|
+
}
|
|
98
|
+
const expressions: AstNode[] = [first]
|
|
99
|
+
while (this._eat('semicolon')) {
|
|
100
|
+
const next = this._peek()
|
|
101
|
+
if (!next || next.type === 'closeParen') {
|
|
102
|
+
break
|
|
103
|
+
}
|
|
104
|
+
expressions.push(this._assignment())
|
|
105
|
+
}
|
|
106
|
+
return { type: 'SequenceExpression', expressions }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_assignment(): AstNodeUnion {
|
|
110
|
+
if (this._atLambda()) {
|
|
111
|
+
return this._lambda()
|
|
112
|
+
}
|
|
113
|
+
const left = this._ternary()
|
|
114
|
+
const token = this._peek()
|
|
115
|
+
if (token?.type !== 'binaryOp' || token.value !== '=') {
|
|
116
|
+
return left
|
|
117
|
+
}
|
|
118
|
+
if (left.type !== 'Identifier' || left.from) {
|
|
119
|
+
throw this._error(
|
|
120
|
+
'Left side of assignment must be a variable name',
|
|
121
|
+
this._pos
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
if (this._lambdaDepth > 0) {
|
|
125
|
+
throw this._error('Assignment is not supported in a lambda', this._pos)
|
|
126
|
+
}
|
|
127
|
+
this._pos++
|
|
128
|
+
return {
|
|
129
|
+
type: 'AssignmentExpression',
|
|
130
|
+
operator: '=',
|
|
131
|
+
left,
|
|
132
|
+
right: this._assignment()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
_atLambda() {
|
|
137
|
+
let i = this._pos
|
|
138
|
+
if (this._tokens[i]?.type === 'identifier') {
|
|
139
|
+
return this._tokens[i + 1]?.type === 'arrow'
|
|
140
|
+
}
|
|
141
|
+
if (this._tokens[i]?.type !== 'openParen') {
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
i++
|
|
145
|
+
while (this._tokens[i]?.type === 'identifier') {
|
|
146
|
+
i++
|
|
147
|
+
if (this._tokens[i]?.type !== 'comma') {
|
|
148
|
+
break
|
|
106
149
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
150
|
+
i++
|
|
151
|
+
}
|
|
152
|
+
return (
|
|
153
|
+
this._tokens[i]?.type === 'closeParen' &&
|
|
154
|
+
this._tokens[i + 1]?.type === 'arrow'
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Parses the lambda {@link _atLambda} found, whose shape it has checked. */
|
|
159
|
+
_lambda(): Lambda {
|
|
160
|
+
const params: string[] = []
|
|
161
|
+
if (this._eat('openParen')) {
|
|
162
|
+
while (!this._eat('closeParen')) {
|
|
163
|
+
params.push(this._next().value as string)
|
|
164
|
+
this._eat('comma')
|
|
117
165
|
}
|
|
118
|
-
} else if (this._stopMap[token.type]) {
|
|
119
|
-
return this._stopMap[token.type]!
|
|
120
166
|
} else {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
167
|
+
params.push(this._next().value as string)
|
|
168
|
+
}
|
|
169
|
+
if (new Set(params).size < params.length) {
|
|
170
|
+
throw this._error('Duplicate lambda parameter name', this._pos)
|
|
124
171
|
}
|
|
125
|
-
|
|
172
|
+
this._pos++
|
|
173
|
+
this._lambdaDepth++
|
|
174
|
+
const body = this._assignment()
|
|
175
|
+
this._lambdaDepth--
|
|
176
|
+
return { type: 'Lambda', params, body }
|
|
126
177
|
}
|
|
127
178
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
* the {@link Lexer#tokenize} function.
|
|
133
|
-
*/
|
|
134
|
-
addTokens(tokens: Token[]) {
|
|
135
|
-
for (const token of tokens) {
|
|
136
|
-
this.addToken(token)
|
|
179
|
+
_ternary(): AstNodeUnion {
|
|
180
|
+
const test = this._binary(-Infinity)
|
|
181
|
+
if (!this._eat('question')) {
|
|
182
|
+
return test
|
|
137
183
|
}
|
|
184
|
+
const node: ConditionalExpression = { type: 'ConditionalExpression', test }
|
|
185
|
+
if (!this._eat('colon')) {
|
|
186
|
+
node.consequent = this._assignment()
|
|
187
|
+
this._expect('colon')
|
|
188
|
+
}
|
|
189
|
+
const next = this._peek()
|
|
190
|
+
if (next && !omittedAlternateBefore.has(next.type)) {
|
|
191
|
+
node.alternate = this._assignment()
|
|
192
|
+
}
|
|
193
|
+
return node
|
|
138
194
|
}
|
|
139
195
|
|
|
140
196
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* the parser before complete was called
|
|
145
|
-
* @throws {Error} if the parser is not in a state where it's legal to end
|
|
146
|
-
* the expression, indicating that the expression is incomplete
|
|
197
|
+
* Parses operands joined by binary operators that bind tighter than `floor`.
|
|
198
|
+
* One exactly at `floor` binds only when it continues a right-associative
|
|
199
|
+
* chain, which is how `a ^ b ^ c` groups from the right.
|
|
147
200
|
*/
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
201
|
+
_binary(floor: number, rightAssociative = false): AstNodeUnion {
|
|
202
|
+
let left = this._unary()
|
|
203
|
+
for (;;) {
|
|
204
|
+
const token = this._peek()
|
|
205
|
+
if (token?.type !== 'binaryOp' || token.value === '=') {
|
|
206
|
+
return left
|
|
207
|
+
}
|
|
208
|
+
const operator = token.value as string
|
|
209
|
+
const op = this._grammar.elements[operator]!
|
|
210
|
+
if (
|
|
211
|
+
op.type !== 'binaryOp' ||
|
|
212
|
+
op.precedence < floor ||
|
|
213
|
+
(op.precedence === floor && !(rightAssociative && op.rightAssociative))
|
|
214
|
+
) {
|
|
215
|
+
return left
|
|
216
|
+
}
|
|
217
|
+
const at = this._pos++
|
|
218
|
+
const right = this._binary(op.precedence, op.rightAssociative)
|
|
219
|
+
if (this._mixesNullish(operator, left, right)) {
|
|
220
|
+
throw this._error('Parenthesize ?? when mixing it with && or ||', at)
|
|
221
|
+
}
|
|
222
|
+
left = { type: 'BinaryExpression', operator, left, right }
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** `a ?? b || c` has no grouping a reader can guess; JS refuses it too. */
|
|
227
|
+
_mixesNullish(operator: string, ...operands: AstNodeUnion[]) {
|
|
228
|
+
return operands.some(
|
|
229
|
+
(operand) =>
|
|
230
|
+
operand.type === 'BinaryExpression' &&
|
|
231
|
+
!this._grouped?.has(operand) &&
|
|
232
|
+
(operator === '??'
|
|
233
|
+
? logical.has(operand.operator)
|
|
234
|
+
: logical.has(operator) && operand.operator === '??')
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
_unary(): AstNodeUnion {
|
|
239
|
+
const token = this._peek()
|
|
240
|
+
if (token?.type !== 'unaryOp') {
|
|
241
|
+
return this._postfix(this._primary())
|
|
158
242
|
}
|
|
159
|
-
|
|
160
|
-
|
|
243
|
+
this._pos++
|
|
244
|
+
const operator = token.value as string
|
|
245
|
+
const op = this._grammar.elements[operator]
|
|
246
|
+
return {
|
|
247
|
+
type: 'UnaryExpression',
|
|
248
|
+
operator,
|
|
249
|
+
right: this._binary(op?.type === 'unaryOp' ? op.precedence : Infinity)
|
|
161
250
|
}
|
|
251
|
+
}
|
|
162
252
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
253
|
+
_postfix(subject: AstNodeUnion): AstNodeUnion {
|
|
254
|
+
let node = subject
|
|
255
|
+
for (;;) {
|
|
256
|
+
switch (this._peek()?.type) {
|
|
257
|
+
case 'dot': {
|
|
258
|
+
this._pos++
|
|
259
|
+
node = {
|
|
260
|
+
type: 'Identifier',
|
|
261
|
+
value: this._expect('identifier').value as string,
|
|
262
|
+
from: node
|
|
263
|
+
}
|
|
264
|
+
break
|
|
265
|
+
}
|
|
266
|
+
case 'openBracket': {
|
|
267
|
+
this._pos++
|
|
268
|
+
const expr = this._assignment()
|
|
269
|
+
this._expect('closeBracket')
|
|
270
|
+
node = { type: 'FilterExpression', expr, subject: node }
|
|
271
|
+
break
|
|
272
|
+
}
|
|
273
|
+
case 'openParen': {
|
|
274
|
+
if (
|
|
275
|
+
node.type !== 'Identifier' ||
|
|
276
|
+
this._tokens[this._pos - 1]!.type !== 'identifier'
|
|
277
|
+
) {
|
|
278
|
+
throw this._error('Functions must be called by name', this._pos)
|
|
279
|
+
}
|
|
280
|
+
this._pos++
|
|
281
|
+
const { from, value } = node
|
|
282
|
+
const args = this._list('closeParen')
|
|
283
|
+
node = {
|
|
284
|
+
type: 'FunctionCall',
|
|
285
|
+
name: value,
|
|
286
|
+
args: from ? [from, ...args] : args
|
|
287
|
+
}
|
|
288
|
+
break
|
|
289
|
+
}
|
|
290
|
+
default: {
|
|
291
|
+
return node
|
|
292
|
+
}
|
|
170
293
|
}
|
|
171
|
-
this._state = 'complete'
|
|
172
|
-
return sequence
|
|
173
294
|
}
|
|
174
|
-
|
|
175
|
-
this._state = 'complete'
|
|
176
|
-
return this._cursor ? this._tree : null
|
|
177
295
|
}
|
|
178
296
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
297
|
+
_primary(): AstNodeUnion {
|
|
298
|
+
const token = this._next()
|
|
299
|
+
switch (token.type) {
|
|
300
|
+
case 'literal': {
|
|
301
|
+
return {
|
|
302
|
+
type: 'Literal',
|
|
303
|
+
value: token.value as string | number | boolean
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
case 'identifier': {
|
|
307
|
+
return { type: 'Identifier', value: token.value as string }
|
|
308
|
+
}
|
|
309
|
+
case 'templateString': {
|
|
310
|
+
return this._template(token.value as TemplatePart[], this._pos - 1)
|
|
311
|
+
}
|
|
312
|
+
case 'openParen': {
|
|
313
|
+
const node = this._sequence()
|
|
314
|
+
this._expect('closeParen')
|
|
315
|
+
this._grouped ??= new WeakSet()
|
|
316
|
+
this._grouped.add(node)
|
|
317
|
+
return node
|
|
318
|
+
}
|
|
319
|
+
case 'openBracket': {
|
|
320
|
+
return { type: 'ArrayLiteral', value: this._list('closeBracket') }
|
|
321
|
+
}
|
|
322
|
+
case 'openCurl': {
|
|
323
|
+
return this._object()
|
|
324
|
+
}
|
|
325
|
+
case 'dot': {
|
|
326
|
+
throw this._error('Relative paths are not supported', this._pos - 1)
|
|
327
|
+
}
|
|
328
|
+
default: {
|
|
329
|
+
throw this._unexpected(this._pos - 1)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
187
332
|
}
|
|
188
333
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (!this._cursor) {
|
|
198
|
-
this._tree = node
|
|
199
|
-
} else {
|
|
200
|
-
this._cursor.right = node
|
|
201
|
-
this._setParent(node, this._cursor)
|
|
334
|
+
_list(close: string): AstNode[] {
|
|
335
|
+
const items: AstNode[] = []
|
|
336
|
+
while (!this._eat(close)) {
|
|
337
|
+
items.push(this._assignment())
|
|
338
|
+
if (!this._eat('comma')) {
|
|
339
|
+
this._expect(close)
|
|
340
|
+
break
|
|
341
|
+
}
|
|
202
342
|
}
|
|
203
|
-
|
|
343
|
+
return items
|
|
204
344
|
}
|
|
205
345
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
346
|
+
_object(): ObjectLiteral {
|
|
347
|
+
const node: ObjectLiteral = { type: 'ObjectLiteral', value: {} }
|
|
348
|
+
while (!this._eat('closeCurl')) {
|
|
349
|
+
const key = this._next()
|
|
350
|
+
if (key.type !== 'identifier' && key.type !== 'literal') {
|
|
351
|
+
throw this._unexpected(this._pos - 1)
|
|
352
|
+
}
|
|
353
|
+
this._expect('colon')
|
|
354
|
+
// defined rather than assigned, so that "__proto__" is an ordinary key
|
|
355
|
+
Object.defineProperty(node.value, String(key.value as Literal['value']), {
|
|
356
|
+
value: this._assignment(),
|
|
357
|
+
writable: true,
|
|
358
|
+
enumerable: true,
|
|
359
|
+
configurable: true
|
|
360
|
+
})
|
|
361
|
+
if (!this._eat('comma')) {
|
|
362
|
+
this._expect('closeCurl')
|
|
363
|
+
break
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return node
|
|
217
367
|
}
|
|
218
368
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
value
|
|
231
|
-
|
|
369
|
+
_template(tokenParts: TemplatePart[], index: number): TemplateLiteral {
|
|
370
|
+
let offset = this._offsetOf(index) + '`'.length
|
|
371
|
+
const parts: TemplateLiteral['parts'] = tokenParts.map((part) => {
|
|
372
|
+
if (part.type === 'static') {
|
|
373
|
+
offset += part.value.length
|
|
374
|
+
return {
|
|
375
|
+
type: 'static',
|
|
376
|
+
value: this._lexer._unescapeTemplateString(part.value)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const start = offset + '${'.length
|
|
380
|
+
offset = start + part.value.length + '}'.length
|
|
381
|
+
const sub = new Parser(this._grammar, this._lexer, start)
|
|
382
|
+
sub._lambdaDepth = this._lambdaDepth
|
|
383
|
+
const value = sub.parse(part.value)
|
|
384
|
+
if (!value) {
|
|
385
|
+
throw new JexlSyntaxError(
|
|
386
|
+
'Empty interpolation in template string',
|
|
387
|
+
start
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
return { type: 'expression', value }
|
|
232
391
|
})
|
|
392
|
+
return { type: 'TemplateLiteral', parts }
|
|
233
393
|
}
|
|
234
394
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
let endStates = states[this._state]!.endStates
|
|
244
|
-
if (!endStates) {
|
|
245
|
-
this._parentStop = true
|
|
246
|
-
endStates = this._stopMap
|
|
395
|
+
_peek(): Token | undefined {
|
|
396
|
+
return this._tokens[this._pos]
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
_next(): Token {
|
|
400
|
+
const token = this._tokens[this._pos]
|
|
401
|
+
if (!token) {
|
|
402
|
+
throw this._error('Unexpected end of expression', this._pos)
|
|
247
403
|
}
|
|
248
|
-
|
|
249
|
-
|
|
404
|
+
this._pos++
|
|
405
|
+
return token
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
_eat(type: string) {
|
|
409
|
+
if (this._tokens[this._pos]?.type !== type) {
|
|
410
|
+
return false
|
|
411
|
+
}
|
|
412
|
+
this._pos++
|
|
413
|
+
return true
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
_expect(type: string): Token {
|
|
417
|
+
const token = this._next()
|
|
418
|
+
if (token.type !== type) {
|
|
419
|
+
throw this._unexpected(this._pos - 1)
|
|
420
|
+
}
|
|
421
|
+
return token
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
_unexpected(index: number) {
|
|
425
|
+
const { raw, type } = this._tokens[index]!
|
|
426
|
+
return this._error(`Token ${raw} (${type}) unexpected in expression`, index)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
_error(message: string, index: number) {
|
|
430
|
+
const source = this._tokens
|
|
431
|
+
.slice(0, index + 1)
|
|
432
|
+
.map((token) => token.raw)
|
|
433
|
+
.join('')
|
|
434
|
+
return new JexlSyntaxError(`${message}: ${source}`, this._offsetOf(index))
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
_offsetOf(index: number) {
|
|
438
|
+
let offset = this._offset
|
|
439
|
+
for (let i = 0; i < index; i++) {
|
|
440
|
+
offset += this._tokens[i]!.raw.length
|
|
250
441
|
}
|
|
251
|
-
|
|
252
|
-
return this._subParser
|
|
442
|
+
return offset
|
|
253
443
|
}
|
|
254
444
|
}
|
|
255
445
|
|