@jbrowse/jexl 2.3.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/CHANGELOG.md +222 -0
- package/LICENSE.txt +19 -0
- package/README.md +216 -0
- package/dist/Expression.d.ts +38 -0
- package/dist/Expression.js +75 -0
- package/dist/Expression.js.map +1 -0
- package/dist/Jexl.d.ts +148 -0
- package/dist/Jexl.js +208 -0
- package/dist/Jexl.js.map +1 -0
- package/dist/Lexer.d.ts +130 -0
- package/dist/Lexer.js +321 -0
- package/dist/Lexer.js.map +1 -0
- package/dist/PromiseSync.d.ts +13 -0
- package/dist/PromiseSync.js +80 -0
- package/dist/PromiseSync.js.map +1 -0
- package/dist/evaluator/Evaluator.d.ts +92 -0
- package/dist/evaluator/Evaluator.js +153 -0
- package/dist/evaluator/Evaluator.js.map +1 -0
- package/dist/evaluator/handlers.d.ts +111 -0
- package/dist/evaluator/handlers.js +216 -0
- package/dist/evaluator/handlers.js.map +1 -0
- package/dist/grammar.d.ts +25 -0
- package/dist/grammar.js +179 -0
- package/dist/grammar.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/package.json +1 -0
- package/dist/parser/Parser.d.ts +112 -0
- package/dist/parser/Parser.js +233 -0
- package/dist/parser/Parser.js.map +1 -0
- package/dist/parser/handlers.d.ts +112 -0
- package/dist/parser/handlers.js +302 -0
- package/dist/parser/handlers.js.map +1 -0
- package/dist/parser/states.d.ts +47 -0
- package/dist/parser/states.js +195 -0
- package/dist/parser/states.js.map +1 -0
- package/dist/types.d.ts +77 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/esm/Expression.d.ts +38 -0
- package/esm/Expression.js +70 -0
- package/esm/Expression.js.map +1 -0
- package/esm/Jexl.d.ts +148 -0
- package/esm/Jexl.js +202 -0
- package/esm/Jexl.js.map +1 -0
- package/esm/Lexer.d.ts +130 -0
- package/esm/Lexer.js +319 -0
- package/esm/Lexer.js.map +1 -0
- package/esm/PromiseSync.d.ts +13 -0
- package/esm/PromiseSync.js +78 -0
- package/esm/PromiseSync.js.map +1 -0
- package/esm/evaluator/Evaluator.d.ts +92 -0
- package/esm/evaluator/Evaluator.js +118 -0
- package/esm/evaluator/Evaluator.js.map +1 -0
- package/esm/evaluator/handlers.d.ts +111 -0
- package/esm/evaluator/handlers.js +202 -0
- package/esm/evaluator/handlers.js.map +1 -0
- package/esm/grammar.d.ts +25 -0
- package/esm/grammar.js +175 -0
- package/esm/grammar.js.map +1 -0
- package/esm/index.d.ts +5 -0
- package/esm/index.js +9 -0
- package/esm/index.js.map +1 -0
- package/esm/parser/Parser.d.ts +112 -0
- package/esm/parser/Parser.js +198 -0
- package/esm/parser/Parser.js.map +1 -0
- package/esm/parser/handlers.d.ts +112 -0
- package/esm/parser/handlers.js +280 -0
- package/esm/parser/handlers.js.map +1 -0
- package/esm/parser/states.d.ts +47 -0
- package/esm/parser/states.js +159 -0
- package/esm/parser/states.js.map +1 -0
- package/esm/types.d.ts +77 -0
- package/esm/types.js +6 -0
- package/esm/types.js.map +1 -0
- package/package.json +67 -0
- package/src/Expression.ts +95 -0
- package/src/Jexl.ts +232 -0
- package/src/Lexer.ts +344 -0
- package/src/PromiseSync.ts +86 -0
- package/src/evaluator/Evaluator.ts +153 -0
- package/src/evaluator/handlers.ts +226 -0
- package/src/grammar.ts +200 -0
- package/src/index.ts +10 -0
- package/src/parser/Parser.ts +235 -0
- package/src/parser/handlers.ts +309 -0
- package/src/parser/states.ts +177 -0
- package/src/types.ts +107 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Jexl
|
|
3
|
+
* Copyright 2020 Tom Shawver
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AstNode } from '../types.ts'
|
|
7
|
+
import type Evaluator from './Evaluator.ts'
|
|
8
|
+
|
|
9
|
+
const poolNames: Record<string, string> = {
|
|
10
|
+
functions: 'Jexl Function',
|
|
11
|
+
transforms: 'Transform'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Evaluates an ArrayLiteral by returning its value, with each element
|
|
16
|
+
* independently run through the evaluator.
|
|
17
|
+
* @param {{type: 'ObjectLiteral', value: <{}>}} ast An expression tree with an
|
|
18
|
+
* ObjectLiteral as the top node
|
|
19
|
+
* @returns {Promise.<[]>} resolves to a map contained evaluated values.
|
|
20
|
+
* @private
|
|
21
|
+
*/
|
|
22
|
+
export function ArrayLiteral(this: Evaluator, ast: any) {
|
|
23
|
+
return this.evalArray(ast.value)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Evaluates a BinaryExpression node by running the Grammar's evaluator for
|
|
28
|
+
* the given operator. Note that binary expressions support two types of
|
|
29
|
+
* evaluators: `eval` is called with the left and right operands pre-evaluated.
|
|
30
|
+
* `evalOnDemand`, if it exists, will be called with the left and right operands
|
|
31
|
+
* each individually wrapped in an object with an "eval" function that returns
|
|
32
|
+
* a promise with the resulting value. This allows the binary expression to
|
|
33
|
+
* evaluate the operands conditionally.
|
|
34
|
+
* @param {{type: 'BinaryExpression', operator: <string>, left: {},
|
|
35
|
+
* right: {}}} ast An expression tree with a BinaryExpression as the top
|
|
36
|
+
* node
|
|
37
|
+
* @returns {Promise<*>} resolves with the value of the BinaryExpression.
|
|
38
|
+
* @private
|
|
39
|
+
*/
|
|
40
|
+
export function BinaryExpression(this: Evaluator, ast: any) {
|
|
41
|
+
const grammarOp = this._grammar.elements[ast.operator]
|
|
42
|
+
if (grammarOp.evalOnDemand) {
|
|
43
|
+
const wrap = (subAst: AstNode) => ({ eval: () => this.eval(subAst) })
|
|
44
|
+
return grammarOp.evalOnDemand(wrap(ast.left), wrap(ast.right))
|
|
45
|
+
}
|
|
46
|
+
return this.Promise.all([this.eval(ast.left), this.eval(ast.right)]).then(
|
|
47
|
+
(arr: any[]) => grammarOp.eval(arr[0], arr[1])
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Evaluates a ConditionalExpression node by first evaluating its test branch,
|
|
53
|
+
* and resolving with the consequent branch if the test is truthy, or the
|
|
54
|
+
* alternate branch if it is not. If there is no consequent branch, the test
|
|
55
|
+
* result will be used instead.
|
|
56
|
+
* @param {{type: 'ConditionalExpression', test: {}, consequent: {},
|
|
57
|
+
* alternate: {}}} ast An expression tree with a ConditionalExpression as
|
|
58
|
+
* the top node
|
|
59
|
+
* @private
|
|
60
|
+
*/
|
|
61
|
+
export function ConditionalExpression(this: Evaluator, ast: any) {
|
|
62
|
+
return this.eval(ast.test).then((res: any) => {
|
|
63
|
+
if (res) {
|
|
64
|
+
if (ast.consequent) {
|
|
65
|
+
return this.eval(ast.consequent)
|
|
66
|
+
}
|
|
67
|
+
return res
|
|
68
|
+
}
|
|
69
|
+
return this.eval(ast.alternate)
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Evaluates a FilterExpression by applying it to the subject value.
|
|
75
|
+
* @param {{type: 'FilterExpression', relative: <boolean>, expr: {},
|
|
76
|
+
* subject: {}}} ast An expression tree with a FilterExpression as the top
|
|
77
|
+
* node
|
|
78
|
+
* @returns {Promise<*>} resolves with the value of the FilterExpression.
|
|
79
|
+
* @private
|
|
80
|
+
*/
|
|
81
|
+
export function FilterExpression(this: Evaluator, ast: any) {
|
|
82
|
+
return this.eval(ast.subject).then((subject: any) => {
|
|
83
|
+
if (ast.relative) {
|
|
84
|
+
return this._filterRelative(subject, ast.expr)
|
|
85
|
+
}
|
|
86
|
+
return this._filterStatic(subject, ast.expr)
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Evaluates an Identifier by either stemming from the evaluated 'from'
|
|
92
|
+
* expression tree or accessing the context provided when this Evaluator was
|
|
93
|
+
* constructed.
|
|
94
|
+
* @param {{type: 'Identifier', value: <string>, [from]: {}}} ast An expression
|
|
95
|
+
* tree with an Identifier as the top node
|
|
96
|
+
* @returns {Promise<*>|*} either the identifier's value, or a Promise that
|
|
97
|
+
* will resolve with the identifier's value.
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
export function Identifier(this: Evaluator, ast: any) {
|
|
101
|
+
if (!ast.from) {
|
|
102
|
+
const contextSource = ast.relative ? this._relContext : this._context
|
|
103
|
+
return contextSource[ast.value]
|
|
104
|
+
}
|
|
105
|
+
return this.eval(ast.from).then((context: any) => {
|
|
106
|
+
if (context == null) {
|
|
107
|
+
return undefined
|
|
108
|
+
}
|
|
109
|
+
const ctx = Array.isArray(context) ? context[0] : context
|
|
110
|
+
return ctx?.[ast.value]
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Evaluates a Literal by returning its value property.
|
|
116
|
+
* @param {{type: 'Literal', value: <string|number|boolean>}} ast An expression
|
|
117
|
+
* tree with a Literal as its only node
|
|
118
|
+
* @returns {string|number|boolean} The value of the Literal node
|
|
119
|
+
* @private
|
|
120
|
+
*/
|
|
121
|
+
export function Literal(this: Evaluator, ast: any) {
|
|
122
|
+
return ast.value
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Evaluates a TemplateLiteral by evaluating each interpolated expression
|
|
127
|
+
* and concatenating all parts into a final string.
|
|
128
|
+
* @param {{type: 'TemplateLiteral', parts: Array<{}>}} ast An expression
|
|
129
|
+
* tree with a TemplateLiteral as the top node
|
|
130
|
+
* @returns {Promise<string>} resolves with the final interpolated string
|
|
131
|
+
* @private
|
|
132
|
+
*/
|
|
133
|
+
export function TemplateLiteral(this: Evaluator, ast: any) {
|
|
134
|
+
const promises = ast.parts.map((part: any) => {
|
|
135
|
+
if (part.type === 'static') {
|
|
136
|
+
return this.Promise.resolve(part.value)
|
|
137
|
+
}
|
|
138
|
+
return this.eval(part.value).then((result: any) => {
|
|
139
|
+
if (result == null) {
|
|
140
|
+
return ''
|
|
141
|
+
}
|
|
142
|
+
return String(result)
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
return this.Promise.all(promises).then((values: string[]) => values.join(''))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Evaluates an ObjectLiteral by returning its value, with each key
|
|
151
|
+
* independently run through the evaluator.
|
|
152
|
+
* @param {{type: 'ObjectLiteral', value: <{}>}} ast An expression tree with an
|
|
153
|
+
* ObjectLiteral as the top node
|
|
154
|
+
* @returns {Promise<{}>} resolves to a map contained evaluated values.
|
|
155
|
+
* @private
|
|
156
|
+
*/
|
|
157
|
+
export function ObjectLiteral(this: Evaluator, ast: any) {
|
|
158
|
+
return this.evalMap(ast.value)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Evaluates a FunctionCall node by applying the supplied arguments to a
|
|
163
|
+
* function defined in one of the grammar's function pools.
|
|
164
|
+
* @param {{type: 'FunctionCall', name: <string>}} ast An
|
|
165
|
+
* expression tree with a FunctionCall as the top node
|
|
166
|
+
* @returns {Promise<*>|*} the value of the function call, or a Promise that
|
|
167
|
+
* will resolve with the resulting value.
|
|
168
|
+
* @private
|
|
169
|
+
*/
|
|
170
|
+
export function FunctionCall(this: Evaluator, ast: any) {
|
|
171
|
+
const poolName = poolNames[ast.pool]
|
|
172
|
+
if (!poolName) {
|
|
173
|
+
throw new Error(`Corrupt AST: Pool '${ast.pool}' not found`)
|
|
174
|
+
}
|
|
175
|
+
const pool = (this._grammar as any)[ast.pool]
|
|
176
|
+
const func = pool?.[ast.name]
|
|
177
|
+
if (!func) {
|
|
178
|
+
throw new Error(`${poolName} ${ast.name} is not defined.`)
|
|
179
|
+
}
|
|
180
|
+
return this.evalArray(ast.args || []).then((args: any[]) => func(...args))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Evaluates a Unary expression by passing the right side through the
|
|
185
|
+
* operator's eval function.
|
|
186
|
+
* @param {{type: 'UnaryExpression', operator: <string>, right: {}}} ast An
|
|
187
|
+
* expression tree with a UnaryExpression as the top node
|
|
188
|
+
* @returns {Promise<*>} resolves with the value of the UnaryExpression.
|
|
189
|
+
* @constructor
|
|
190
|
+
*/
|
|
191
|
+
export function UnaryExpression(this: Evaluator, ast: any) {
|
|
192
|
+
return this.eval(ast.right).then((right: any) =>
|
|
193
|
+
this._grammar.elements[ast.operator].eval(right)
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Evaluates a SequenceExpression by evaluating each expression in order
|
|
199
|
+
* and returning the value of the last expression.
|
|
200
|
+
*/
|
|
201
|
+
export function SequenceExpression(this: Evaluator, ast: any) {
|
|
202
|
+
let lastValue: any
|
|
203
|
+
let promise = this.Promise.resolve()
|
|
204
|
+
|
|
205
|
+
for (const expr of ast.expressions) {
|
|
206
|
+
promise = promise.then(() =>
|
|
207
|
+
this.eval(expr).then((val: any) => {
|
|
208
|
+
lastValue = val
|
|
209
|
+
})
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return promise.then(() => lastValue)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Evaluates an AssignmentExpression by evaluating the right side
|
|
218
|
+
* and assigning it to the variable name on the left side.
|
|
219
|
+
*/
|
|
220
|
+
export function AssignmentExpression(this: Evaluator, ast: any) {
|
|
221
|
+
return this.eval(ast.right).then((value: any) => {
|
|
222
|
+
const varName = ast.left.value
|
|
223
|
+
this._context[varName] = value
|
|
224
|
+
return value
|
|
225
|
+
})
|
|
226
|
+
}
|
package/src/grammar.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Jexl
|
|
3
|
+
* Copyright 2020 Tom Shawver
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* eslint eqeqeq:0 */
|
|
7
|
+
|
|
8
|
+
export interface BinaryOp {
|
|
9
|
+
type: 'binaryOp'
|
|
10
|
+
precedence: number
|
|
11
|
+
eval?: (left: any, right: any) => any
|
|
12
|
+
evalOnDemand?: (left: { eval: () => Promise<any> }, right: { eval: () => Promise<any> }) => Promise<any>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UnaryOp {
|
|
16
|
+
type: 'unaryOp'
|
|
17
|
+
precedence: number
|
|
18
|
+
eval: (right: any) => any
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SimpleElement {
|
|
22
|
+
type: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type GrammarElement = BinaryOp | UnaryOp | SimpleElement
|
|
26
|
+
|
|
27
|
+
export interface Grammar {
|
|
28
|
+
elements: Record<string, GrammarElement>
|
|
29
|
+
functions: Record<string, (...args: any[]) => any>
|
|
30
|
+
transforms: Record<string, (val: any, ...args: any[]) => any>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const getGrammar = (): Grammar => ({
|
|
34
|
+
/**
|
|
35
|
+
* A map of all expression elements to their properties. Note that changes
|
|
36
|
+
* here may require changes in the Lexer or Parser.
|
|
37
|
+
* @type {{}}
|
|
38
|
+
*/
|
|
39
|
+
elements: {
|
|
40
|
+
'.': { type: 'dot' },
|
|
41
|
+
'[': { type: 'openBracket' },
|
|
42
|
+
']': { type: 'closeBracket' },
|
|
43
|
+
'|': { type: 'pipe' },
|
|
44
|
+
'{': { type: 'openCurl' },
|
|
45
|
+
'}': { type: 'closeCurl' },
|
|
46
|
+
':': { type: 'colon' },
|
|
47
|
+
',': { type: 'comma' },
|
|
48
|
+
'(': { type: 'openParen' },
|
|
49
|
+
')': { type: 'closeParen' },
|
|
50
|
+
'?': { type: 'question' },
|
|
51
|
+
';': { type: 'semicolon' },
|
|
52
|
+
'+': {
|
|
53
|
+
type: 'binaryOp',
|
|
54
|
+
precedence: 30,
|
|
55
|
+
eval: (left, right) => left + right
|
|
56
|
+
},
|
|
57
|
+
'-': {
|
|
58
|
+
type: 'binaryOp',
|
|
59
|
+
precedence: 30,
|
|
60
|
+
eval: (left, right) => left - right
|
|
61
|
+
},
|
|
62
|
+
'*': {
|
|
63
|
+
type: 'binaryOp',
|
|
64
|
+
precedence: 40,
|
|
65
|
+
eval: (left, right) => left * right
|
|
66
|
+
},
|
|
67
|
+
'/': {
|
|
68
|
+
type: 'binaryOp',
|
|
69
|
+
precedence: 40,
|
|
70
|
+
eval: (left, right) => left / right
|
|
71
|
+
},
|
|
72
|
+
'//': {
|
|
73
|
+
type: 'binaryOp',
|
|
74
|
+
precedence: 40,
|
|
75
|
+
eval: (left, right) => Math.floor(left / right)
|
|
76
|
+
},
|
|
77
|
+
'%': {
|
|
78
|
+
type: 'binaryOp',
|
|
79
|
+
precedence: 50,
|
|
80
|
+
eval: (left, right) => left % right
|
|
81
|
+
},
|
|
82
|
+
'^': {
|
|
83
|
+
type: 'binaryOp',
|
|
84
|
+
precedence: 50,
|
|
85
|
+
eval: (left, right) => Math.pow(left, right)
|
|
86
|
+
},
|
|
87
|
+
'==': {
|
|
88
|
+
type: 'binaryOp',
|
|
89
|
+
precedence: 20,
|
|
90
|
+
eval: (left, right) => left == right
|
|
91
|
+
},
|
|
92
|
+
'!=': {
|
|
93
|
+
type: 'binaryOp',
|
|
94
|
+
precedence: 20,
|
|
95
|
+
eval: (left, right) => left != right
|
|
96
|
+
},
|
|
97
|
+
'>': {
|
|
98
|
+
type: 'binaryOp',
|
|
99
|
+
precedence: 20,
|
|
100
|
+
eval: (left, right) => left > right
|
|
101
|
+
},
|
|
102
|
+
'>=': {
|
|
103
|
+
type: 'binaryOp',
|
|
104
|
+
precedence: 20,
|
|
105
|
+
eval: (left, right) => left >= right
|
|
106
|
+
},
|
|
107
|
+
'<': {
|
|
108
|
+
type: 'binaryOp',
|
|
109
|
+
precedence: 20,
|
|
110
|
+
eval: (left, right) => left < right
|
|
111
|
+
},
|
|
112
|
+
'<=': {
|
|
113
|
+
type: 'binaryOp',
|
|
114
|
+
precedence: 20,
|
|
115
|
+
eval: (left, right) => left <= right
|
|
116
|
+
},
|
|
117
|
+
'&&': {
|
|
118
|
+
type: 'binaryOp',
|
|
119
|
+
precedence: 10,
|
|
120
|
+
evalOnDemand: (left, right) => {
|
|
121
|
+
return left.eval().then((leftVal) => {
|
|
122
|
+
if (!leftVal) {return leftVal}
|
|
123
|
+
return right.eval()
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
'||': {
|
|
128
|
+
type: 'binaryOp',
|
|
129
|
+
precedence: 10,
|
|
130
|
+
evalOnDemand: (left, right) => {
|
|
131
|
+
return left.eval().then((leftVal) => {
|
|
132
|
+
if (leftVal) {return leftVal}
|
|
133
|
+
return right.eval()
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
in: {
|
|
138
|
+
type: 'binaryOp',
|
|
139
|
+
precedence: 20,
|
|
140
|
+
eval: (left, right) => {
|
|
141
|
+
if (typeof right === 'string') {
|
|
142
|
+
return right.includes(left)
|
|
143
|
+
}
|
|
144
|
+
if (Array.isArray(right)) {
|
|
145
|
+
return right.includes(left)
|
|
146
|
+
}
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
'!': {
|
|
151
|
+
type: 'unaryOp',
|
|
152
|
+
precedence: Infinity,
|
|
153
|
+
eval: (right) => !right
|
|
154
|
+
},
|
|
155
|
+
'=': {
|
|
156
|
+
type: 'binaryOp',
|
|
157
|
+
precedence: 2,
|
|
158
|
+
eval: (_left, _right) => {
|
|
159
|
+
throw new Error('Assignment handled specially')
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* A map of function names to javascript functions. A Jexl function
|
|
166
|
+
* takes zero ore more arguemnts:
|
|
167
|
+
*
|
|
168
|
+
* - {*} ...args: A variable number of arguments passed to this function.
|
|
169
|
+
* All of these are pre-evaluated to their actual values before calling
|
|
170
|
+
* the function.
|
|
171
|
+
*
|
|
172
|
+
* The Jexl function should return either the transformed value, or
|
|
173
|
+
* a Promises/A+ Promise object that resolves with the value and rejects
|
|
174
|
+
* or throws only when an unrecoverable error occurs. Functions should
|
|
175
|
+
* generally return undefined when they don't make sense to be used on the
|
|
176
|
+
* given value type, rather than throw/reject. An error is only
|
|
177
|
+
* appropriate when the function would normally return a value, but
|
|
178
|
+
* cannot due to some other failure.
|
|
179
|
+
*/
|
|
180
|
+
functions: {},
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* A map of transform names to transform functions. A transform function
|
|
184
|
+
* takes one ore more arguemnts:
|
|
185
|
+
*
|
|
186
|
+
* - {*} val: A value to be transformed
|
|
187
|
+
* - {*} ...args: A variable number of arguments passed to this transform.
|
|
188
|
+
* All of these are pre-evaluated to their actual values before calling
|
|
189
|
+
* the function.
|
|
190
|
+
*
|
|
191
|
+
* The transform function should return either the transformed value, or
|
|
192
|
+
* a Promises/A+ Promise object that resolves with the value and rejects
|
|
193
|
+
* or throws only when an unrecoverable error occurs. Transforms should
|
|
194
|
+
* generally return undefined when they don't make sense to be used on the
|
|
195
|
+
* given value type, rather than throw/reject. An error is only
|
|
196
|
+
* appropriate when the transform would normally return a value, but
|
|
197
|
+
* cannot due to some other failure.
|
|
198
|
+
*/
|
|
199
|
+
transforms: {}
|
|
200
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Jexl
|
|
3
|
+
* Copyright 2020 Tom Shawver
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { Jexl, default } from './Jexl.ts'
|
|
7
|
+
export { default as Expression } from './Expression.ts'
|
|
8
|
+
export { default as Lexer } from './Lexer.ts'
|
|
9
|
+
export { getGrammar } from './grammar.ts'
|
|
10
|
+
export type * from './types.ts'
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Jexl
|
|
3
|
+
* Copyright 2020 Tom Shawver
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as handlers from './handlers.ts'
|
|
7
|
+
import { states } from './states.ts'
|
|
8
|
+
|
|
9
|
+
import type Lexer from '../Lexer.ts'
|
|
10
|
+
import type { AstNode, Token } from '../types.ts'
|
|
11
|
+
|
|
12
|
+
interface Grammar {
|
|
13
|
+
elements: Record<string, any>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The Parser is a state machine that converts tokens from the {@link Lexer}
|
|
18
|
+
* into an Abstract Syntax Tree (AST), capable of being evaluated in any
|
|
19
|
+
* context by the {@link Evaluator}. The Parser expects that all tokens
|
|
20
|
+
* provided to it are legal and typed properly according to the grammar, but
|
|
21
|
+
* accepts that the tokens may still be in an invalid order or in some other
|
|
22
|
+
* unparsable configuration that requires it to throw an Error.
|
|
23
|
+
* @param {{}} grammar The grammar object to use to parse Jexl strings
|
|
24
|
+
* @param {string} [prefix] A string prefix to prepend to the expression string
|
|
25
|
+
* for error messaging purposes. This is useful for when a new Parser is
|
|
26
|
+
* instantiated to parse an subexpression, as the parent Parser's
|
|
27
|
+
* expression string thus far can be passed for a more user-friendly
|
|
28
|
+
* error message.
|
|
29
|
+
* @param {{}} [stopMap] A mapping of token types to any truthy value. When the
|
|
30
|
+
* token type is encountered, the parser will return the mapped value
|
|
31
|
+
* instead of boolean false.
|
|
32
|
+
*/
|
|
33
|
+
class Parser {
|
|
34
|
+
_grammar: Grammar
|
|
35
|
+
_lexer: Lexer
|
|
36
|
+
_state: string
|
|
37
|
+
_tree: AstNode | null
|
|
38
|
+
_exprStr: string
|
|
39
|
+
_relative: boolean
|
|
40
|
+
_stopMap: Record<string, any>
|
|
41
|
+
_cursor?: AstNode | null
|
|
42
|
+
_subParser?: Parser
|
|
43
|
+
_parentStop?: boolean
|
|
44
|
+
_nextIdentEncapsulate?: boolean
|
|
45
|
+
_nextIdentRelative?: boolean
|
|
46
|
+
_curObjKey?: string
|
|
47
|
+
_sequenceExpressions?: AstNode[]
|
|
48
|
+
|
|
49
|
+
constructor(
|
|
50
|
+
grammar: Grammar,
|
|
51
|
+
lexer: Lexer,
|
|
52
|
+
prefix?: string,
|
|
53
|
+
stopMap?: Record<string, any>
|
|
54
|
+
) {
|
|
55
|
+
this._grammar = grammar
|
|
56
|
+
this._lexer = lexer
|
|
57
|
+
this._state = 'expectOperand'
|
|
58
|
+
this._tree = null
|
|
59
|
+
this._exprStr = prefix || ''
|
|
60
|
+
this._relative = false
|
|
61
|
+
this._stopMap = stopMap || {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Processes a new token into the AST and manages the transitions of the state
|
|
66
|
+
* machine.
|
|
67
|
+
* @param {{type: <string>}} token A token object, as provided by the
|
|
68
|
+
* {@link Lexer#tokenize} function.
|
|
69
|
+
* @throws {Error} if a token is added when the Parser has been marked as
|
|
70
|
+
* complete by {@link #complete}, or if an unexpected token type is added.
|
|
71
|
+
* @returns {boolean|*} the stopState value if this parser encountered a token
|
|
72
|
+
* in the stopState mapb false if tokens can continue.
|
|
73
|
+
*/
|
|
74
|
+
addToken(token: Token): any {
|
|
75
|
+
if (this._state === 'complete') {
|
|
76
|
+
throw new Error('Cannot add a new token to a completed Parser')
|
|
77
|
+
}
|
|
78
|
+
const state = states[this._state]
|
|
79
|
+
const startExpr = this._exprStr
|
|
80
|
+
this._exprStr += token.raw
|
|
81
|
+
if (state.subHandler) {
|
|
82
|
+
if (!this._subParser) {
|
|
83
|
+
this._startSubExpression(startExpr)
|
|
84
|
+
}
|
|
85
|
+
const stopState = this._subParser!.addToken(token)
|
|
86
|
+
if (stopState) {
|
|
87
|
+
this._endSubExpression()
|
|
88
|
+
if (this._parentStop) {
|
|
89
|
+
return stopState
|
|
90
|
+
}
|
|
91
|
+
this._state = stopState
|
|
92
|
+
}
|
|
93
|
+
} else if (state.tokenTypes?.[token.type]) {
|
|
94
|
+
const typeOpts = state.tokenTypes[token.type]
|
|
95
|
+
let handleFunc = (handlers as any)[token.type]
|
|
96
|
+
if (typeOpts.handler) {
|
|
97
|
+
handleFunc = typeOpts.handler
|
|
98
|
+
}
|
|
99
|
+
if (handleFunc) {
|
|
100
|
+
handleFunc.call(this, token)
|
|
101
|
+
}
|
|
102
|
+
if (typeOpts.toState) {
|
|
103
|
+
this._state = typeOpts.toState
|
|
104
|
+
}
|
|
105
|
+
} else if (this._stopMap[token.type]) {
|
|
106
|
+
return this._stopMap[token.type]
|
|
107
|
+
} else {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Token ${token.raw} (${token.type}) unexpected in expression: ${this._exprStr}`
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Processes an array of tokens iteratively through the {@link #addToken}
|
|
117
|
+
* function.
|
|
118
|
+
* @param {Array<{type: <string>}>} tokens An array of tokens, as provided by
|
|
119
|
+
* the {@link Lexer#tokenize} function.
|
|
120
|
+
*/
|
|
121
|
+
addTokens(tokens: Token[]) {
|
|
122
|
+
tokens.forEach((token) => this.addToken(token))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Marks this Parser instance as completed and retrieves the full AST.
|
|
127
|
+
* @returns {{}|null} a full expression tree, ready for evaluation by the
|
|
128
|
+
* {@link Evaluator#eval} function, or null if no tokens were passed to
|
|
129
|
+
* the parser before complete was called
|
|
130
|
+
* @throws {Error} if the parser is not in a state where it's legal to end
|
|
131
|
+
* the expression, indicating that the expression is incomplete
|
|
132
|
+
*/
|
|
133
|
+
complete() {
|
|
134
|
+
if (this._cursor && !states[this._state].completable) {
|
|
135
|
+
throw new Error(`Unexpected end of expression: ${this._exprStr}`)
|
|
136
|
+
}
|
|
137
|
+
if (this._subParser) {
|
|
138
|
+
this._endSubExpression()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (this._sequenceExpressions) {
|
|
142
|
+
this._sequenceExpressions.push(this._tree!)
|
|
143
|
+
const sequence: any = {
|
|
144
|
+
type: 'SequenceExpression',
|
|
145
|
+
expressions: this._sequenceExpressions
|
|
146
|
+
}
|
|
147
|
+
this._state = 'complete'
|
|
148
|
+
return sequence
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this._state = 'complete'
|
|
152
|
+
return this._cursor ? this._tree : null
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Indicates whether the expression tree contains a relative path identifier.
|
|
157
|
+
* @returns {boolean} true if a relative identifier exists false otherwise.
|
|
158
|
+
*/
|
|
159
|
+
isRelative() {
|
|
160
|
+
return this._relative
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Ends a subexpression by completing the subParser and passing its result
|
|
165
|
+
* to the subHandler configured in the current state.
|
|
166
|
+
* @private
|
|
167
|
+
*/
|
|
168
|
+
_endSubExpression() {
|
|
169
|
+
states[this._state].subHandler!.call(this, this._subParser!.complete())
|
|
170
|
+
this._subParser = undefined
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Places a new tree node at the current position of the cursor (to the 'right'
|
|
175
|
+
* property) and then advances the cursor to the new node. This function also
|
|
176
|
+
* handles setting the parent of the new node.
|
|
177
|
+
* @param {{type: <string>}} node A node to be added to the AST
|
|
178
|
+
* @private
|
|
179
|
+
*/
|
|
180
|
+
_placeAtCursor(node: AstNode) {
|
|
181
|
+
if (!this._cursor) {
|
|
182
|
+
this._tree = node
|
|
183
|
+
} else {
|
|
184
|
+
;(this._cursor as any).right = node
|
|
185
|
+
this._setParent(node, this._cursor)
|
|
186
|
+
}
|
|
187
|
+
this._cursor = node
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Places a tree node before the current position of the cursor, replacing
|
|
192
|
+
* the node that the cursor currently points to. This should only be called in
|
|
193
|
+
* cases where the cursor is known to exist, and the provided node already
|
|
194
|
+
* contains a pointer to what's at the cursor currently.
|
|
195
|
+
* @param {{type: <string>}} node A node to be added to the AST
|
|
196
|
+
* @private
|
|
197
|
+
*/
|
|
198
|
+
_placeBeforeCursor(node: AstNode) {
|
|
199
|
+
this._cursor = this._cursor?._parent
|
|
200
|
+
this._placeAtCursor(node)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Sets the parent of a node by creating a non-enumerable _parent property
|
|
205
|
+
* that points to the supplied parent argument.
|
|
206
|
+
* @param {{type: <string>}} node A node of the AST on which to set a new
|
|
207
|
+
* parent
|
|
208
|
+
* @param {{type: <string>}} parent An existing node of the AST to serve as the
|
|
209
|
+
* parent of the new node
|
|
210
|
+
* @private
|
|
211
|
+
*/
|
|
212
|
+
_setParent(node: AstNode, parent: AstNode) {
|
|
213
|
+
Object.defineProperty(node, '_parent', {
|
|
214
|
+
value: parent,
|
|
215
|
+
writable: true
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Prepares the Parser to accept a subexpression by (re)instantiating the
|
|
221
|
+
* subParser.
|
|
222
|
+
* @param {string} [exprStr] The expression string to prefix to the new Parser
|
|
223
|
+
* @private
|
|
224
|
+
*/
|
|
225
|
+
_startSubExpression(exprStr?: string) {
|
|
226
|
+
let endStates = states[this._state].endStates
|
|
227
|
+
if (!endStates) {
|
|
228
|
+
this._parentStop = true
|
|
229
|
+
endStates = this._stopMap
|
|
230
|
+
}
|
|
231
|
+
this._subParser = new Parser(this._grammar, this._lexer, exprStr, endStates)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export default Parser
|