@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.
- package/README.md +26 -2
- package/dist/Expression.d.ts +19 -7
- package/dist/Expression.js +32 -11
- package/dist/Expression.js.map +1 -1
- package/dist/Jexl.d.ts +13 -12
- package/dist/Jexl.js +31 -30
- package/dist/Jexl.js.map +1 -1
- package/dist/Lexer.d.ts +9 -9
- package/dist/Lexer.js +61 -24
- package/dist/Lexer.js.map +1 -1
- package/dist/evaluator/Evaluator.d.ts +20 -34
- package/dist/evaluator/Evaluator.js +19 -64
- package/dist/evaluator/Evaluator.js.map +1 -1
- package/dist/evaluator/compile.d.ts +27 -0
- package/dist/evaluator/compile.js +279 -0
- package/dist/evaluator/compile.js.map +1 -0
- package/dist/grammar.d.ts +29 -9
- package/dist/grammar.js +23 -7
- package/dist/grammar.js.map +1 -1
- package/dist/parser/Parser.d.ts +7 -8
- package/dist/parser/Parser.js +46 -14
- package/dist/parser/Parser.js.map +1 -1
- package/dist/parser/handlers.d.ts +8 -8
- package/dist/parser/handlers.js +84 -51
- package/dist/parser/handlers.js.map +1 -1
- package/dist/parser/states.d.ts +2 -2
- package/dist/parser/states.js +19 -40
- package/dist/parser/states.js.map +1 -1
- package/dist/types.d.ts +18 -7
- package/esm/Expression.d.ts +19 -7
- package/esm/Expression.js +32 -11
- package/esm/Expression.js.map +1 -1
- package/esm/Jexl.d.ts +13 -12
- package/esm/Jexl.js +31 -30
- package/esm/Jexl.js.map +1 -1
- package/esm/Lexer.d.ts +9 -9
- package/esm/Lexer.js +61 -24
- package/esm/Lexer.js.map +1 -1
- package/esm/evaluator/Evaluator.d.ts +20 -34
- package/esm/evaluator/Evaluator.js +19 -31
- package/esm/evaluator/Evaluator.js.map +1 -1
- package/esm/evaluator/compile.d.ts +27 -0
- package/esm/evaluator/compile.js +276 -0
- package/esm/evaluator/compile.js.map +1 -0
- package/esm/grammar.d.ts +29 -9
- package/esm/grammar.js +22 -7
- package/esm/grammar.js.map +1 -1
- package/esm/parser/Parser.d.ts +7 -8
- package/esm/parser/Parser.js +46 -14
- package/esm/parser/Parser.js.map +1 -1
- package/esm/parser/handlers.d.ts +8 -8
- package/esm/parser/handlers.js +81 -50
- package/esm/parser/handlers.js.map +1 -1
- package/esm/parser/states.d.ts +2 -2
- package/esm/parser/states.js +19 -40
- package/esm/parser/states.js.map +1 -1
- package/esm/types.d.ts +18 -7
- package/package.json +25 -28
- package/src/Expression.ts +31 -17
- package/src/Jexl.ts +59 -21
- package/src/Lexer.ts +63 -33
- package/src/evaluator/Evaluator.ts +27 -44
- package/src/evaluator/compile.ts +317 -0
- package/src/grammar.ts +69 -21
- package/src/parser/Parser.ts +50 -28
- package/src/parser/handlers.ts +122 -76
- package/src/parser/states.ts +22 -42
- package/src/types.ts +22 -7
- package/CHANGELOG.md +0 -249
- package/dist/evaluator/handlers.d.ts +0 -112
- package/dist/evaluator/handlers.js +0 -208
- package/dist/evaluator/handlers.js.map +0 -1
- package/esm/evaluator/handlers.d.ts +0 -112
- package/esm/evaluator/handlers.js +0 -194
- package/esm/evaluator/handlers.js.map +0 -1
- 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:
|
|
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'
|
|
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
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "A fork of the jexl lang for jbrowse",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"
|
|
6
|
+
"main": "dist/index.js",
|
|
7
7
|
"exports": {
|
|
8
|
-
"import":
|
|
9
|
-
|
|
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": "
|
|
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/
|
|
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
|
-
"
|
|
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.
|
|
56
|
-
"@vitest/coverage-v8": "^4.
|
|
57
|
-
"eslint": "^10.
|
|
58
|
-
"eslint-plugin-import": "^
|
|
59
|
-
"eslint-plugin-unicorn": "^
|
|
60
|
-
"prettier": "^3.8.
|
|
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": "^
|
|
63
|
-
"typescript-eslint": "^8.
|
|
64
|
-
"vitest": "^4.
|
|
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
|
-
|
|
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
|
|
36
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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:
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
117
|
+
addUnaryOp(operator: string, fn: (right: JexlValue) => JexlValue) {
|
|
91
118
|
this._addGrammarElement(operator, {
|
|
92
119
|
type: 'unaryOp',
|
|
93
|
-
|
|
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:
|
|
176
|
+
expr(strs: TemplateStringsArray, ...args: JexlValue[]) {
|
|
150
177
|
let exprStr = ''
|
|
151
|
-
for (
|
|
152
|
-
exprStr +=
|
|
178
|
+
for (const [idx, str] of strs.entries()) {
|
|
179
|
+
exprStr += str
|
|
153
180
|
if (idx < args.length) {
|
|
154
|
-
|
|
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
|
|
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:
|
|
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 {
|
|
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 (
|
|
90
|
-
if (this._isWhitespace(
|
|
91
|
-
|
|
92
|
-
|
|
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 (
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
negate = false
|
|
112
|
+
tokens.push(unaryMinusToken())
|
|
100
113
|
}
|
|
101
|
-
|
|
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
|
|
170
|
-
token.type = this._grammar.elements[element]
|
|
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]
|
|
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
|
|
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
|
-
.
|
|
273
|
-
.
|
|
292
|
+
.replaceAll(escQuoteRegex[quote], quote)
|
|
293
|
+
.replaceAll(escEscRegex, '\\')
|
|
274
294
|
}
|
|
275
295
|
|
|
276
296
|
_parseTemplateString(str: string) {
|
|
277
|
-
const parts:
|
|
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
|
-
|
|
320
|
+
const char = str[current]
|
|
321
|
+
if (char === '\\') {
|
|
301
322
|
current += 2
|
|
302
323
|
continue
|
|
303
324
|
}
|
|
304
|
-
|
|
305
|
-
|
|
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 (
|
|
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
|
|
6
|
+
import { compileAst } from './compile.ts'
|
|
7
7
|
|
|
8
|
-
import type {
|
|
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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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:
|
|
33
|
-
_relContext:
|
|
34
|
-
|
|
35
|
-
constructor(
|
|
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
|
-
*
|
|
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 (
|
|
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
|
|