@jbrowse/jexl 5.0.1 → 6.0.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 +8 -3
- package/dist/Expression.d.ts +0 -12
- package/dist/Expression.js +0 -16
- package/dist/Expression.js.map +1 -1
- package/dist/Jexl.js +12 -16
- package/dist/Jexl.js.map +1 -1
- package/dist/Lexer.d.ts +12 -94
- package/dist/Lexer.js +41 -142
- package/dist/Lexer.js.map +1 -1
- package/dist/analyze.d.ts +2 -0
- package/dist/analyze.js +7 -7
- package/dist/analyze.js.map +1 -1
- package/dist/check.d.ts +0 -5
- package/dist/check.js +62 -240
- package/dist/check.js.map +1 -1
- package/dist/collections.d.ts +7 -0
- package/dist/collections.js +9 -2
- package/dist/collections.js.map +1 -1
- package/dist/conditions.d.ts +1 -1
- package/dist/conditions.js +28 -50
- package/dist/conditions.js.map +1 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +8 -0
- package/dist/errors.js.map +1 -1
- package/dist/evaluator/compile.js +37 -24
- package/dist/evaluator/compile.js.map +1 -1
- package/dist/grammar.d.ts +2 -2
- package/dist/grammar.js +1 -8
- package/dist/grammar.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/operators.d.ts +5 -0
- package/dist/operators.js +9 -5
- package/dist/operators.js.map +1 -1
- package/dist/parser/Parser.d.ts +11 -12
- package/dist/parser/Parser.js +4 -10
- package/dist/parser/Parser.js.map +1 -1
- package/dist/print.d.ts +9 -0
- package/dist/print.js +183 -0
- package/dist/print.js.map +1 -0
- package/dist/types.d.ts +18 -21
- package/esm/Expression.d.ts +0 -12
- package/esm/Expression.js +0 -16
- package/esm/Expression.js.map +1 -1
- package/esm/Jexl.js +12 -16
- package/esm/Jexl.js.map +1 -1
- package/esm/Lexer.d.ts +12 -94
- package/esm/Lexer.js +40 -142
- package/esm/Lexer.js.map +1 -1
- package/esm/analyze.d.ts +2 -0
- package/esm/analyze.js +6 -7
- package/esm/analyze.js.map +1 -1
- package/esm/check.d.ts +0 -5
- package/esm/check.js +29 -206
- package/esm/check.js.map +1 -1
- package/esm/collections.d.ts +7 -0
- package/esm/collections.js +8 -1
- package/esm/collections.js.map +1 -1
- package/esm/conditions.d.ts +1 -1
- package/esm/conditions.js +27 -49
- package/esm/conditions.js.map +1 -1
- package/esm/errors.d.ts +5 -0
- package/esm/errors.js +7 -0
- package/esm/errors.js.map +1 -1
- package/esm/evaluator/compile.js +37 -24
- package/esm/evaluator/compile.js.map +1 -1
- package/esm/grammar.d.ts +2 -2
- package/esm/grammar.js +1 -8
- package/esm/grammar.js.map +1 -1
- package/esm/index.d.ts +2 -1
- package/esm/index.js +2 -1
- package/esm/index.js.map +1 -1
- package/esm/operators.d.ts +5 -0
- package/esm/operators.js +8 -5
- package/esm/operators.js.map +1 -1
- package/esm/parser/Parser.d.ts +11 -12
- package/esm/parser/Parser.js +4 -8
- package/esm/parser/Parser.js.map +1 -1
- package/esm/print.d.ts +9 -0
- package/esm/print.js +177 -0
- package/esm/print.js.map +1 -0
- package/esm/types.d.ts +18 -21
- package/package.json +2 -1
- package/src/Expression.ts +0 -20
- package/src/Jexl.ts +13 -20
- package/src/Lexer.ts +42 -144
- package/src/analyze.ts +10 -10
- package/src/check.ts +48 -250
- package/src/collections.ts +11 -1
- package/src/conditions.ts +39 -62
- package/src/errors.ts +10 -0
- package/src/evaluator/compile.ts +49 -36
- package/src/grammar.ts +3 -9
- package/src/index.ts +2 -1
- package/src/operators.ts +10 -6
- package/src/parser/Parser.ts +13 -19
- package/src/print.ts +203 -0
- package/src/types.ts +19 -23
package/src/Lexer.ts
CHANGED
|
@@ -23,22 +23,21 @@ const wholeWord = (word: string) => `(?<!${identPart})${word}(?!${identPart})`
|
|
|
23
23
|
const numberPattern = String.raw`(?:(?:[0-9]*\.[0-9]+)|[0-9]+)(?:[eE][+-]?[0-9]+)?`
|
|
24
24
|
|
|
25
25
|
const numericRegex = new RegExp(`^-?${numberPattern}$`)
|
|
26
|
-
const identRegex = new RegExp(`^${identPattern}$`)
|
|
27
|
-
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
const
|
|
31
|
-
// the escapes a template string's static text recognizes. One pass, so the
|
|
32
|
-
// backslash an escaped backslash yields can't be re-read as the start of the
|
|
33
|
-
// escape that follows it
|
|
26
|
+
export const identRegex = new RegExp(`^${identPattern}$`)
|
|
27
|
+
// the escapes each kind of literal recognizes. One pass, so the backslash an
|
|
28
|
+
// escaped backslash yields can't be re-read as the start of the escape that
|
|
29
|
+
// follows it
|
|
30
|
+
const quoteEscRegex = { "'": /\\([\\'])/g, '"': /\\([\\"])/g }
|
|
34
31
|
const templateEscRegex = /\\([`$\\])/g
|
|
35
32
|
const whitespaceRegex = /^\s*$/
|
|
33
|
+
// a backslash and the character after it are a pair, so an escaped backslash
|
|
34
|
+
// before the closing quote can't escape that quote
|
|
35
|
+
const quoted = (quote: string) =>
|
|
36
|
+
String.raw`${quote}(?:\\[\s\S]|[^${quote}\\])*${quote}`
|
|
36
37
|
const preOpRegexElems = [
|
|
37
|
-
|
|
38
|
-
'
|
|
39
|
-
|
|
40
|
-
String.raw`'(?:(?:\\')|[^'])*'`,
|
|
41
|
-
String.raw`"(?:(?:\\")|[^"])*"`,
|
|
38
|
+
quoted('`'),
|
|
39
|
+
quoted("'"),
|
|
40
|
+
quoted('"'),
|
|
42
41
|
// Whitespace
|
|
43
42
|
String.raw`\s+`,
|
|
44
43
|
// ahead of the grammar's '.', so that '.5' is a number rather than a dot
|
|
@@ -59,8 +58,12 @@ const minusNegatesAfter = new Set([
|
|
|
59
58
|
'colon',
|
|
60
59
|
'comma',
|
|
61
60
|
'semicolon',
|
|
62
|
-
'arrow'
|
|
61
|
+
'arrow',
|
|
62
|
+
'assign'
|
|
63
63
|
])
|
|
64
|
+
// whether a `-` after this token negates what follows rather than subtracting
|
|
65
|
+
const negates = (last: Token | undefined) =>
|
|
66
|
+
!last || minusNegatesAfter.has(last.type)
|
|
64
67
|
|
|
65
68
|
/**
|
|
66
69
|
* Lexer handles the lexical parsing of a Jexl string. Its responsibility is to
|
|
@@ -91,26 +94,14 @@ class Lexer {
|
|
|
91
94
|
this._splitRegex = undefined
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
/**
|
|
95
|
-
* Splits a Jexl expression string into an array of expression elements.
|
|
96
|
-
* @param {string} str A Jexl expression string
|
|
97
|
-
* @returns {Array<string>} An array of substrings defining the functional
|
|
98
|
-
* elements of the expression.
|
|
99
|
-
*/
|
|
97
|
+
/** Splits a Jexl string into its elements: tokens and runs of whitespace. */
|
|
100
98
|
getElements(str: string) {
|
|
101
|
-
|
|
102
|
-
return str.split(regex).filter(Boolean)
|
|
99
|
+
return str.split(this._getSplitRegex()).filter(Boolean)
|
|
103
100
|
}
|
|
104
101
|
|
|
105
102
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* elements that consist only of whitespace get appended to the previous
|
|
109
|
-
* token's "raw" property. For the structure of a token object, please see
|
|
110
|
-
* {@link Lexer#tokenize}.
|
|
111
|
-
* @param {Array<string>} elements An array of Jexl expression elements to be
|
|
112
|
-
* converted to tokens
|
|
113
|
-
* @returns {Array<{type, value, raw}>} an array of token objects.
|
|
103
|
+
* The tokens for a list of elements. Whitespace makes no token of its own;
|
|
104
|
+
* it joins the `raw` of the token before it.
|
|
114
105
|
*/
|
|
115
106
|
getTokens(elements: string[]) {
|
|
116
107
|
const tokens: Token[] = []
|
|
@@ -121,12 +112,12 @@ class Lexer {
|
|
|
121
112
|
let pendingMinus: Token | undefined
|
|
122
113
|
let offset = 0
|
|
123
114
|
for (const element of elements) {
|
|
124
|
-
if (
|
|
115
|
+
if (whitespaceRegex.test(element)) {
|
|
125
116
|
const last = pendingMinus ?? tokens.at(-1)
|
|
126
117
|
if (last) {
|
|
127
118
|
last.raw += element
|
|
128
119
|
}
|
|
129
|
-
} else if (element === '-' &&
|
|
120
|
+
} else if (element === '-' && negates(tokens.at(-1))) {
|
|
130
121
|
// a second prefix minus in a row ("- -x"): emit the pending one as a
|
|
131
122
|
// unary operator so this one can negate whatever comes next
|
|
132
123
|
if (pendingMinus) {
|
|
@@ -134,7 +125,7 @@ class Lexer {
|
|
|
134
125
|
}
|
|
135
126
|
pendingMinus = unaryMinusToken()
|
|
136
127
|
} else if (pendingMinus) {
|
|
137
|
-
if (numericRegex.
|
|
128
|
+
if (numericRegex.test(element)) {
|
|
138
129
|
// fold the sign into the number, so "-1" stays a single literal
|
|
139
130
|
const token = this._createToken('-' + element, offset)
|
|
140
131
|
token.raw = pendingMinus.raw + element
|
|
@@ -158,47 +149,16 @@ class Lexer {
|
|
|
158
149
|
}
|
|
159
150
|
|
|
160
151
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* [name]: <string>,
|
|
167
|
-
* value: <boolean|number|string>,
|
|
168
|
-
* raw: <string>
|
|
169
|
-
* }
|
|
170
|
-
*
|
|
171
|
-
* Type is one of the following:
|
|
172
|
-
*
|
|
173
|
-
* literal, identifier, binaryOp, unaryOp
|
|
174
|
-
*
|
|
175
|
-
* OR, if the token is a control character its type is the name of the element
|
|
176
|
-
* defined in the Grammar.
|
|
177
|
-
*
|
|
178
|
-
* Name appears only if the token is a control string found in
|
|
179
|
-
* {@link grammar#elements}, and is set to the name of the element.
|
|
180
|
-
*
|
|
181
|
-
* Value is the value of the token in the correct type (boolean or numeric as
|
|
182
|
-
* appropriate). Raw is the string representation of this value taken directly
|
|
183
|
-
* from the expression string, including any trailing spaces.
|
|
184
|
-
* @param {string} str The Jexl string to be tokenized
|
|
185
|
-
* @returns {Array<{type, value, raw}>} an array of token objects.
|
|
186
|
-
* @throws {Error} if the provided string contains an invalid token.
|
|
152
|
+
* Splits a Jexl string into tokens. A token's `type` is `literal`,
|
|
153
|
+
* `templateString`, `identifier` or the type of the grammar element it
|
|
154
|
+
* spells; its `value` is a literal's value or the template's parts; its `raw`
|
|
155
|
+
* is its source text and any whitespace after it.
|
|
156
|
+
* @throws {JexlSyntaxError} for text that is no token
|
|
187
157
|
*/
|
|
188
158
|
tokenize(str: string) {
|
|
189
|
-
|
|
190
|
-
return this.getTokens(elements)
|
|
159
|
+
return this.getTokens(this.getElements(str))
|
|
191
160
|
}
|
|
192
161
|
|
|
193
|
-
/**
|
|
194
|
-
* Creates a new token object from an element of a Jexl string. See
|
|
195
|
-
* {@link Lexer#tokenize} for a description of the token object.
|
|
196
|
-
* @param {string} element The element from which a token should be made
|
|
197
|
-
* @returns {{value: number|boolean|string, [name]: string, type: string,
|
|
198
|
-
* raw: string}} a token object describing the provided element.
|
|
199
|
-
* @throws {Error} if the provided string is not a valid expression element.
|
|
200
|
-
* @private
|
|
201
|
-
*/
|
|
202
162
|
_createToken(element: string, offset = 0): Token {
|
|
203
163
|
const token: Token = {
|
|
204
164
|
type: 'literal',
|
|
@@ -211,7 +171,7 @@ class Lexer {
|
|
|
211
171
|
return token
|
|
212
172
|
} else if (element.startsWith('"') || element.startsWith("'")) {
|
|
213
173
|
token.value = this._unquote(element)
|
|
214
|
-
} else if (numericRegex.
|
|
174
|
+
} else if (numericRegex.test(element)) {
|
|
215
175
|
token.value = parseFloat(element)
|
|
216
176
|
} else if (element === 'true' || element === 'false') {
|
|
217
177
|
token.value = element === 'true'
|
|
@@ -219,7 +179,7 @@ class Lexer {
|
|
|
219
179
|
token.value = null
|
|
220
180
|
} else if (Object.hasOwn(this._grammar.elements, element)) {
|
|
221
181
|
token.type = this._grammar.elements[element]!.type
|
|
222
|
-
} else if (identRegex.
|
|
182
|
+
} else if (identRegex.test(element)) {
|
|
223
183
|
token.type = 'identifier'
|
|
224
184
|
} else {
|
|
225
185
|
throw new JexlSyntaxError(`Invalid expression token: ${element}`, offset)
|
|
@@ -227,92 +187,30 @@ class Lexer {
|
|
|
227
187
|
return token
|
|
228
188
|
}
|
|
229
189
|
|
|
230
|
-
/**
|
|
231
|
-
*
|
|
232
|
-
* regular expression. A word such as `in` also stops matching inside a
|
|
233
|
-
* longer name.
|
|
234
|
-
* @param {string} str The string to be escaped
|
|
235
|
-
* @returns {string} the RegExp-escaped string.
|
|
236
|
-
* @see https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions
|
|
237
|
-
* @private
|
|
238
|
-
*/
|
|
190
|
+
/** A grammar element's text as a regex. A word such as `in` also stops
|
|
191
|
+
* matching inside a longer name. */
|
|
239
192
|
_escapeRegExp(str: string) {
|
|
240
193
|
const escaped = str.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)
|
|
241
|
-
return identRegex.
|
|
194
|
+
return identRegex.test(str) ? wholeWord(escaped) : escaped
|
|
242
195
|
}
|
|
243
196
|
|
|
244
|
-
/**
|
|
245
|
-
* Gets a RegEx object appropriate for splitting a Jexl string into its core
|
|
246
|
-
* elements.
|
|
247
|
-
* @returns {RegExp} An element-splitting RegExp object
|
|
248
|
-
* @private
|
|
249
|
-
*/
|
|
250
197
|
_getSplitRegex() {
|
|
251
198
|
if (!this._splitRegex) {
|
|
252
|
-
//
|
|
253
|
-
const
|
|
254
|
-
.sort((a, b) =>
|
|
255
|
-
|
|
256
|
-
})
|
|
257
|
-
.map((elem) => {
|
|
258
|
-
return this._escapeRegExp(elem)
|
|
259
|
-
})
|
|
199
|
+
// longest first, so that `==` wins over `=`
|
|
200
|
+
const elements = Object.keys(this._grammar.elements)
|
|
201
|
+
.sort((a, b) => b.length - a.length)
|
|
202
|
+
.map((element) => this._escapeRegExp(element))
|
|
260
203
|
this._splitRegex = new RegExp(
|
|
261
|
-
|
|
262
|
-
[
|
|
263
|
-
preOpRegexElems.join('|'),
|
|
264
|
-
elemArray.join('|'),
|
|
265
|
-
postOpRegexElems.join('|')
|
|
266
|
-
].join('|') +
|
|
267
|
-
')'
|
|
204
|
+
`(${[...preOpRegexElems, ...elements, ...postOpRegexElems].join('|')})`
|
|
268
205
|
)
|
|
269
206
|
}
|
|
270
207
|
return this._splitRegex
|
|
271
208
|
}
|
|
272
209
|
|
|
273
|
-
/**
|
|
274
|
-
* Determines whether the addition of a '-' token should be interpreted as a
|
|
275
|
-
* negative symbol for an upcoming number, given an array of tokens already
|
|
276
|
-
* processed.
|
|
277
|
-
* @param {Array<Object>} tokens An array of tokens already processed
|
|
278
|
-
* @returns {boolean} true if adding a '-' should be considered a negative
|
|
279
|
-
* symbol; false otherwise
|
|
280
|
-
* @private
|
|
281
|
-
*/
|
|
282
|
-
_isNegative(tokens: Token[]) {
|
|
283
|
-
const last = tokens.at(-1)
|
|
284
|
-
return !last || minusNegatesAfter.has(last.type)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* A utility function to determine if a string consists of only space
|
|
289
|
-
* characters.
|
|
290
|
-
* @param {string} str A string to be tested
|
|
291
|
-
* @returns {boolean} true if the string is empty or consists of only spaces;
|
|
292
|
-
* false otherwise.
|
|
293
|
-
* @private
|
|
294
|
-
*/
|
|
295
|
-
_isWhitespace(str: string) {
|
|
296
|
-
return !!whitespaceRegex.exec(str)
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Removes the beginning and trailing quotes from a string, unescapes any
|
|
301
|
-
* escaped quotes on its interior, and unescapes any escaped escape
|
|
302
|
-
* characters. Note that this function is not defensive; it assumes that the
|
|
303
|
-
* provided string is not empty, and that its first and last characters are
|
|
304
|
-
* actually quotes.
|
|
305
|
-
* @param {string} str A string whose first and last characters are quotes
|
|
306
|
-
* @returns {string} a string with the surrounding quotes stripped and escapes
|
|
307
|
-
* properly processed.
|
|
308
|
-
* @private
|
|
309
|
-
*/
|
|
210
|
+
/** A quoted string literal's text, unquoted and unescaped. */
|
|
310
211
|
_unquote(str: string) {
|
|
311
212
|
const quote = str.startsWith('"') ? '"' : "'"
|
|
312
|
-
return str
|
|
313
|
-
.slice(1, -1)
|
|
314
|
-
.replaceAll(escQuoteRegex[quote], quote)
|
|
315
|
-
.replaceAll(escEscRegex, '\\')
|
|
213
|
+
return str.slice(1, -1).replaceAll(quoteEscRegex[quote], '$1')
|
|
316
214
|
}
|
|
317
215
|
|
|
318
216
|
_parseTemplateString(str: string, offset = 0) {
|
package/src/analyze.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* Copyright 2020 Tom Shawver
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import { unknownNode } from './errors.ts'
|
|
7
|
+
|
|
8
|
+
import type { AstNode, FunctionCall } from './types.ts'
|
|
7
9
|
|
|
8
10
|
export type PathKey = string | number
|
|
9
11
|
|
|
@@ -89,8 +91,8 @@ function literalKey(node: AstNode) {
|
|
|
89
91
|
return typeof value === 'boolean' ? String(value) : (value ?? undefined)
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
/** A literal's value, or a template's with no interpolation. */
|
|
95
|
+
export function literalValue(node: AstNode) {
|
|
94
96
|
if (node.type === 'Literal') {
|
|
95
97
|
return node.value
|
|
96
98
|
}
|
|
@@ -113,8 +115,7 @@ function extend(read: Read, keys: readonly PathKey[], dynamic = false): Read {
|
|
|
113
115
|
: { root: read.root, path }
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
function isBarePath(
|
|
117
|
-
const node = ast as AstNodeUnion
|
|
118
|
+
function isBarePath(node: AstNode): boolean {
|
|
118
119
|
if (node.type === 'Identifier') {
|
|
119
120
|
return !node.from || isBarePath(node.from)
|
|
120
121
|
}
|
|
@@ -238,8 +239,7 @@ export function analyze(
|
|
|
238
239
|
return subject.map((read) => extend(read, keys, dynamic))
|
|
239
240
|
}
|
|
240
241
|
|
|
241
|
-
function walk(
|
|
242
|
-
const node = ast as AstNodeUnion
|
|
242
|
+
function walk(node: AstNode, scope: Scope): Read[] {
|
|
243
243
|
switch (node.type) {
|
|
244
244
|
case 'Literal': {
|
|
245
245
|
return []
|
|
@@ -289,7 +289,7 @@ export function analyze(
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
case 'UnaryExpression': {
|
|
292
|
-
use(node.right
|
|
292
|
+
use(node.right, scope)
|
|
293
293
|
return []
|
|
294
294
|
}
|
|
295
295
|
|
|
@@ -328,7 +328,7 @@ export function analyze(
|
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
case 'AssignmentExpression': {
|
|
331
|
-
const value = walk(node.right
|
|
331
|
+
const value = walk(node.right, scope)
|
|
332
332
|
const name = node.left.value
|
|
333
333
|
const previous = scope.get(name)
|
|
334
334
|
if (previous && !previous.used) {
|
|
@@ -349,7 +349,7 @@ export function analyze(
|
|
|
349
349
|
}
|
|
350
350
|
|
|
351
351
|
default: {
|
|
352
|
-
|
|
352
|
+
return unknownNode(node)
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
}
|