@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
package/src/Lexer.ts ADDED
@@ -0,0 +1,344 @@
1
+ /*
2
+ * Jexl
3
+ * Copyright 2020 Tom Shawver
4
+ */
5
+
6
+ import type { Token } from './types.ts'
7
+
8
+ const numericRegex = /^-?(?:(?:[0-9]*\.[0-9]+)|[0-9]+)$/
9
+ const identRegex =
10
+ /^[a-zA-Zа-яА-Я_\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF$][a-zA-Zа-яА-Я0-9_\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF$]*$/
11
+ const escEscRegex = /\\\\/
12
+ const whitespaceRegex = /^\s*$/
13
+ const preOpRegexElems = [
14
+ // Template strings
15
+ '`(?:[^`\\\\]|\\\\.)*`',
16
+ // Strings
17
+ String.raw`'(?:(?:\\')|[^'])*'`,
18
+ String.raw`"(?:(?:\\")|[^"])*"`,
19
+ // Whitespace
20
+ String.raw`\s+`,
21
+ // Booleans
22
+ String.raw`\btrue\b`,
23
+ String.raw`\bfalse\b`
24
+ ]
25
+ const postOpRegexElems = [
26
+ // Identifiers
27
+ '[a-zA-Zа-яА-Я_\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\\$][a-zA-Z0-9а-яА-Я_\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\\$]*',
28
+ // Numerics (without negative symbol)
29
+ String.raw`(?:(?:[0-9]*\.[0-9]+)|[0-9]+)`
30
+ ]
31
+ const minusNegatesAfter = new Set([
32
+ 'binaryOp',
33
+ 'unaryOp',
34
+ 'openParen',
35
+ 'openBracket',
36
+ 'question',
37
+ 'colon'
38
+ ])
39
+
40
+ interface Grammar {
41
+ elements: Record<string, any>
42
+ }
43
+
44
+ /**
45
+ * Lexer is a collection of stateless, statically-accessed functions for the
46
+ * lexical parsing of a Jexl string. Its responsibility is to identify the
47
+ * "parts of speech" of a Jexl expression, and tokenize and label each, but
48
+ * to do only the most minimal syntax checking; the only errors the Lexer
49
+ * should be concerned with are if it's unable to identify the utility of
50
+ * any of its tokens. Errors stemming from these tokens not being in a
51
+ * sensible configuration should be left for the Parser to handle.
52
+ * @type {{}}
53
+ */
54
+ class Lexer {
55
+ _grammar: Grammar
56
+ _splitRegex?: RegExp
57
+ _escQuoteRegexCache = new Map<string, RegExp>()
58
+
59
+ constructor(grammar: Grammar) {
60
+ this._grammar = grammar
61
+ }
62
+
63
+ /**
64
+ * Splits a Jexl expression string into an array of expression elements.
65
+ * @param {string} str A Jexl expression string
66
+ * @returns {Array<string>} An array of substrings defining the functional
67
+ * elements of the expression.
68
+ */
69
+ getElements(str: string) {
70
+ const regex = this._getSplitRegex()
71
+ return str.split(regex).filter(Boolean)
72
+ }
73
+
74
+ /**
75
+ * Converts an array of expression elements into an array of tokens. Note that
76
+ * the resulting array may not equal the element array in length, as any
77
+ * elements that consist only of whitespace get appended to the previous
78
+ * token's "raw" property. For the structure of a token object, please see
79
+ * {@link Lexer#tokenize}.
80
+ * @param {Array<string>} elements An array of Jexl expression elements to be
81
+ * converted to tokens
82
+ * @returns {Array<{type, value, raw}>} an array of token objects.
83
+ */
84
+ getTokens(elements: string[]) {
85
+ const tokens: Token[] = []
86
+ let negate = false
87
+ for (let i = 0; i < elements.length; i++) {
88
+ if (this._isWhitespace(elements[i])) {
89
+ if (tokens.length) {
90
+ tokens[tokens.length - 1].raw += elements[i]
91
+ }
92
+ } else if (elements[i] === '-' && this._isNegative(tokens)) {
93
+ negate = true
94
+ } else {
95
+ if (negate) {
96
+ elements[i] = '-' + elements[i]
97
+ negate = false
98
+ }
99
+ tokens.push(this._createToken(elements[i]))
100
+ }
101
+ }
102
+ // Catch a - at the end of the string. Let the parser handle that issue.
103
+ if (negate) {
104
+ tokens.push(this._createToken('-'))
105
+ }
106
+ return tokens
107
+ }
108
+
109
+ /**
110
+ * Converts a Jexl string into an array of tokens. Each token is an object
111
+ * in the following format:
112
+ *
113
+ * {
114
+ * type: <string>,
115
+ * [name]: <string>,
116
+ * value: <boolean|number|string>,
117
+ * raw: <string>
118
+ * }
119
+ *
120
+ * Type is one of the following:
121
+ *
122
+ * literal, identifier, binaryOp, unaryOp
123
+ *
124
+ * OR, if the token is a control character its type is the name of the element
125
+ * defined in the Grammar.
126
+ *
127
+ * Name appears only if the token is a control string found in
128
+ * {@link grammar#elements}, and is set to the name of the element.
129
+ *
130
+ * Value is the value of the token in the correct type (boolean or numeric as
131
+ * appropriate). Raw is the string representation of this value taken directly
132
+ * from the expression string, including any trailing spaces.
133
+ * @param {string} str The Jexl string to be tokenized
134
+ * @returns {Array<{type, value, raw}>} an array of token objects.
135
+ * @throws {Error} if the provided string contains an invalid token.
136
+ */
137
+ tokenize(str: string) {
138
+ const elements = this.getElements(str)
139
+ return this.getTokens(elements)
140
+ }
141
+
142
+ /**
143
+ * Creates a new token object from an element of a Jexl string. See
144
+ * {@link Lexer#tokenize} for a description of the token object.
145
+ * @param {string} element The element from which a token should be made
146
+ * @returns {{value: number|boolean|string, [name]: string, type: string,
147
+ * raw: string}} a token object describing the provided element.
148
+ * @throws {Error} if the provided string is not a valid expression element.
149
+ * @private
150
+ */
151
+ _createToken(element: string): Token {
152
+ const token: Token = {
153
+ type: 'literal',
154
+ value: element,
155
+ raw: element
156
+ }
157
+ if (element.startsWith('`')) {
158
+ token.type = 'templateString'
159
+ token.value = this._parseTemplateString(element)
160
+ return token
161
+ } else if (element.startsWith('"') || element.startsWith("'")) {
162
+ token.value = this._unquote(element)
163
+ } else if (numericRegex.exec(element)) {
164
+ token.value = parseFloat(element)
165
+ } else if (element === 'true' || element === 'false') {
166
+ token.value = element === 'true'
167
+ } else if (this._grammar.elements[element]) {
168
+ token.type = this._grammar.elements[element].type
169
+ } else if (identRegex.exec(element)) {
170
+ token.type = 'identifier'
171
+ } else {
172
+ throw new Error(`Invalid expression token: ${element}`)
173
+ }
174
+ return token
175
+ }
176
+
177
+ /**
178
+ * Escapes a string so that it can be treated as a string literal within a
179
+ * regular expression.
180
+ * @param {string} str The string to be escaped
181
+ * @returns {string} the RegExp-escaped string.
182
+ * @see https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions
183
+ * @private
184
+ */
185
+ _escapeRegExp(str: string) {
186
+ str = str.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)
187
+ if (identRegex.exec(str)) {
188
+ str = String.raw`\b` + str + String.raw`\b`
189
+ }
190
+ return str
191
+ }
192
+
193
+ /**
194
+ * Gets a RegEx object appropriate for splitting a Jexl string into its core
195
+ * elements.
196
+ * @returns {RegExp} An element-splitting RegExp object
197
+ * @private
198
+ */
199
+ _getSplitRegex() {
200
+ if (!this._splitRegex) {
201
+ // Sort by most characters to least, then regex escape each
202
+ const elemArray = Object.keys(this._grammar.elements)
203
+ .sort((a, b) => {
204
+ return b.length - a.length
205
+ })
206
+ .map((elem) => {
207
+ return this._escapeRegExp(elem)
208
+ })
209
+ this._splitRegex = new RegExp(
210
+ '(' +
211
+ [
212
+ preOpRegexElems.join('|'),
213
+ elemArray.join('|'),
214
+ postOpRegexElems.join('|')
215
+ ].join('|') +
216
+ ')'
217
+ )
218
+ }
219
+ return this._splitRegex
220
+ }
221
+
222
+ /**
223
+ * Determines whether the addition of a '-' token should be interpreted as a
224
+ * negative symbol for an upcoming number, given an array of tokens already
225
+ * processed.
226
+ * @param {Array<Object>} tokens An array of tokens already processed
227
+ * @returns {boolean} true if adding a '-' should be considered a negative
228
+ * symbol; false otherwise
229
+ * @private
230
+ */
231
+ _isNegative(tokens: Token[]) {
232
+ if (!tokens.length) {
233
+ return true
234
+ }
235
+ return minusNegatesAfter.has(tokens[tokens.length - 1].type)
236
+ }
237
+
238
+ /**
239
+ * A utility function to determine if a string consists of only space
240
+ * characters.
241
+ * @param {string} str A string to be tested
242
+ * @returns {boolean} true if the string is empty or consists of only spaces;
243
+ * false otherwise.
244
+ * @private
245
+ */
246
+ _isWhitespace(str: string) {
247
+ return !!whitespaceRegex.exec(str)
248
+ }
249
+
250
+ /**
251
+ * Removes the beginning and trailing quotes from a string, unescapes any
252
+ * escaped quotes on its interior, and unescapes any escaped escape
253
+ * characters. Note that this function is not defensive; it assumes that the
254
+ * provided string is not empty, and that its first and last characters are
255
+ * actually quotes.
256
+ * @param {string} str A string whose first and last characters are quotes
257
+ * @returns {string} a string with the surrounding quotes stripped and escapes
258
+ * properly processed.
259
+ * @private
260
+ */
261
+ _unquote(str: string) {
262
+ const quote = str[0]
263
+ let escQuoteRegex = this._escQuoteRegexCache.get(quote)
264
+ if (!escQuoteRegex) {
265
+ escQuoteRegex = new RegExp('\\\\' + quote, 'g')
266
+ this._escQuoteRegexCache.set(quote, escQuoteRegex)
267
+ }
268
+ return str
269
+ .slice(1, -1)
270
+ .replace(escQuoteRegex, quote)
271
+ .replace(escEscRegex, '\\')
272
+ }
273
+
274
+ _parseTemplateString(str: string) {
275
+ const parts: { type: 'static' | 'interpolation'; value: string }[] = []
276
+ let current = 1
277
+ let staticStart = 1
278
+
279
+ while (current < str.length - 1) {
280
+ if (str[current] === '\\') {
281
+ current += 2
282
+ continue
283
+ }
284
+
285
+ if (str[current] === '$' && str[current + 1] === '{') {
286
+ if (current > staticStart) {
287
+ parts.push({
288
+ type: 'static',
289
+ value: str.slice(staticStart, current)
290
+ })
291
+ }
292
+
293
+ let braceDepth = 1
294
+ const interpStart = current + 2
295
+ current += 2
296
+
297
+ while (current < str.length && braceDepth > 0) {
298
+ if (str[current] === '\\') {
299
+ current += 2
300
+ continue
301
+ }
302
+ if (str[current] === '{') {
303
+ braceDepth++
304
+ }
305
+ if (str[current] === '}') {
306
+ braceDepth--
307
+ }
308
+ current++
309
+ }
310
+
311
+ if (braceDepth !== 0) {
312
+ throw new Error(`Unclosed interpolation in template string: ${str}`)
313
+ }
314
+
315
+ parts.push({
316
+ type: 'interpolation',
317
+ value: str.slice(interpStart, current - 1)
318
+ })
319
+
320
+ staticStart = current
321
+ } else {
322
+ current++
323
+ }
324
+ }
325
+
326
+ if (current > staticStart) {
327
+ parts.push({
328
+ type: 'static',
329
+ value: str.slice(staticStart, current)
330
+ })
331
+ }
332
+
333
+ return parts
334
+ }
335
+
336
+ _unescapeTemplateString(str: string) {
337
+ return str
338
+ .replaceAll('\\`', '`')
339
+ .replaceAll(String.raw`\$`, '$')
340
+ .replaceAll('\\\\', '\\')
341
+ }
342
+ }
343
+
344
+ export default Lexer
@@ -0,0 +1,86 @@
1
+ /*
2
+ * Jexl
3
+ * Copyright 2020 Tom Shawver
4
+ */
5
+
6
+ class PromiseSync<T = unknown> {
7
+ value?: T
8
+ error?: unknown
9
+
10
+ constructor(fn: (resolve: (val: T) => void, reject: (error: unknown) => void) => void) {
11
+ fn(this._resolve.bind(this), this._reject.bind(this))
12
+ }
13
+
14
+ catch<TResult = never>(rejected: (error: unknown) => TResult): PromiseSync<T | TResult> {
15
+ if (this.error) {
16
+ try {
17
+ this._resolve(rejected(this.error) as any)
18
+ } catch (e) {
19
+ this._reject(e)
20
+ }
21
+ }
22
+ return this as any
23
+ }
24
+
25
+ // eslint-disable-next-line unicorn/no-thenable
26
+ then<TResult1 = T, TResult2 = never>(
27
+ resolved: (val: T) => TResult1,
28
+ rejected?: (error: unknown) => TResult2
29
+ ): PromiseSync<TResult1 | TResult2> {
30
+ if (!this.error) {
31
+ try {
32
+ this._resolve(resolved(this.value as T) as any)
33
+ } catch (e) {
34
+ this._reject(e)
35
+ }
36
+ }
37
+ if (rejected) {this.catch(rejected)}
38
+ return this as any
39
+ }
40
+
41
+ _reject(error: unknown) {
42
+ this.value = undefined
43
+ this.error = error
44
+ }
45
+
46
+ _resolve(val: any) {
47
+ if (val instanceof PromiseSync) {
48
+ if (val.error) {
49
+ this._reject(val.error)
50
+ } else {
51
+ this._resolve(val.value)
52
+ }
53
+ } else {
54
+ this.value = val
55
+ this.error = undefined
56
+ }
57
+ }
58
+
59
+ static all<T>(vals: (PromiseSync<T> | T)[]): PromiseSync<T[]> {
60
+ return new PromiseSync((resolve) => {
61
+ const resolved = vals.map((val) => {
62
+ while (val instanceof PromiseSync) {
63
+ if (val.error) {
64
+ if (val.error instanceof Error) {
65
+ throw val.error
66
+ }
67
+ throw new Error(typeof val.error === 'string' ? val.error : JSON.stringify(val.error))
68
+ }
69
+ val = val.value as T
70
+ }
71
+ return val
72
+ })
73
+ resolve(resolved)
74
+ })
75
+ }
76
+
77
+ static resolve<T>(val?: T): PromiseSync<T> {
78
+ return new PromiseSync<T>((resolve) => { resolve(val as T) })
79
+ }
80
+
81
+ static reject(error: unknown): PromiseSync<never> {
82
+ return new PromiseSync((_, reject) => { reject(error) })
83
+ }
84
+ }
85
+
86
+ export default PromiseSync
@@ -0,0 +1,153 @@
1
+ /*
2
+ * Jexl
3
+ * Copyright 2020 Tom Shawver
4
+ */
5
+
6
+ import * as handlers from './handlers.ts'
7
+
8
+ import type { AstNode } from '../types.ts'
9
+
10
+ interface Grammar {
11
+ elements: Record<string, any>
12
+ [key: string]: any
13
+ }
14
+
15
+ // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
16
+ type PromiseConstructor = typeof Promise | any
17
+
18
+ /**
19
+ * The Evaluator takes a Jexl expression tree as generated by the
20
+ * {@link Parser} and calculates its value within a given context. The
21
+ * collection of transforms, context, and a relative context to be used as the
22
+ * root for relative identifiers, are all specific to an Evaluator instance.
23
+ * When any of these things change, a new instance is required. However, a
24
+ * single instance can be used to simultaneously evaluate many different
25
+ * expressions, and does not have to be reinstantiated for each.
26
+ * @param {{}} grammar A grammar object against which to evaluate the expression
27
+ * tree
28
+ * @param {{}} [context] A map of variable keys to their values. This will be
29
+ * accessed to resolve the value of each non-relative identifier. Any
30
+ * Promise values will be passed to the expression as their resolved
31
+ * value.
32
+ * @param {{}|Array<{}|Array>} [relativeContext] A map or array to be accessed
33
+ * to resolve the value of a relative identifier.
34
+ * @param {function} promise A constructor for the Promise class to be used;
35
+ * probably either Promise or PromiseSync.
36
+ */
37
+ class Evaluator {
38
+ _grammar: Grammar
39
+ _context: any
40
+ _relContext: any
41
+ Promise: PromiseConstructor
42
+
43
+ constructor(
44
+ grammar: Grammar,
45
+ context?: any,
46
+ relativeContext?: any,
47
+ promise: PromiseConstructor = Promise
48
+ ) {
49
+ this._grammar = grammar
50
+ this._context = context || {}
51
+ this._relContext = relativeContext || this._context
52
+ this.Promise = promise
53
+ }
54
+
55
+ /**
56
+ * Evaluates an expression tree within the configured context.
57
+ * @param {{}} ast An expression tree object
58
+ * @returns {Promise<*>} resolves with the resulting value of the expression.
59
+ */
60
+ eval(ast: AstNode) {
61
+ return this.Promise.resolve().then(() => {
62
+ return (handlers as any)[ast.type].call(this, ast)
63
+ })
64
+ }
65
+
66
+ /**
67
+ * Simultaneously evaluates each expression within an array, and delivers the
68
+ * response as an array with the resulting values at the same indexes as their
69
+ * originating expressions.
70
+ * @param {Array<string>} arr An array of expression strings to be evaluated
71
+ * @returns {Promise<Array<{}>>} resolves with the result array
72
+ */
73
+ evalArray(arr: AstNode[]) {
74
+ return this.Promise.all(arr.map((elem: AstNode) => this.eval(elem)))
75
+ }
76
+
77
+ /**
78
+ * Simultaneously evaluates each expression within a map, and delivers the
79
+ * response as a map with the same keys, but with the evaluated result for each
80
+ * as their value.
81
+ * @param {{}} map A map of expression names to expression trees to be
82
+ * evaluated
83
+ * @returns {Promise<{}>} resolves with the result map.
84
+ */
85
+ evalMap(map: Record<string, AstNode>) {
86
+ const entries = Object.entries(map)
87
+ const promises = entries.map(([_, ast]) => this.eval(ast))
88
+ return this.Promise.all(promises).then((vals: any[]) =>
89
+ Object.fromEntries(entries.map(([key], idx) => [key, vals[idx]]))
90
+ )
91
+ }
92
+
93
+ /**
94
+ * Applies a filter expression with relative identifier elements to a subject.
95
+ * The intent is for the subject to be an array of subjects that will be
96
+ * individually used as the relative context against the provided expression
97
+ * tree. Only the elements whose expressions result in a truthy value will be
98
+ * included in the resulting array.
99
+ *
100
+ * If the subject is not an array of values, it will be converted to a single-
101
+ * element array before running the filter.
102
+ * @param {*} subject The value to be filtered usually an array. If this value is
103
+ * not an array, it will be converted to an array with this value as the
104
+ * only element.
105
+ * @param {{}} expr The expression tree to run against each subject. If the
106
+ * tree evaluates to a truthy result, then the value will be included in
107
+ * the returned array otherwise, it will be eliminated.
108
+ * @returns {Promise<Array>} resolves with an array of values that passed the
109
+ * expression filter.
110
+ * @private
111
+ */
112
+ _filterRelative(subject: any, expr: AstNode) {
113
+ const arr = Array.isArray(subject)
114
+ ? subject
115
+ : subject == null
116
+ ? []
117
+ : [subject]
118
+
119
+ const promises = arr.map((elem) =>
120
+ new Evaluator(this._grammar, this._context, elem, this.Promise).eval(expr)
121
+ )
122
+
123
+ return this.Promise.all(promises).then((values: any[]) =>
124
+ arr.filter((_, idx) => values[idx])
125
+ )
126
+ }
127
+
128
+ /**
129
+ * Applies a static filter expression to a subject value. If the filter
130
+ * expression evaluates to boolean true, the subject is returned if false,
131
+ * undefined.
132
+ *
133
+ * For any other resulting value of the expression, this function will attempt
134
+ * to respond with the property at that name or index of the subject.
135
+ * @param {*} subject The value to be filtered. Usually an Array (for which
136
+ * the expression would generally resolve to a numeric index) or an
137
+ * Object (for which the expression would generally resolve to a string
138
+ * indicating a property name)
139
+ * @param {{}} expr The expression tree to run against the subject
140
+ * @returns {Promise<*>} resolves with the value of the drill-down.
141
+ * @private
142
+ */
143
+ _filterStatic(subject: any, expr: AstNode) {
144
+ return this.eval(expr).then((res: any) => {
145
+ if (typeof res === 'boolean') {
146
+ return res ? subject : undefined
147
+ }
148
+ return subject?.[res]
149
+ })
150
+ }
151
+ }
152
+
153
+ export default Evaluator