@jbrowse/jexl 3.0.2 → 3.2.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 +26 -2
  2. package/dist/Expression.d.ts +19 -7
  3. package/dist/Expression.js +32 -11
  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 -9
  9. package/dist/Lexer.js +61 -24
  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 +279 -0
  16. package/dist/evaluator/compile.js.map +1 -0
  17. package/dist/grammar.d.ts +29 -9
  18. package/dist/grammar.js +23 -7
  19. package/dist/grammar.js.map +1 -1
  20. package/dist/parser/Parser.d.ts +7 -8
  21. package/dist/parser/Parser.js +46 -14
  22. package/dist/parser/Parser.js.map +1 -1
  23. package/dist/parser/handlers.d.ts +8 -8
  24. package/dist/parser/handlers.js +84 -51
  25. package/dist/parser/handlers.js.map +1 -1
  26. package/dist/parser/states.d.ts +2 -2
  27. package/dist/parser/states.js +19 -40
  28. package/dist/parser/states.js.map +1 -1
  29. package/dist/types.d.ts +18 -7
  30. package/esm/Expression.d.ts +19 -7
  31. package/esm/Expression.js +32 -11
  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 -9
  37. package/esm/Lexer.js +61 -24
  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 +276 -0
  44. package/esm/evaluator/compile.js.map +1 -0
  45. package/esm/grammar.d.ts +29 -9
  46. package/esm/grammar.js +22 -7
  47. package/esm/grammar.js.map +1 -1
  48. package/esm/parser/Parser.d.ts +7 -8
  49. package/esm/parser/Parser.js +46 -14
  50. package/esm/parser/Parser.js.map +1 -1
  51. package/esm/parser/handlers.d.ts +8 -8
  52. package/esm/parser/handlers.js +81 -50
  53. package/esm/parser/handlers.js.map +1 -1
  54. package/esm/parser/states.d.ts +2 -2
  55. package/esm/parser/states.js +19 -40
  56. package/esm/parser/states.js.map +1 -1
  57. package/esm/types.d.ts +18 -7
  58. package/package.json +25 -28
  59. package/src/Expression.ts +31 -17
  60. package/src/Jexl.ts +59 -21
  61. package/src/Lexer.ts +63 -33
  62. package/src/evaluator/Evaluator.ts +27 -44
  63. package/src/evaluator/compile.ts +317 -0
  64. package/src/grammar.ts +69 -21
  65. package/src/parser/Parser.ts +50 -28
  66. package/src/parser/handlers.ts +122 -76
  67. package/src/parser/states.ts +22 -42
  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/esm/types.d.ts CHANGED
@@ -1,11 +1,19 @@
1
+ export interface TemplatePart {
2
+ type: 'static' | 'interpolation';
3
+ value: string;
4
+ }
5
+ export type JexlValue = string | number | boolean | null | undefined | JexlValue[] | {
6
+ [key: string]: JexlValue;
7
+ };
1
8
  export interface Token {
2
9
  type: string;
3
- value: any;
10
+ value: string | number | boolean | TemplatePart[];
4
11
  raw: string;
5
12
  }
6
13
  export interface AstNode {
7
14
  type: string;
8
15
  _parent?: AstNode;
16
+ right?: AstNode;
9
17
  }
10
18
  export interface Literal extends AstNode {
11
19
  type: 'Literal';
@@ -36,18 +44,22 @@ export interface ObjectLiteral extends AstNode {
36
44
  type: 'ObjectLiteral';
37
45
  value: Record<string, AstNode>;
38
46
  }
47
+ export type TemplateLiteralPart = {
48
+ type: 'static';
49
+ value: string;
50
+ } | {
51
+ type: 'expression';
52
+ value: AstNode;
53
+ };
39
54
  export interface TemplateLiteral extends AstNode {
40
55
  type: 'TemplateLiteral';
41
- parts: {
42
- type: 'static' | 'expression';
43
- value: string | AstNode;
44
- }[];
56
+ parts: TemplateLiteralPart[];
45
57
  }
46
58
  export interface FunctionCall extends AstNode {
47
59
  type: 'FunctionCall';
48
60
  name: string;
49
61
  args: AstNode[];
50
- pool: 'functions' | 'transforms';
62
+ pool: 'functions';
51
63
  }
52
64
  export interface FilterExpression extends AstNode {
53
65
  type: 'FilterExpression';
@@ -69,7 +81,6 @@ export interface AssignmentExpression extends AstNode {
69
81
  type: 'AssignmentExpression';
70
82
  operator: '=';
71
83
  left: Identifier;
72
- right: AstNode;
73
84
  }
74
85
  export type AstNodeUnion = Literal | Identifier | BinaryExpression | UnaryExpression | ArrayLiteral | ObjectLiteral | TemplateLiteral | FunctionCall | FilterExpression | ConditionalExpression | SequenceExpression | AssignmentExpression;
75
86
  export type NodeByType<T extends AstNodeUnion['type']> = Extract<AstNodeUnion, {
package/package.json CHANGED
@@ -1,16 +1,12 @@
1
1
  {
2
2
  "name": "@jbrowse/jexl",
3
- "version": "3.0.2",
3
+ "version": "3.2.0",
4
4
  "description": "A fork of the jexl lang for jbrowse",
5
5
  "type": "module",
6
- "types": "./dist/index.d.ts",
6
+ "main": "dist/index.js",
7
7
  "exports": {
8
- "import": {
9
- "import": "./esm/index.js"
10
- },
11
- "require": {
12
- "require": "./dist/index.js"
13
- }
8
+ "import": "./esm/index.js",
9
+ "require": "./dist/index.js"
14
10
  },
15
11
  "files": [
16
12
  "dist",
@@ -18,20 +14,24 @@
18
14
  "src"
19
15
  ],
20
16
  "scripts": {
21
- "format": "prettier --write .",
22
- "lint": "eslint --report-unused-disable-directives --max-warnings 0",
23
17
  "test": "vitest",
18
+ "coverage": "pnpm test --coverage",
24
19
  "clean": "rimraf dist esm",
20
+ "format": "prettier --write .",
21
+ "format:check": "prettier --check .",
22
+ "lint": "eslint --report-unused-disable-directives --max-warnings 0",
23
+ "typecheck": "tsc --noEmit -p tsconfig.lint.json",
24
+ "prebuild": "pnpm clean",
25
25
  "build:esm": "tsc --outDir esm",
26
- "build:es5": "tsc --module commonjs --outDir dist",
27
- "build": "yarn build:esm && yarn build:es5",
28
- "prebuild": "yarn clean",
26
+ "build:es5": "tsc --module commonjs --moduleResolution bundler --outDir dist",
29
27
  "postbuild:es5": "echo '{\"type\": \"commonjs\"}' > dist/package.json",
30
- "preversion": "yarn lint && yarn test --run && yarn build"
28
+ "preversion": "pnpm lint && pnpm format:check && pnpm typecheck && pnpm test --run && pnpm build",
29
+ "postversion": "git push --follow-tags",
30
+ "build": "pnpm build:esm && pnpm build:es5"
31
31
  },
32
32
  "repository": {
33
33
  "type": "git",
34
- "url": "https://github.com/TomFrost/jexl"
34
+ "url": "https://github.com/GMOD/jexl.git"
35
35
  },
36
36
  "keywords": [
37
37
  "JSON",
@@ -46,22 +46,19 @@
46
46
  ],
47
47
  "author": "Tom Shawver <tom@frosteddesign.com>",
48
48
  "license": "MIT",
49
- "bugs": {
50
- "url": "https://github.com/TomFrost/jexl/issues"
51
- },
52
- "homepage": "https://github.com/TomFrost/jexl",
49
+ "packageManager": "pnpm@11.20.0",
53
50
  "devDependencies": {
54
51
  "@eslint/js": "^10.0.1",
55
- "@types/node": "^25.3.3",
56
- "@vitest/coverage-v8": "^4.0.18",
57
- "eslint": "^10.0.2",
58
- "eslint-plugin-import": "^2.32.0",
59
- "eslint-plugin-unicorn": "^63.0.0",
60
- "prettier": "^3.8.1",
52
+ "@types/node": "^25.6.0",
53
+ "@vitest/coverage-v8": "^4.1.5",
54
+ "eslint": "^10.2.1",
55
+ "eslint-plugin-import-x": "^4.16.2",
56
+ "eslint-plugin-unicorn": "^64.0.0",
57
+ "prettier": "^3.8.3",
61
58
  "rimraf": "^6.1.3",
62
- "typescript": "^5.9.3",
63
- "typescript-eslint": "^8.56.1",
64
- "vitest": "^4.0.18"
59
+ "typescript": "^6.0.3",
60
+ "typescript-eslint": "^8.59.0",
61
+ "vitest": "^4.1.5"
65
62
  },
66
63
  "publishConfig": {
67
64
  "access": "public"
package/src/Expression.ts CHANGED
@@ -5,24 +5,38 @@
5
5
 
6
6
  import Lexer from './Lexer.ts'
7
7
  import Evaluator from './evaluator/Evaluator.ts'
8
+ import { compileAst } from './evaluator/compile.ts'
8
9
  import Parser from './parser/Parser.ts'
9
10
 
11
+ import type { CompiledNode } from './evaluator/compile.ts'
12
+ import type { Grammar } from './grammar.ts'
10
13
  import type { AstNode } from './types.ts'
11
14
 
12
- interface Grammar {
13
- elements: Record<string, any>
14
- [key: string]: any
15
- }
16
-
17
15
  class Expression {
18
16
  _grammar: Grammar
19
17
  _exprStr: string
18
+ /** The parsed tree, exposed for inspection. Legitimately null for an
19
+ * expression with no tokens, which is why compilation is tracked by
20
+ * `_compiled` rather than by this being set. */
20
21
  _ast: AstNode | null
22
+ _lexer: Lexer
23
+ _compiled = false
24
+ // the AST lowered to closures, so repeated eval() calls skip node dispatch
25
+ _fn: CompiledNode | null = null
21
26
 
22
- constructor(grammar: Grammar, exprStr: string) {
27
+ /**
28
+ * @param {{}} grammar The grammar to compile and evaluate against
29
+ * @param {string} exprStr The Jexl expression string
30
+ * @param {Lexer} [lexer] A Lexer to reuse. Lexers memoize the regex used to
31
+ * split expressions, which is expensive to build, so passing a shared
32
+ * one avoids paying that cost per expression. It must be invalidated
33
+ * via {@link Lexer#_clearCache} whenever the grammar's elements change.
34
+ */
35
+ constructor(grammar: Grammar, exprStr: string, lexer?: Lexer) {
23
36
  this._grammar = grammar
24
37
  this._exprStr = exprStr
25
38
  this._ast = null
39
+ this._lexer = lexer ?? new Lexer(grammar)
26
40
  }
27
41
 
28
42
  /**
@@ -32,11 +46,13 @@ class Expression {
32
46
  * @returns {Expression} this Expression instance, for convenience
33
47
  */
34
48
  compile() {
35
- const lexer = new Lexer(this._grammar)
36
- const parser = new Parser(this._grammar, lexer)
37
- const tokens = lexer.tokenize(this._exprStr)
49
+ const parser = new Parser(this._grammar, this._lexer)
50
+ const tokens = this._lexer.tokenize(this._exprStr)
38
51
  parser.addTokens(tokens)
39
52
  this._ast = parser.complete()
53
+ // lower the tree to closures once, here, so that eval() is just a call
54
+ this._fn = this._ast ? compileAst(this._ast, this._grammar) : null
55
+ this._compiled = true
40
56
  return this
41
57
  }
42
58
 
@@ -48,16 +64,14 @@ class Expression {
48
64
  * @throws {*} on error
49
65
  */
50
66
  eval(context = {}) {
51
- const ast = this._getAst()
52
- const evaluator = new Evaluator(this._grammar, context)
53
- return evaluator.eval(ast!)
54
- }
55
-
56
- _getAst() {
57
- if (!this._ast) {
67
+ if (!this._compiled) {
58
68
  this.compile()
59
69
  }
60
- return this._ast
70
+ // an expression with no tokens (empty or whitespace-only) has no AST
71
+ if (!this._fn) {
72
+ return undefined
73
+ }
74
+ return this._fn(new Evaluator(this._grammar, context))
61
75
  }
62
76
  }
63
77
 
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,16 @@
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
13
+ // a string literal opens with one of exactly two quote characters, so the two
14
+ // unescaping regexes can just be named rather than built and cached per quote
15
+ const escQuoteRegex = { "'": /\\'/g, '"': /\\"/g }
12
16
  const whitespaceRegex = /^\s*$/
13
17
  const preOpRegexElems = [
14
18
  // Template strings
@@ -28,6 +32,11 @@ const postOpRegexElems = [
28
32
  // Numerics (without negative symbol)
29
33
  String.raw`(?:(?:[0-9]*\.[0-9]+)|[0-9]+)`
30
34
  ]
35
+ const unaryMinusToken = (): Token => ({
36
+ type: 'unaryOp',
37
+ value: '-',
38
+ raw: '-'
39
+ })
31
40
  const minusNegatesAfter = new Set([
32
41
  'binaryOp',
33
42
  'unaryOp',
@@ -39,10 +48,6 @@ const minusNegatesAfter = new Set([
39
48
  'semicolon'
40
49
  ])
41
50
 
42
- interface Grammar {
43
- elements: Record<string, any>
44
- }
45
-
46
51
  /**
47
52
  * Lexer is a collection of stateless, statically-accessed functions for the
48
53
  * lexical parsing of a Jexl string. Its responsibility is to identify the
@@ -56,12 +61,20 @@ interface Grammar {
56
61
  class Lexer {
57
62
  _grammar: Grammar
58
63
  _splitRegex?: RegExp
59
- _escQuoteRegexCache = new Map<string, RegExp>()
60
64
 
61
65
  constructor(grammar: Grammar) {
62
66
  this._grammar = grammar
63
67
  }
64
68
 
69
+ /**
70
+ * Discards the memoized split regex, so that it is rebuilt on the next
71
+ * tokenize. Must be called whenever the grammar's elements change, since the
72
+ * regex is derived from their keys.
73
+ */
74
+ _clearCache() {
75
+ this._splitRegex = undefined
76
+ }
77
+
65
78
  /**
66
79
  * Splits a Jexl expression string into an array of expression elements.
67
80
  * @param {string} str A Jexl expression string
@@ -86,19 +99,31 @@ class Lexer {
86
99
  getTokens(elements: string[]) {
87
100
  const tokens: Token[] = []
88
101
  let negate = false
89
- for (let i = 0; i < elements.length; i++) {
90
- if (this._isWhitespace(elements[i])) {
91
- if (tokens.length) {
92
- tokens[tokens.length - 1].raw += elements[i]
102
+ for (const element of elements) {
103
+ if (this._isWhitespace(element)) {
104
+ const last = tokens.at(-1)
105
+ if (last) {
106
+ last.raw += element
93
107
  }
94
- } else if (elements[i] === '-' && this._isNegative(tokens)) {
95
- negate = true
96
- } else {
108
+ } else if (element === '-' && this._isNegative(tokens)) {
109
+ // a second prefix minus in a row ("- -x"): emit the pending one as a
110
+ // unary operator so this one can negate whatever comes next
97
111
  if (negate) {
98
- elements[i] = '-' + elements[i]
99
- negate = false
112
+ tokens.push(unaryMinusToken())
100
113
  }
101
- tokens.push(this._createToken(elements[i]))
114
+ negate = true
115
+ } else if (negate) {
116
+ negate = false
117
+ if (numericRegex.exec(element)) {
118
+ // fold the sign into the number, so "-1" stays a single literal
119
+ tokens.push(this._createToken('-' + element))
120
+ } else {
121
+ // anything else gets a standalone prefix operator, letting "-x",
122
+ // "-(a + b)" and "-foo.bar" negate a computed value
123
+ tokens.push(unaryMinusToken(), this._createToken(element))
124
+ }
125
+ } else {
126
+ tokens.push(this._createToken(element))
102
127
  }
103
128
  }
104
129
  // Catch a - at the end of the string. Let the parser handle that issue.
@@ -166,8 +191,8 @@ class Lexer {
166
191
  token.value = parseFloat(element)
167
192
  } else if (element === 'true' || element === 'false') {
168
193
  token.value = element === 'true'
169
- } else if (this._grammar.elements[element]) {
170
- token.type = this._grammar.elements[element].type
194
+ } else if (Object.hasOwn(this._grammar.elements, element)) {
195
+ token.type = this._grammar.elements[element]!.type
171
196
  } else if (identRegex.exec(element)) {
172
197
  token.type = 'identifier'
173
198
  } else {
@@ -234,7 +259,7 @@ class Lexer {
234
259
  if (!tokens.length) {
235
260
  return true
236
261
  }
237
- return minusNegatesAfter.has(tokens[tokens.length - 1].type)
262
+ return minusNegatesAfter.has(tokens[tokens.length - 1]!.type)
238
263
  }
239
264
 
240
265
  /**
@@ -261,20 +286,15 @@ class Lexer {
261
286
  * @private
262
287
  */
263
288
  _unquote(str: string) {
264
- const quote = str[0]
265
- let escQuoteRegex = this._escQuoteRegexCache.get(quote)
266
- if (!escQuoteRegex) {
267
- escQuoteRegex = new RegExp('\\\\' + quote, 'g')
268
- this._escQuoteRegexCache.set(quote, escQuoteRegex)
269
- }
289
+ const quote = str.startsWith('"') ? '"' : "'"
270
290
  return str
271
291
  .slice(1, -1)
272
- .replace(escQuoteRegex, quote)
273
- .replace(escEscRegex, '\\')
292
+ .replaceAll(escQuoteRegex[quote], quote)
293
+ .replaceAll(escEscRegex, '\\')
274
294
  }
275
295
 
276
296
  _parseTemplateString(str: string) {
277
- const parts: { type: 'static' | 'interpolation'; value: string }[] = []
297
+ const parts: TemplatePart[] = []
278
298
  let current = 1
279
299
  let staticStart = 1
280
300
 
@@ -297,14 +317,24 @@ class Lexer {
297
317
  current += 2
298
318
 
299
319
  while (current < str.length && braceDepth > 0) {
300
- if (str[current] === '\\') {
320
+ const char = str[current]
321
+ if (char === '\\') {
301
322
  current += 2
302
323
  continue
303
324
  }
304
- if (str[current] === '{') {
305
- braceDepth++
325
+ // skip over string literals so that braces inside them, as in
326
+ // `${ f('}') }`, don't unbalance the depth count
327
+ if (char === '"' || char === "'") {
328
+ current++
329
+ while (current < str.length && str[current] !== char) {
330
+ current += str[current] === '\\' ? 2 : 1
331
+ }
332
+ current++
333
+ continue
306
334
  }
307
- if (str[current] === '}') {
335
+ if (char === '{') {
336
+ braceDepth++
337
+ } else if (char === '}') {
308
338
  braceDepth--
309
339
  }
310
340
  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