@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.
Files changed (99) hide show
  1. package/README.md +8 -3
  2. package/dist/Expression.d.ts +0 -12
  3. package/dist/Expression.js +0 -16
  4. package/dist/Expression.js.map +1 -1
  5. package/dist/Jexl.js +12 -16
  6. package/dist/Jexl.js.map +1 -1
  7. package/dist/Lexer.d.ts +12 -94
  8. package/dist/Lexer.js +41 -142
  9. package/dist/Lexer.js.map +1 -1
  10. package/dist/analyze.d.ts +2 -0
  11. package/dist/analyze.js +7 -7
  12. package/dist/analyze.js.map +1 -1
  13. package/dist/check.d.ts +0 -5
  14. package/dist/check.js +62 -240
  15. package/dist/check.js.map +1 -1
  16. package/dist/collections.d.ts +7 -0
  17. package/dist/collections.js +9 -2
  18. package/dist/collections.js.map +1 -1
  19. package/dist/conditions.d.ts +1 -1
  20. package/dist/conditions.js +28 -50
  21. package/dist/conditions.js.map +1 -1
  22. package/dist/errors.d.ts +5 -0
  23. package/dist/errors.js +8 -0
  24. package/dist/errors.js.map +1 -1
  25. package/dist/evaluator/compile.js +37 -24
  26. package/dist/evaluator/compile.js.map +1 -1
  27. package/dist/grammar.d.ts +2 -2
  28. package/dist/grammar.js +1 -8
  29. package/dist/grammar.js.map +1 -1
  30. package/dist/index.d.ts +2 -1
  31. package/dist/index.js +3 -2
  32. package/dist/index.js.map +1 -1
  33. package/dist/operators.d.ts +5 -0
  34. package/dist/operators.js +9 -5
  35. package/dist/operators.js.map +1 -1
  36. package/dist/parser/Parser.d.ts +11 -12
  37. package/dist/parser/Parser.js +4 -10
  38. package/dist/parser/Parser.js.map +1 -1
  39. package/dist/print.d.ts +9 -0
  40. package/dist/print.js +183 -0
  41. package/dist/print.js.map +1 -0
  42. package/dist/types.d.ts +18 -21
  43. package/esm/Expression.d.ts +0 -12
  44. package/esm/Expression.js +0 -16
  45. package/esm/Expression.js.map +1 -1
  46. package/esm/Jexl.js +12 -16
  47. package/esm/Jexl.js.map +1 -1
  48. package/esm/Lexer.d.ts +12 -94
  49. package/esm/Lexer.js +40 -142
  50. package/esm/Lexer.js.map +1 -1
  51. package/esm/analyze.d.ts +2 -0
  52. package/esm/analyze.js +6 -7
  53. package/esm/analyze.js.map +1 -1
  54. package/esm/check.d.ts +0 -5
  55. package/esm/check.js +29 -206
  56. package/esm/check.js.map +1 -1
  57. package/esm/collections.d.ts +7 -0
  58. package/esm/collections.js +8 -1
  59. package/esm/collections.js.map +1 -1
  60. package/esm/conditions.d.ts +1 -1
  61. package/esm/conditions.js +27 -49
  62. package/esm/conditions.js.map +1 -1
  63. package/esm/errors.d.ts +5 -0
  64. package/esm/errors.js +7 -0
  65. package/esm/errors.js.map +1 -1
  66. package/esm/evaluator/compile.js +37 -24
  67. package/esm/evaluator/compile.js.map +1 -1
  68. package/esm/grammar.d.ts +2 -2
  69. package/esm/grammar.js +1 -8
  70. package/esm/grammar.js.map +1 -1
  71. package/esm/index.d.ts +2 -1
  72. package/esm/index.js +2 -1
  73. package/esm/index.js.map +1 -1
  74. package/esm/operators.d.ts +5 -0
  75. package/esm/operators.js +8 -5
  76. package/esm/operators.js.map +1 -1
  77. package/esm/parser/Parser.d.ts +11 -12
  78. package/esm/parser/Parser.js +4 -8
  79. package/esm/parser/Parser.js.map +1 -1
  80. package/esm/print.d.ts +9 -0
  81. package/esm/print.js +177 -0
  82. package/esm/print.js.map +1 -0
  83. package/esm/types.d.ts +18 -21
  84. package/package.json +2 -1
  85. package/src/Expression.ts +0 -20
  86. package/src/Jexl.ts +13 -20
  87. package/src/Lexer.ts +42 -144
  88. package/src/analyze.ts +10 -10
  89. package/src/check.ts +48 -250
  90. package/src/collections.ts +11 -1
  91. package/src/conditions.ts +39 -62
  92. package/src/errors.ts +10 -0
  93. package/src/evaluator/compile.ts +49 -36
  94. package/src/grammar.ts +3 -9
  95. package/src/index.ts +2 -1
  96. package/src/operators.ts +10 -6
  97. package/src/parser/Parser.ts +13 -19
  98. package/src/print.ts +203 -0
  99. 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
- const escEscRegex = /\\\\/g
28
- // a string literal opens with one of exactly two quote characters, so the two
29
- // unescaping regexes can just be named rather than built and cached per quote
30
- const escQuoteRegex = { "'": /\\'/g, '"': /\\"/g }
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
- // Template strings
38
- '`(?:[^`\\\\]|\\\\.)*`',
39
- // Strings
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
- const regex = this._getSplitRegex()
102
- return str.split(regex).filter(Boolean)
99
+ return str.split(this._getSplitRegex()).filter(Boolean)
103
100
  }
104
101
 
105
102
  /**
106
- * Converts an array of expression elements into an array of tokens. Note that
107
- * the resulting array may not equal the element array in length, as any
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 (this._isWhitespace(element)) {
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 === '-' && this._isNegative(tokens)) {
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.exec(element)) {
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
- * Converts a Jexl string into an array of tokens. Each token is an object
162
- * in the following format:
163
- *
164
- * {
165
- * type: <string>,
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
- const elements = this.getElements(str)
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.exec(element)) {
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.exec(element)) {
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
- * Escapes a string so that it can be treated as a string literal within a
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.exec(str) ? wholeWord(escaped) : escaped
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
- // Sort by most characters to least, then regex escape each
253
- const elemArray = Object.keys(this._grammar.elements)
254
- .sort((a, b) => {
255
- return b.length - a.length
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 type { AstNode, AstNodeUnion, FunctionCall } from './types.ts'
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
- function literalValue(ast: AstNode) {
93
- const node = ast as AstNodeUnion
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(ast: AstNode): boolean {
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(ast: AstNode, scope: Scope): Read[] {
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!, scope)
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!, scope)
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
- throw new Error(`Corrupt AST: unknown node type '${ast.type}'`)
352
+ return unknownNode(node)
353
353
  }
354
354
  }
355
355
  }