@jbrowse/jexl 5.0.2 → 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 (83) hide show
  1. package/README.md +4 -1
  2. package/dist/Jexl.js +12 -16
  3. package/dist/Jexl.js.map +1 -1
  4. package/dist/Lexer.d.ts +12 -94
  5. package/dist/Lexer.js +41 -142
  6. package/dist/Lexer.js.map +1 -1
  7. package/dist/analyze.d.ts +2 -0
  8. package/dist/analyze.js +7 -7
  9. package/dist/analyze.js.map +1 -1
  10. package/dist/check.js +30 -30
  11. package/dist/check.js.map +1 -1
  12. package/dist/collections.d.ts +7 -0
  13. package/dist/collections.js +9 -2
  14. package/dist/collections.js.map +1 -1
  15. package/dist/conditions.d.ts +1 -1
  16. package/dist/conditions.js +25 -47
  17. package/dist/conditions.js.map +1 -1
  18. package/dist/errors.d.ts +5 -0
  19. package/dist/errors.js +8 -0
  20. package/dist/errors.js.map +1 -1
  21. package/dist/evaluator/compile.js +37 -24
  22. package/dist/evaluator/compile.js.map +1 -1
  23. package/dist/grammar.d.ts +2 -2
  24. package/dist/grammar.js +1 -8
  25. package/dist/grammar.js.map +1 -1
  26. package/dist/operators.d.ts +5 -0
  27. package/dist/operators.js +9 -5
  28. package/dist/operators.js.map +1 -1
  29. package/dist/parser/Parser.d.ts +11 -12
  30. package/dist/parser/Parser.js +4 -10
  31. package/dist/parser/Parser.js.map +1 -1
  32. package/dist/print.d.ts +1 -1
  33. package/dist/print.js +41 -47
  34. package/dist/print.js.map +1 -1
  35. package/dist/types.d.ts +18 -21
  36. package/esm/Jexl.js +12 -16
  37. package/esm/Jexl.js.map +1 -1
  38. package/esm/Lexer.d.ts +12 -94
  39. package/esm/Lexer.js +40 -142
  40. package/esm/Lexer.js.map +1 -1
  41. package/esm/analyze.d.ts +2 -0
  42. package/esm/analyze.js +6 -7
  43. package/esm/analyze.js.map +1 -1
  44. package/esm/check.js +28 -28
  45. package/esm/check.js.map +1 -1
  46. package/esm/collections.d.ts +7 -0
  47. package/esm/collections.js +8 -1
  48. package/esm/collections.js.map +1 -1
  49. package/esm/conditions.d.ts +1 -1
  50. package/esm/conditions.js +26 -48
  51. package/esm/conditions.js.map +1 -1
  52. package/esm/errors.d.ts +5 -0
  53. package/esm/errors.js +7 -0
  54. package/esm/errors.js.map +1 -1
  55. package/esm/evaluator/compile.js +37 -24
  56. package/esm/evaluator/compile.js.map +1 -1
  57. package/esm/grammar.d.ts +2 -2
  58. package/esm/grammar.js +1 -8
  59. package/esm/grammar.js.map +1 -1
  60. package/esm/operators.d.ts +5 -0
  61. package/esm/operators.js +8 -5
  62. package/esm/operators.js.map +1 -1
  63. package/esm/parser/Parser.d.ts +11 -12
  64. package/esm/parser/Parser.js +4 -8
  65. package/esm/parser/Parser.js.map +1 -1
  66. package/esm/print.d.ts +1 -1
  67. package/esm/print.js +41 -47
  68. package/esm/print.js.map +1 -1
  69. package/esm/types.d.ts +18 -21
  70. package/package.json +1 -1
  71. package/src/Jexl.ts +13 -20
  72. package/src/Lexer.ts +42 -144
  73. package/src/analyze.ts +10 -10
  74. package/src/check.ts +47 -53
  75. package/src/collections.ts +11 -1
  76. package/src/conditions.ts +38 -61
  77. package/src/errors.ts +10 -0
  78. package/src/evaluator/compile.ts +49 -36
  79. package/src/grammar.ts +3 -9
  80. package/src/operators.ts +10 -6
  81. package/src/parser/Parser.ts +13 -19
  82. package/src/print.ts +50 -52
  83. package/src/types.ts +19 -23
package/src/grammar.ts CHANGED
@@ -56,9 +56,9 @@ export interface BinaryOp {
56
56
 
57
57
  export type UnaryOpEval = (right: JexlValue) => JexlValue
58
58
 
59
+ /** A prefix operator, which binds tighter than any binary one. */
59
60
  export interface UnaryOp {
60
61
  type: 'unaryOp'
61
- precedence: number
62
62
  eval: UnaryOpEval
63
63
  }
64
64
 
@@ -82,6 +82,7 @@ export interface SimpleElement {
82
82
  | 'question'
83
83
  | 'semicolon'
84
84
  | 'arrow'
85
+ | 'assign'
85
86
  }
86
87
 
87
88
  export type GrammarElement = BinaryOp | UnaryOp | SimpleElement
@@ -174,6 +175,7 @@ export const getGrammar = (): Grammar => ({
174
175
  '?': { type: 'question' },
175
176
  ';': { type: 'semicolon' },
176
177
  '=>': { type: 'arrow' },
178
+ '=': { type: 'assign' },
177
179
  '+': {
178
180
  type: 'binaryOp',
179
181
  precedence: 30,
@@ -299,15 +301,7 @@ export const getGrammar = (): Grammar => ({
299
301
  },
300
302
  '!': {
301
303
  type: 'unaryOp',
302
- precedence: Infinity,
303
304
  eval: (right) => !right
304
- },
305
- '=': {
306
- type: 'binaryOp',
307
- precedence: 2,
308
- eval: (_left, _right) => {
309
- throw new Error('Assignment handled specially')
310
- }
311
305
  }
312
306
  },
313
307
 
package/src/operators.ts CHANGED
@@ -74,20 +74,24 @@ export function pairwise(
74
74
  return out
75
75
  }
76
76
 
77
- const patterns = new Map<string, RegExp>()
78
- const MAX_PATTERNS = 1000
79
-
80
77
  /**
81
78
  * The regular expression a pattern string stands for. A leading `(?i)`, the
82
79
  * inline flag PCRE, Python and Go read, makes it case-insensitive.
83
80
  */
81
+ export function toRegExp(pattern: string) {
82
+ return pattern.startsWith('(?i)')
83
+ ? new RegExp(pattern.slice(4), 'i')
84
+ : new RegExp(pattern)
85
+ }
86
+
87
+ const patterns = new Map<string, RegExp>()
88
+ const MAX_PATTERNS = 1000
89
+
84
90
  function regex(pattern: string) {
85
91
  let re = patterns.get(pattern)
86
92
  if (!re) {
87
93
  try {
88
- re = pattern.startsWith('(?i)')
89
- ? new RegExp(pattern.slice(4), 'i')
90
- : new RegExp(pattern)
94
+ re = toRegExp(pattern)
91
95
  } catch {
92
96
  throw new Error(`Invalid regular expression: ${pattern}`)
93
97
  }
@@ -9,7 +9,6 @@ import type Lexer from '../Lexer.ts'
9
9
  import type { Grammar } from '../grammar.ts'
10
10
  import type {
11
11
  AstNode,
12
- AstNodeUnion,
13
12
  ConditionalExpression,
14
13
  Lambda,
15
14
  Literal,
@@ -29,8 +28,6 @@ const omittedAlternateBefore = new Set([
29
28
  'semicolon'
30
29
  ])
31
30
 
32
- export { JexlSyntaxError } from '../errors.ts'
33
-
34
31
  /**
35
32
  * Converts the tokens from the {@link Lexer} into an Abstract Syntax Tree, for
36
33
  * {@link compileAst} to lower to closures. A Pratt parser: each level of the
@@ -79,7 +76,7 @@ class Parser {
79
76
  * @returns the expression tree, or null for an expression with no tokens
80
77
  * @throws {JexlSyntaxError} if the tokens do not form exactly one expression
81
78
  */
82
- complete(): AstNodeUnion | null {
79
+ complete(): AstNode | null {
83
80
  if (this._tokens.length === 0) {
84
81
  return null
85
82
  }
@@ -90,7 +87,7 @@ class Parser {
90
87
  return ast
91
88
  }
92
89
 
93
- _sequence(): AstNodeUnion {
90
+ _sequence(): AstNode {
94
91
  const first = this._assignment()
95
92
  if (this._peek()?.type !== 'semicolon') {
96
93
  return first
@@ -106,13 +103,12 @@ class Parser {
106
103
  return { type: 'SequenceExpression', expressions }
107
104
  }
108
105
 
109
- _assignment(): AstNodeUnion {
106
+ _assignment(): AstNode {
110
107
  if (this._atLambda()) {
111
108
  return this._lambda()
112
109
  }
113
110
  const left = this._ternary()
114
- const token = this._peek()
115
- if (token?.type !== 'binaryOp' || token.value !== '=') {
111
+ if (this._peek()?.type !== 'assign') {
116
112
  return left
117
113
  }
118
114
  if (left.type !== 'Identifier' || left.from) {
@@ -176,7 +172,7 @@ class Parser {
176
172
  return { type: 'Lambda', params, body }
177
173
  }
178
174
 
179
- _ternary(): AstNodeUnion {
175
+ _ternary(): AstNode {
180
176
  const test = this._binary(-Infinity)
181
177
  if (!this._eat('question')) {
182
178
  return test
@@ -198,11 +194,11 @@ class Parser {
198
194
  * One exactly at `floor` binds only when it continues a right-associative
199
195
  * chain, which is how `a ^ b ^ c` groups from the right.
200
196
  */
201
- _binary(floor: number, rightAssociative = false): AstNodeUnion {
197
+ _binary(floor: number, rightAssociative = false): AstNode {
202
198
  let left = this._unary()
203
199
  for (;;) {
204
200
  const token = this._peek()
205
- if (token?.type !== 'binaryOp' || token.value === '=') {
201
+ if (token?.type !== 'binaryOp') {
206
202
  return left
207
203
  }
208
204
  const operator = token.value as string
@@ -224,7 +220,7 @@ class Parser {
224
220
  }
225
221
 
226
222
  /** `a ?? b || c` has no grouping a reader can guess; JS refuses it too. */
227
- _mixesNullish(operator: string, ...operands: AstNodeUnion[]) {
223
+ _mixesNullish(operator: string, ...operands: AstNode[]) {
228
224
  return operands.some(
229
225
  (operand) =>
230
226
  operand.type === 'BinaryExpression' &&
@@ -235,22 +231,20 @@ class Parser {
235
231
  )
236
232
  }
237
233
 
238
- _unary(): AstNodeUnion {
234
+ _unary(): AstNode {
239
235
  const token = this._peek()
240
236
  if (token?.type !== 'unaryOp') {
241
237
  return this._postfix(this._primary())
242
238
  }
243
239
  this._pos++
244
- const operator = token.value as string
245
- const op = this._grammar.elements[operator]
246
240
  return {
247
241
  type: 'UnaryExpression',
248
- operator,
249
- right: this._binary(op?.type === 'unaryOp' ? op.precedence : Infinity)
242
+ operator: token.value as string,
243
+ right: this._unary()
250
244
  }
251
245
  }
252
246
 
253
- _postfix(subject: AstNodeUnion): AstNodeUnion {
247
+ _postfix(subject: AstNode): AstNode {
254
248
  let node = subject
255
249
  for (;;) {
256
250
  switch (this._peek()?.type) {
@@ -294,7 +288,7 @@ class Parser {
294
288
  }
295
289
  }
296
290
 
297
- _primary(): AstNodeUnion {
291
+ _primary(): AstNode {
298
292
  const token = this._next()
299
293
  switch (token.type) {
300
294
  case 'literal': {
package/src/print.ts CHANGED
@@ -3,34 +3,20 @@
3
3
  * Copyright 2020 Tom Shawver
4
4
  */
5
5
 
6
- import type { AstNode, AstNodeUnion, BinaryExpression } from './types.ts'
6
+ import { identRegex } from './Lexer.ts'
7
+ import { getGrammar } from './grammar.ts'
7
8
 
8
- type Node = AstNodeUnion
9
+ import type { AstNode, BinaryExpression } from './types.ts'
9
10
 
10
- const PRECEDENCE: Record<string, number> = {
11
- '=': 2,
12
- '||': 10,
13
- '??': 10,
14
- '&&': 11,
15
- '==': 20,
16
- '!=': 20,
17
- '~': 20,
18
- '!~': 20,
19
- '<': 20,
20
- '<=': 20,
21
- '>': 20,
22
- '>=': 20,
23
- in: 20,
24
- '+': 30,
25
- '-': 30,
26
- '*': 40,
27
- '/': 40,
28
- '//': 40,
29
- '%': 50,
30
- '^': 50
11
+ const { elements } = getGrammar()
12
+
13
+ // print knows where only the built-in operators bind, so a host's own ones
14
+ // print with their operands parenthesized
15
+ function builtIn(operator: string) {
16
+ const element = elements[operator]
17
+ return element?.type === 'binaryOp' ? element : undefined
31
18
  }
32
19
 
33
- const RIGHT_ASSOCIATIVE = new Set(['^'])
34
20
  const LOGICAL = new Set(['||', '&&'])
35
21
 
36
22
  // how tightly each form binds, mirroring the Parser's levels: `;`, then `=`
@@ -41,12 +27,15 @@ const CONDITIONAL = 2
41
27
  const CUSTOM_BINARY = 3
42
28
  const UNARY = 100
43
29
 
44
- const letter = String.raw`a-zA-Zа-яА-Я_À-ÖØ-öø-ÿ$`
45
- const IDENTIFIER = new RegExp(`^[${letter}][${letter}0-9]*$`)
46
- const WORDS = new Set(['in', 'true', 'false', 'null'])
30
+ const WORDS = new Set([
31
+ 'true',
32
+ 'false',
33
+ 'null',
34
+ ...Object.keys(elements).filter((word) => identRegex.test(word))
35
+ ])
47
36
 
48
37
  export function isName(text: string) {
49
- return IDENTIFIER.test(text) && !WORDS.has(text)
38
+ return identRegex.test(text) && !WORDS.has(text)
50
39
  }
51
40
 
52
41
  export function key(name: string) {
@@ -57,7 +46,7 @@ export function quote(text: string) {
57
46
  return `'${text.replaceAll('\\', '\\\\').replaceAll("'", String.raw`\'`)}'`
58
47
  }
59
48
 
60
- function precedenceOf(node: Node) {
49
+ function precedenceOf(node: AstNode) {
61
50
  switch (node.type) {
62
51
  case 'SequenceExpression': {
63
52
  return SEQUENCE
@@ -71,7 +60,7 @@ function precedenceOf(node: Node) {
71
60
  return node.alternate ? CONDITIONAL : SEQUENCE
72
61
  }
73
62
  case 'BinaryExpression': {
74
- return PRECEDENCE[node.operator] ?? CUSTOM_BINARY
63
+ return builtIn(node.operator)?.precedence ?? CUSTOM_BINARY
75
64
  }
76
65
  case 'UnaryExpression': {
77
66
  return UNARY
@@ -82,27 +71,31 @@ function precedenceOf(node: Node) {
82
71
  }
83
72
  }
84
73
 
85
- function within(ast: AstNode, least: number) {
86
- const text = print(ast)
87
- return precedenceOf(ast as Node) < least ? `(${text})` : text
74
+ function within(node: AstNode, least: number) {
75
+ const text = print(node)
76
+ return precedenceOf(node) < least ? `(${text})` : text
88
77
  }
89
78
 
90
- function binaryOperand(parent: BinaryExpression, ast: AstNode, left: boolean) {
91
- const own = PRECEDENCE[parent.operator]
92
- if (own === undefined) {
93
- return within(ast, UNARY)
79
+ function binaryOperand(
80
+ parent: BinaryExpression,
81
+ child: AstNode,
82
+ left: boolean
83
+ ) {
84
+ const op = builtIn(parent.operator)
85
+ if (!op) {
86
+ return within(child, UNARY)
94
87
  }
95
- const child = ast as Node
88
+ const own = op.precedence
96
89
  const theirs = precedenceOf(child)
97
90
  const chained =
98
91
  child.type === 'BinaryExpression' &&
99
- RIGHT_ASSOCIATIVE.has(parent.operator) &&
100
- RIGHT_ASSOCIATIVE.has(child.operator)
92
+ !!op.rightAssociative &&
93
+ !!builtIn(child.operator)?.rightAssociative
101
94
  const mixesNullish =
102
95
  child.type === 'BinaryExpression' &&
103
96
  ((parent.operator === '??' && LOGICAL.has(child.operator)) ||
104
97
  (child.operator === '??' && LOGICAL.has(parent.operator)))
105
- const text = print(ast)
98
+ const text = print(child)
106
99
  return theirs < own || (theirs === own && left === chained) || mixesNullish
107
100
  ? `(${text})`
108
101
  : text
@@ -112,16 +105,21 @@ function binaryOperand(parent: BinaryExpression, ast: AstNode, left: boolean) {
112
105
  * Renders a tree back to expression text, for messages and suggestions. The
113
106
  * text parses back to the same tree.
114
107
  */
115
- export function print(ast: AstNode): string {
116
- const node = ast as Node
108
+ export function print(node: AstNode): string {
117
109
  switch (node.type) {
118
110
  case 'Literal': {
119
111
  const { value } = node
120
- return typeof value === 'string'
121
- ? quote(value)
122
- : Object.is(value, -0)
123
- ? '-0'
124
- : String(value)
112
+ if (typeof value === 'string') {
113
+ return quote(value)
114
+ }
115
+ // a number too large for a double reads as Infinity, which is a name
116
+ return Object.is(value, -0)
117
+ ? '-0'
118
+ : value === Infinity
119
+ ? '1e999'
120
+ : value === -Infinity
121
+ ? '-1e999'
122
+ : String(value)
125
123
  }
126
124
  case 'Identifier': {
127
125
  return node.from
@@ -132,16 +130,16 @@ export function print(ast: AstNode): string {
132
130
  return `${within(node.subject, Infinity)}[${within(node.expr, ASSIGNMENT)}]`
133
131
  }
134
132
  case 'BinaryExpression': {
135
- return `${binaryOperand(node, node.left, true)} ${node.operator} ${binaryOperand(node, node.right!, false)}`
133
+ return `${binaryOperand(node, node.left, true)} ${node.operator} ${binaryOperand(node, node.right, false)}`
136
134
  }
137
135
  case 'UnaryExpression': {
138
136
  const { operator } = node
139
- const right = within(node.right!, UNARY)
137
+ const right = within(node.right, UNARY)
140
138
  // the Lexer folds a minus before a digit into the number
141
139
  if (operator === '-' && /^\d/.test(right)) {
142
140
  return `-(${right})`
143
141
  }
144
- return IDENTIFIER.test(operator)
142
+ return identRegex.test(operator)
145
143
  ? `${operator} ${right}`
146
144
  : operator + right
147
145
  }
@@ -189,7 +187,7 @@ export function print(ast: AstNode): string {
189
187
  return expressions.length === 1 ? `${text};` : text
190
188
  }
191
189
  case 'AssignmentExpression': {
192
- return `${node.left.value} = ${within(node.right!, ASSIGNMENT)}`
190
+ return `${node.left.value} = ${within(node.right, ASSIGNMENT)}`
193
191
  }
194
192
  case 'Lambda': {
195
193
  const params =
package/src/types.ts CHANGED
@@ -27,41 +27,36 @@ export interface Token {
27
27
  raw: string
28
28
  }
29
29
 
30
- export interface AstNode {
31
- type: string
32
- right?: AstNode
33
- }
34
-
35
- export interface Literal extends AstNode {
30
+ export interface Literal {
36
31
  type: 'Literal'
37
32
  value: string | number | boolean | null
38
33
  }
39
34
 
40
- export interface Identifier extends AstNode {
35
+ export interface Identifier {
41
36
  type: 'Identifier'
42
37
  value: string
43
38
  from?: AstNode
44
39
  }
45
40
 
46
- export interface BinaryExpression extends AstNode {
41
+ export interface BinaryExpression {
47
42
  type: 'BinaryExpression'
48
43
  operator: string
49
44
  left: AstNode
50
- right?: AstNode
45
+ right: AstNode
51
46
  }
52
47
 
53
- export interface UnaryExpression extends AstNode {
48
+ export interface UnaryExpression {
54
49
  type: 'UnaryExpression'
55
50
  operator: string
56
- right?: AstNode
51
+ right: AstNode
57
52
  }
58
53
 
59
- export interface ArrayLiteral extends AstNode {
54
+ export interface ArrayLiteral {
60
55
  type: 'ArrayLiteral'
61
56
  value: AstNode[]
62
57
  }
63
58
 
64
- export interface ObjectLiteral extends AstNode {
59
+ export interface ObjectLiteral {
65
60
  type: 'ObjectLiteral'
66
61
  value: Record<string, AstNode>
67
62
  }
@@ -70,48 +65,49 @@ export type TemplateLiteralPart =
70
65
  | { type: 'static'; value: string }
71
66
  | { type: 'expression'; value: AstNode }
72
67
 
73
- export interface TemplateLiteral extends AstNode {
68
+ export interface TemplateLiteral {
74
69
  type: 'TemplateLiteral'
75
70
  parts: TemplateLiteralPart[]
76
71
  }
77
72
 
78
- export interface FunctionCall extends AstNode {
73
+ export interface FunctionCall {
79
74
  type: 'FunctionCall'
80
75
  name: string
81
76
  args: AstNode[]
82
77
  }
83
78
 
84
- export interface FilterExpression extends AstNode {
79
+ export interface FilterExpression {
85
80
  type: 'FilterExpression'
86
81
  expr: AstNode
87
82
  subject: AstNode
88
83
  }
89
84
 
90
- export interface ConditionalExpression extends AstNode {
85
+ export interface ConditionalExpression {
91
86
  type: 'ConditionalExpression'
92
87
  test: AstNode
93
88
  consequent?: AstNode
94
89
  alternate?: AstNode
95
90
  }
96
91
 
97
- export interface SequenceExpression extends AstNode {
92
+ export interface SequenceExpression {
98
93
  type: 'SequenceExpression'
99
94
  expressions: AstNode[]
100
95
  }
101
96
 
102
- export interface AssignmentExpression extends AstNode {
97
+ export interface AssignmentExpression {
103
98
  type: 'AssignmentExpression'
104
99
  operator: '='
105
100
  left: Identifier
101
+ right: AstNode
106
102
  }
107
103
 
108
- export interface Lambda extends AstNode {
104
+ export interface Lambda {
109
105
  type: 'Lambda'
110
106
  params: string[]
111
107
  body: AstNode
112
108
  }
113
109
 
114
- export type AstNodeUnion =
110
+ export type AstNode =
115
111
  | Literal
116
112
  | Identifier
117
113
  | BinaryExpression
@@ -126,7 +122,7 @@ export type AstNodeUnion =
126
122
  | AssignmentExpression
127
123
  | Lambda
128
124
 
129
- export type NodeByType<T extends AstNodeUnion['type']> = Extract<
130
- AstNodeUnion,
125
+ export type NodeByType<T extends AstNode['type']> = Extract<
126
+ AstNode,
131
127
  { type: T }
132
128
  >