@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.
Files changed (89) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/LICENSE.txt +19 -0
  3. package/README.md +216 -0
  4. package/dist/Expression.d.ts +38 -0
  5. package/dist/Expression.js +75 -0
  6. package/dist/Expression.js.map +1 -0
  7. package/dist/Jexl.d.ts +148 -0
  8. package/dist/Jexl.js +208 -0
  9. package/dist/Jexl.js.map +1 -0
  10. package/dist/Lexer.d.ts +130 -0
  11. package/dist/Lexer.js +321 -0
  12. package/dist/Lexer.js.map +1 -0
  13. package/dist/PromiseSync.d.ts +13 -0
  14. package/dist/PromiseSync.js +80 -0
  15. package/dist/PromiseSync.js.map +1 -0
  16. package/dist/evaluator/Evaluator.d.ts +92 -0
  17. package/dist/evaluator/Evaluator.js +153 -0
  18. package/dist/evaluator/Evaluator.js.map +1 -0
  19. package/dist/evaluator/handlers.d.ts +111 -0
  20. package/dist/evaluator/handlers.js +216 -0
  21. package/dist/evaluator/handlers.js.map +1 -0
  22. package/dist/grammar.d.ts +25 -0
  23. package/dist/grammar.js +179 -0
  24. package/dist/grammar.js.map +1 -0
  25. package/dist/index.d.ts +5 -0
  26. package/dist/index.js +20 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/package.json +1 -0
  29. package/dist/parser/Parser.d.ts +112 -0
  30. package/dist/parser/Parser.js +233 -0
  31. package/dist/parser/Parser.js.map +1 -0
  32. package/dist/parser/handlers.d.ts +112 -0
  33. package/dist/parser/handlers.js +302 -0
  34. package/dist/parser/handlers.js.map +1 -0
  35. package/dist/parser/states.d.ts +47 -0
  36. package/dist/parser/states.js +195 -0
  37. package/dist/parser/states.js.map +1 -0
  38. package/dist/types.d.ts +77 -0
  39. package/dist/types.js +7 -0
  40. package/dist/types.js.map +1 -0
  41. package/esm/Expression.d.ts +38 -0
  42. package/esm/Expression.js +70 -0
  43. package/esm/Expression.js.map +1 -0
  44. package/esm/Jexl.d.ts +148 -0
  45. package/esm/Jexl.js +202 -0
  46. package/esm/Jexl.js.map +1 -0
  47. package/esm/Lexer.d.ts +130 -0
  48. package/esm/Lexer.js +319 -0
  49. package/esm/Lexer.js.map +1 -0
  50. package/esm/PromiseSync.d.ts +13 -0
  51. package/esm/PromiseSync.js +78 -0
  52. package/esm/PromiseSync.js.map +1 -0
  53. package/esm/evaluator/Evaluator.d.ts +92 -0
  54. package/esm/evaluator/Evaluator.js +118 -0
  55. package/esm/evaluator/Evaluator.js.map +1 -0
  56. package/esm/evaluator/handlers.d.ts +111 -0
  57. package/esm/evaluator/handlers.js +202 -0
  58. package/esm/evaluator/handlers.js.map +1 -0
  59. package/esm/grammar.d.ts +25 -0
  60. package/esm/grammar.js +175 -0
  61. package/esm/grammar.js.map +1 -0
  62. package/esm/index.d.ts +5 -0
  63. package/esm/index.js +9 -0
  64. package/esm/index.js.map +1 -0
  65. package/esm/parser/Parser.d.ts +112 -0
  66. package/esm/parser/Parser.js +198 -0
  67. package/esm/parser/Parser.js.map +1 -0
  68. package/esm/parser/handlers.d.ts +112 -0
  69. package/esm/parser/handlers.js +280 -0
  70. package/esm/parser/handlers.js.map +1 -0
  71. package/esm/parser/states.d.ts +47 -0
  72. package/esm/parser/states.js +159 -0
  73. package/esm/parser/states.js.map +1 -0
  74. package/esm/types.d.ts +77 -0
  75. package/esm/types.js +6 -0
  76. package/esm/types.js.map +1 -0
  77. package/package.json +67 -0
  78. package/src/Expression.ts +95 -0
  79. package/src/Jexl.ts +232 -0
  80. package/src/Lexer.ts +344 -0
  81. package/src/PromiseSync.ts +86 -0
  82. package/src/evaluator/Evaluator.ts +153 -0
  83. package/src/evaluator/handlers.ts +226 -0
  84. package/src/grammar.ts +200 -0
  85. package/src/index.ts +10 -0
  86. package/src/parser/Parser.ts +235 -0
  87. package/src/parser/handlers.ts +309 -0
  88. package/src/parser/states.ts +177 -0
  89. 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