@jbrowse/jexl 3.0.1 → 3.1.0

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 (76) hide show
  1. package/README.md +24 -25
  2. package/dist/Expression.d.ts +16 -6
  3. package/dist/Expression.js +34 -8
  4. package/dist/Expression.js.map +1 -1
  5. package/dist/Jexl.d.ts +13 -12
  6. package/dist/Jexl.js +31 -30
  7. package/dist/Jexl.js.map +1 -1
  8. package/dist/Lexer.d.ts +9 -8
  9. package/dist/Lexer.js +61 -19
  10. package/dist/Lexer.js.map +1 -1
  11. package/dist/evaluator/Evaluator.d.ts +20 -34
  12. package/dist/evaluator/Evaluator.js +19 -64
  13. package/dist/evaluator/Evaluator.js.map +1 -1
  14. package/dist/evaluator/compile.d.ts +27 -0
  15. package/dist/evaluator/compile.js +261 -0
  16. package/dist/evaluator/compile.js.map +1 -0
  17. package/dist/grammar.d.ts +29 -9
  18. package/dist/grammar.js +19 -3
  19. package/dist/grammar.js.map +1 -1
  20. package/dist/parser/Parser.d.ts +5 -7
  21. package/dist/parser/Parser.js +33 -5
  22. package/dist/parser/Parser.js.map +1 -1
  23. package/dist/parser/handlers.d.ts +0 -6
  24. package/dist/parser/handlers.js +54 -46
  25. package/dist/parser/handlers.js.map +1 -1
  26. package/dist/parser/states.d.ts +2 -2
  27. package/dist/parser/states.js +5 -21
  28. package/dist/parser/states.js.map +1 -1
  29. package/dist/types.d.ts +18 -7
  30. package/esm/Expression.d.ts +16 -6
  31. package/esm/Expression.js +34 -8
  32. package/esm/Expression.js.map +1 -1
  33. package/esm/Jexl.d.ts +13 -12
  34. package/esm/Jexl.js +31 -30
  35. package/esm/Jexl.js.map +1 -1
  36. package/esm/Lexer.d.ts +9 -8
  37. package/esm/Lexer.js +61 -19
  38. package/esm/Lexer.js.map +1 -1
  39. package/esm/evaluator/Evaluator.d.ts +20 -34
  40. package/esm/evaluator/Evaluator.js +19 -31
  41. package/esm/evaluator/Evaluator.js.map +1 -1
  42. package/esm/evaluator/compile.d.ts +27 -0
  43. package/esm/evaluator/compile.js +258 -0
  44. package/esm/evaluator/compile.js.map +1 -0
  45. package/esm/grammar.d.ts +29 -9
  46. package/esm/grammar.js +18 -3
  47. package/esm/grammar.js.map +1 -1
  48. package/esm/parser/Parser.d.ts +5 -7
  49. package/esm/parser/Parser.js +33 -5
  50. package/esm/parser/Parser.js.map +1 -1
  51. package/esm/parser/handlers.d.ts +0 -6
  52. package/esm/parser/handlers.js +54 -45
  53. package/esm/parser/handlers.js.map +1 -1
  54. package/esm/parser/states.d.ts +2 -2
  55. package/esm/parser/states.js +5 -21
  56. package/esm/parser/states.js.map +1 -1
  57. package/esm/types.d.ts +18 -7
  58. package/package.json +27 -29
  59. package/src/Expression.ts +33 -13
  60. package/src/Jexl.ts +59 -21
  61. package/src/Lexer.ts +63 -28
  62. package/src/evaluator/Evaluator.ts +27 -44
  63. package/src/evaluator/compile.ts +310 -0
  64. package/src/grammar.ts +66 -17
  65. package/src/parser/Parser.ts +35 -21
  66. package/src/parser/handlers.ts +96 -73
  67. package/src/parser/states.ts +7 -23
  68. package/src/types.ts +22 -7
  69. package/CHANGELOG.md +0 -249
  70. package/dist/evaluator/handlers.d.ts +0 -112
  71. package/dist/evaluator/handlers.js +0 -208
  72. package/dist/evaluator/handlers.js.map +0 -1
  73. package/esm/evaluator/handlers.d.ts +0 -112
  74. package/esm/evaluator/handlers.js +0 -194
  75. package/esm/evaluator/handlers.js.map +0 -1
  76. package/src/evaluator/handlers.ts +0 -212
package/src/Jexl.ts CHANGED
@@ -4,12 +4,16 @@
4
4
  */
5
5
 
6
6
  import Expression from './Expression.ts'
7
+ import Lexer from './Lexer.ts'
7
8
  import { getGrammar } from './grammar.ts'
8
9
 
9
- interface Grammar {
10
- elements: Record<string, any>
11
- functions: Record<string, (...args: any[]) => any>
12
- }
10
+ import type {
11
+ BinaryOpEval,
12
+ BinaryOpEvalOnDemand,
13
+ Grammar,
14
+ GrammarElement
15
+ } from './grammar.ts'
16
+ import type { JexlValue } from './types.ts'
13
17
 
14
18
  /**
15
19
  * Jexl is the Javascript Expression Language, capable of parsing and
@@ -19,9 +23,13 @@ interface Grammar {
19
23
  */
20
24
  class Jexl {
21
25
  _grammar: Grammar
26
+ // shared by every Expression this instance creates, so that the expensive
27
+ // element-splitting regex is built once rather than per compile
28
+ _lexer: Lexer
22
29
 
23
30
  constructor() {
24
31
  this._grammar = getGrammar()
32
+ this._lexer = new Lexer(this._grammar)
25
33
  this.expr = this.expr.bind(this)
26
34
  }
27
35
 
@@ -47,14 +55,33 @@ class Jexl {
47
55
  addBinaryOp(
48
56
  operator: string,
49
57
  precedence: number,
50
- fn: (left: any, right: any) => any,
58
+ fn: BinaryOpEval,
59
+ manualEval?: false
60
+ ): void
61
+ addBinaryOp(
62
+ operator: string,
63
+ precedence: number,
64
+ fn: BinaryOpEvalOnDemand,
65
+ manualEval: true
66
+ ): void
67
+ addBinaryOp(
68
+ operator: string,
69
+ precedence: number,
70
+ fn: BinaryOpEval | BinaryOpEvalOnDemand,
51
71
  manualEval?: boolean
52
72
  ) {
53
- this._addGrammarElement(operator, {
54
- type: 'binaryOp',
55
- precedence: precedence,
56
- [manualEval ? 'evalOnDemand' : 'eval']: fn
57
- })
73
+ // the overloads above pair `fn` with `manualEval`; the implementation
74
+ // signature can't express that correlation, hence the assertions
75
+ this._addGrammarElement(
76
+ operator,
77
+ manualEval
78
+ ? {
79
+ type: 'binaryOp',
80
+ precedence,
81
+ evalOnDemand: fn as BinaryOpEvalOnDemand
82
+ }
83
+ : { type: 'binaryOp', precedence, eval: fn as BinaryOpEval }
84
+ )
58
85
  }
59
86
 
60
87
  /**
@@ -65,7 +92,7 @@ class Jexl {
65
92
  * expression function is invoked. It will be provided with each argument
66
93
  * supplied in the expression, in the same order.
67
94
  */
68
- addFunction(name: string, fn: (...args: any[]) => any) {
95
+ addFunction(name: string, fn: (...args: JexlValue[]) => JexlValue) {
69
96
  this._grammar.functions[name] = fn
70
97
  }
71
98
 
@@ -75,7 +102,7 @@ class Jexl {
75
102
  * function counterpart.
76
103
  * @param {{}} map A map of expression function names to javascript functions
77
104
  */
78
- addFunctions(map: Record<string, (...args: any[]) => any>) {
105
+ addFunctions(map: Record<string, (...args: JexlValue[]) => JexlValue>) {
79
106
  Object.assign(this._grammar.functions, map)
80
107
  }
81
108
 
@@ -87,10 +114,10 @@ class Jexl {
87
114
  * will be called with one argument: the literal value to the right of the
88
115
  * operator. It should return the resulting value.
89
116
  */
90
- addUnaryOp(operator: string, fn: (right: any) => any) {
117
+ addUnaryOp(operator: string, fn: (right: JexlValue) => JexlValue) {
91
118
  this._addGrammarElement(operator, {
92
119
  type: 'unaryOp',
93
- weight: Infinity,
120
+ precedence: Infinity,
94
121
  eval: fn
95
122
  })
96
123
  }
@@ -115,7 +142,7 @@ class Jexl {
115
142
  * @returns {Expression} The Expression object representing the given string
116
143
  */
117
144
  createExpression(expression: string) {
118
- return new Expression(this._grammar, expression)
145
+ return new Expression(this._grammar, expression, this._lexer)
119
146
  }
120
147
 
121
148
  /**
@@ -146,12 +173,21 @@ class Jexl {
146
173
  * @param {Array<string>} strs
147
174
  * @param {...any} args
148
175
  */
149
- expr(strs: TemplateStringsArray, ...args: any[]) {
176
+ expr(strs: TemplateStringsArray, ...args: JexlValue[]) {
150
177
  let exprStr = ''
151
- for (let idx = 0; idx < strs.length; idx++) {
152
- exprStr += strs[idx]
178
+ for (const [idx, str] of strs.entries()) {
179
+ exprStr += str
153
180
  if (idx < args.length) {
154
- exprStr += args[idx]
181
+ const arg = args[idx]
182
+ if (
183
+ typeof arg === 'string' ||
184
+ typeof arg === 'number' ||
185
+ typeof arg === 'boolean'
186
+ ) {
187
+ exprStr += String(arg)
188
+ } else if (arg != null) {
189
+ exprStr += JSON.stringify(arg)
190
+ }
155
191
  }
156
192
  }
157
193
  return this.createExpression(exprStr)
@@ -163,8 +199,9 @@ class Jexl {
163
199
  */
164
200
  removeOp(operator: string) {
165
201
  const elem = this._grammar.elements[operator]
166
- if (elem && (elem.type === 'binaryOp' || elem.type === 'unaryOp')) {
202
+ if (elem?.type === 'binaryOp' || elem?.type === 'unaryOp') {
167
203
  Reflect.deleteProperty(this._grammar.elements, operator)
204
+ this._lexer._clearCache()
168
205
  }
169
206
  }
170
207
 
@@ -175,8 +212,9 @@ class Jexl {
175
212
  * grammar element
176
213
  * @private
177
214
  */
178
- _addGrammarElement(str: string, obj: any) {
215
+ _addGrammarElement(str: string, obj: GrammarElement) {
179
216
  this._grammar.elements[str] = obj
217
+ this._lexer._clearCache()
180
218
  }
181
219
  }
182
220
 
package/src/Lexer.ts CHANGED
@@ -3,12 +3,13 @@
3
3
  * Copyright 2020 Tom Shawver
4
4
  */
5
5
 
6
- import type { Token } from './types.ts'
6
+ import type { Grammar } from './grammar.ts'
7
+ import type { TemplatePart, Token } from './types.ts'
7
8
 
8
9
  const numericRegex = /^-?(?:(?:[0-9]*\.[0-9]+)|[0-9]+)$/
9
10
  const identRegex =
10
11
  /^[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 escEscRegex = /\\\\/g
12
13
  const whitespaceRegex = /^\s*$/
13
14
  const preOpRegexElems = [
14
15
  // Template strings
@@ -28,19 +29,22 @@ const postOpRegexElems = [
28
29
  // Numerics (without negative symbol)
29
30
  String.raw`(?:(?:[0-9]*\.[0-9]+)|[0-9]+)`
30
31
  ]
32
+ const unaryMinusToken = (): Token => ({
33
+ type: 'unaryOp',
34
+ value: '-',
35
+ raw: '-'
36
+ })
31
37
  const minusNegatesAfter = new Set([
32
38
  'binaryOp',
33
39
  'unaryOp',
34
40
  'openParen',
35
41
  'openBracket',
36
42
  'question',
37
- 'colon'
43
+ 'colon',
44
+ 'comma',
45
+ 'semicolon'
38
46
  ])
39
47
 
40
- interface Grammar {
41
- elements: Record<string, any>
42
- }
43
-
44
48
  /**
45
49
  * Lexer is a collection of stateless, statically-accessed functions for the
46
50
  * lexical parsing of a Jexl string. Its responsibility is to identify the
@@ -60,6 +64,15 @@ class Lexer {
60
64
  this._grammar = grammar
61
65
  }
62
66
 
67
+ /**
68
+ * Discards the memoized split regex, so that it is rebuilt on the next
69
+ * tokenize. Must be called whenever the grammar's elements change, since the
70
+ * regex is derived from their keys.
71
+ */
72
+ _clearCache() {
73
+ this._splitRegex = undefined
74
+ }
75
+
63
76
  /**
64
77
  * Splits a Jexl expression string into an array of expression elements.
65
78
  * @param {string} str A Jexl expression string
@@ -84,19 +97,31 @@ class Lexer {
84
97
  getTokens(elements: string[]) {
85
98
  const tokens: Token[] = []
86
99
  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]
100
+ for (const element of elements) {
101
+ if (this._isWhitespace(element)) {
102
+ const last = tokens.at(-1)
103
+ if (last) {
104
+ last.raw += element
91
105
  }
92
- } else if (elements[i] === '-' && this._isNegative(tokens)) {
93
- negate = true
94
- } else {
106
+ } else if (element === '-' && this._isNegative(tokens)) {
107
+ // a second prefix minus in a row ("- -x"): emit the pending one as a
108
+ // unary operator so this one can negate whatever comes next
95
109
  if (negate) {
96
- elements[i] = '-' + elements[i]
97
- negate = false
110
+ tokens.push(unaryMinusToken())
111
+ }
112
+ negate = true
113
+ } else if (negate) {
114
+ negate = false
115
+ if (numericRegex.exec(element)) {
116
+ // fold the sign into the number, so "-1" stays a single literal
117
+ tokens.push(this._createToken('-' + element))
118
+ } else {
119
+ // anything else gets a standalone prefix operator, letting "-x",
120
+ // "-(a + b)" and "-foo.bar" negate a computed value
121
+ tokens.push(unaryMinusToken(), this._createToken(element))
98
122
  }
99
- tokens.push(this._createToken(elements[i]))
123
+ } else {
124
+ tokens.push(this._createToken(element))
100
125
  }
101
126
  }
102
127
  // Catch a - at the end of the string. Let the parser handle that issue.
@@ -164,8 +189,8 @@ class Lexer {
164
189
  token.value = parseFloat(element)
165
190
  } else if (element === 'true' || element === 'false') {
166
191
  token.value = element === 'true'
167
- } else if (this._grammar.elements[element]) {
168
- token.type = this._grammar.elements[element].type
192
+ } else if (Object.hasOwn(this._grammar.elements, element)) {
193
+ token.type = this._grammar.elements[element]!.type
169
194
  } else if (identRegex.exec(element)) {
170
195
  token.type = 'identifier'
171
196
  } else {
@@ -232,7 +257,7 @@ class Lexer {
232
257
  if (!tokens.length) {
233
258
  return true
234
259
  }
235
- return minusNegatesAfter.has(tokens[tokens.length - 1].type)
260
+ return minusNegatesAfter.has(tokens[tokens.length - 1]!.type)
236
261
  }
237
262
 
238
263
  /**
@@ -259,7 +284,7 @@ class Lexer {
259
284
  * @private
260
285
  */
261
286
  _unquote(str: string) {
262
- const quote = str[0]
287
+ const quote = str[0]!
263
288
  let escQuoteRegex = this._escQuoteRegexCache.get(quote)
264
289
  if (!escQuoteRegex) {
265
290
  escQuoteRegex = new RegExp('\\\\' + quote, 'g')
@@ -267,12 +292,12 @@ class Lexer {
267
292
  }
268
293
  return str
269
294
  .slice(1, -1)
270
- .replace(escQuoteRegex, quote)
271
- .replace(escEscRegex, '\\')
295
+ .replaceAll(escQuoteRegex, quote)
296
+ .replaceAll(escEscRegex, '\\')
272
297
  }
273
298
 
274
299
  _parseTemplateString(str: string) {
275
- const parts: { type: 'static' | 'interpolation'; value: string }[] = []
300
+ const parts: TemplatePart[] = []
276
301
  let current = 1
277
302
  let staticStart = 1
278
303
 
@@ -295,14 +320,24 @@ class Lexer {
295
320
  current += 2
296
321
 
297
322
  while (current < str.length && braceDepth > 0) {
298
- if (str[current] === '\\') {
323
+ const char = str[current]
324
+ if (char === '\\') {
299
325
  current += 2
300
326
  continue
301
327
  }
302
- if (str[current] === '{') {
303
- braceDepth++
328
+ // skip over string literals so that braces inside them, as in
329
+ // `${ f('}') }`, don't unbalance the depth count
330
+ if (char === '"' || char === "'") {
331
+ current++
332
+ while (current < str.length && str[current] !== char) {
333
+ current += str[current] === '\\' ? 2 : 1
334
+ }
335
+ current++
336
+ continue
304
337
  }
305
- if (str[current] === '}') {
338
+ if (char === '{') {
339
+ braceDepth++
340
+ } else if (char === '}') {
306
341
  braceDepth--
307
342
  }
308
343
  current++
@@ -3,23 +3,23 @@
3
3
  * Copyright 2020 Tom Shawver
4
4
  */
5
5
 
6
- import * as handlers from './handlers.ts'
6
+ import { compileAst } from './compile.ts'
7
7
 
8
- import type { AstNode } from '../types.ts'
9
-
10
- interface Grammar {
11
- elements: Record<string, any>
12
- [key: string]: any
13
- }
8
+ import type { Grammar } from '../grammar.ts'
9
+ import type { AstNode, JexlValue } from '../types.ts'
14
10
 
15
11
  /**
16
- * The Evaluator takes a Jexl expression tree as generated by the
17
- * {@link Parser} and calculates its value within a given context. The
18
- * collection of transforms, context, and a relative context to be used as the
19
- * root for relative identifiers, are all specific to an Evaluator instance.
20
- * When any of these things change, a new instance is required. However, a
21
- * single instance can be used to simultaneously evaluate many different
12
+ * The Evaluator carries everything an expression is evaluated against: the
13
+ * grammar, the context, and a relative context to be used as the root for
14
+ * relative identifiers. When any of these change, a new instance is required.
15
+ * However, a single instance can be used to evaluate many different
22
16
  * expressions, and does not have to be reinstantiated for each.
17
+ *
18
+ * The work of evaluation itself lives in {@link compileAst}, which lowers a
19
+ * tree to closures. Prefer compiling once and reusing the result — that is what
20
+ * {@link Expression} does — over calling {@link #eval}, which compiles the tree
21
+ * it is given every time.
22
+ *
23
23
  * @param {{}} grammar A grammar object against which to evaluate the expression
24
24
  * tree
25
25
  * @param {{}} [context] A map of variable keys to their values. This will be
@@ -29,46 +29,29 @@ interface Grammar {
29
29
  */
30
30
  class Evaluator {
31
31
  _grammar: Grammar
32
- _context: any
33
- _relContext: any
34
-
35
- constructor(grammar: Grammar, context?: any, relativeContext?: any) {
32
+ _context: Record<string, JexlValue>
33
+ _relContext: Record<string, JexlValue>
34
+
35
+ constructor(
36
+ grammar: Grammar,
37
+ context?: Record<string, JexlValue>,
38
+ relativeContext?: Record<string, JexlValue>
39
+ ) {
36
40
  this._grammar = grammar
37
41
  this._context = context || {}
38
42
  this._relContext = relativeContext || this._context
39
43
  }
40
44
 
41
45
  /**
42
- * Evaluates an expression tree within the configured context.
46
+ * Compiles and evaluates an expression tree within the configured context.
47
+ * Convenient for a one-shot evaluation; for repeated evaluation of the same
48
+ * tree, hold onto the closure from {@link compileAst} instead so the tree is
49
+ * only lowered once.
43
50
  * @param {{}} ast An expression tree object
44
51
  * @returns {*} the resulting value of the expression.
45
52
  */
46
- eval(ast: AstNode) {
47
- return (handlers as any)[ast.type].call(this, ast)
48
- }
49
-
50
- /**
51
- * Evaluates each expression within an array, and delivers the response as an
52
- * array with the resulting values at the same indexes as their originating
53
- * expressions.
54
- * @param {Array<string>} arr An array of expression strings to be evaluated
55
- * @returns {Array<{}>} the result array
56
- */
57
- evalArray(arr: AstNode[]) {
58
- return arr.map((elem: AstNode) => this.eval(elem))
59
- }
60
-
61
- /**
62
- * Evaluates each expression within a map, and delivers the response as a map
63
- * with the same keys, but with the evaluated result for each as their value.
64
- * @param {{}} map A map of expression names to expression trees to be
65
- * evaluated
66
- * @returns {{}} the result map.
67
- */
68
- evalMap(map: Record<string, AstNode>) {
69
- const entries = Object.entries(map)
70
- const vals = entries.map(([_, ast]) => this.eval(ast))
71
- return Object.fromEntries(entries.map(([key], idx) => [key, vals[idx]]))
53
+ eval(ast: AstNode): JexlValue {
54
+ return compileAst(ast, this._grammar)(this)
72
55
  }
73
56
  }
74
57