@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
@@ -3,16 +3,12 @@
3
3
  * Copyright 2020 Tom Shawver
4
4
  */
5
5
 
6
+ import { LAMBDA } from '../collections.ts'
7
+ import { unknownNode } from '../errors.ts'
6
8
  import { and, nullish, or } from '../grammar.ts'
7
9
 
8
10
  import type { Grammar } from '../grammar.ts'
9
- import type {
10
- AstNode,
11
- AstNodeUnion,
12
- JexlFunction,
13
- JexlValue,
14
- Literal
15
- } from '../types.ts'
11
+ import type { AstNode, JexlFunction, JexlValue } from '../types.ts'
16
12
 
17
13
  /** The variables an expression is evaluated against. */
18
14
  export type Context = Record<string, JexlValue>
@@ -44,6 +40,16 @@ function assignOwn(target: Context, key: string, value: JexlValue) {
44
40
  target[key] = value
45
41
  }
46
42
 
43
+ // inherited, these lead from any value to `Function` and `Object.prototype`
44
+ const isUnsafeKey = (key: string | number) =>
45
+ key === 'constructor' || key === '__proto__'
46
+
47
+ function member(target: Context, key: string | number) {
48
+ return isUnsafeKey(key) && !Object.hasOwn(target, key)
49
+ ? undefined
50
+ : target[key]
51
+ }
52
+
47
53
  /** The key `subject[index]` reads under, or undefined when it reads nothing. */
48
54
  function memberKey(index: JexlValue) {
49
55
  return typeof index === 'string' || typeof index === 'number'
@@ -90,23 +96,27 @@ function isKeyPart(
90
96
  * serves every evaluation.
91
97
  */
92
98
  function literalTable(node: AstNode) {
93
- const literal = node as AstNodeUnion
94
- if (literal.type === 'ArrayLiteral') {
95
- return literal.value.every((item) => item.type === 'Literal')
96
- ? Object.freeze(literal.value.map((item) => (item as Literal).value))
99
+ if (node.type === 'ArrayLiteral') {
100
+ return node.value.every((item) => item.type === 'Literal')
101
+ ? Object.freeze(node.value.map((item) => item.value))
97
102
  : undefined
98
103
  }
99
- if (literal.type === 'ObjectLiteral') {
100
- const entries = Object.entries(literal.value)
101
- if (entries.every(([, value]) => value.type === 'Literal')) {
102
- const table: Context = {}
103
- for (const [key, value] of entries) {
104
- defineOwn(table, key, (value as Literal).value)
105
- }
106
- return Object.freeze(table)
104
+ if (node.type !== 'ObjectLiteral') {
105
+ return undefined
106
+ }
107
+ const table: Context = {}
108
+ for (const [key, value] of Object.entries(node.value)) {
109
+ if (value.type !== 'Literal') {
110
+ return undefined
107
111
  }
112
+ defineOwn(table, key, value.value)
108
113
  }
109
- return undefined
114
+ return Object.freeze(table)
115
+ }
116
+
117
+ function lambda(fn: JexlFunction & { [LAMBDA]?: true }) {
118
+ fn[LAMBDA] = true
119
+ return fn
110
120
  }
111
121
 
112
122
  const unassigned = Symbol('unassigned')
@@ -208,8 +218,7 @@ function climb(hops: number, index: number): CompiledNode {
208
218
  }
209
219
  }
210
220
 
211
- function assignedNames(ast: AstNode | undefined, names = new Set<string>()) {
212
- const node = ast as AstNodeUnion | undefined
221
+ function assignedNames(node: AstNode | undefined, names = new Set<string>()) {
213
222
  switch (node?.type) {
214
223
  case 'AssignmentExpression': {
215
224
  names.add(node.left.value)
@@ -335,15 +344,11 @@ function stringify(value: JexlValue) {
335
344
  }
336
345
 
337
346
  function compileNode(
338
- ast: AstNode,
347
+ node: AstNode,
339
348
  grammar: Grammar,
340
349
  scope: Scope
341
350
  ): CompiledNode {
342
351
  const compile = (child: AstNode) => compileNode(child, grammar, scope)
343
- // AstNode types its `type` as a plain string so the Parser can build the tree
344
- // loosely; narrowing to the union once, here, lets every case below see its
345
- // own node type instead of repeating the same cast in each branch
346
- const node = ast as AstNodeUnion
347
352
  switch (node.type) {
348
353
  case 'Literal': {
349
354
  const { value } = node
@@ -365,6 +370,12 @@ function compileNode(
365
370
  : (getMember(target, name) as JexlValue)
366
371
  }
367
372
  }
373
+ if (isUnsafeKey(name)) {
374
+ return (ctx) => {
375
+ const target = from(ctx)
376
+ return target == null ? undefined : member(target as Context, name)
377
+ }
378
+ }
368
379
  return (ctx) => {
369
380
  const target = from(ctx)
370
381
  return target == null ? undefined : (target as Context)[name]
@@ -374,7 +385,7 @@ function compileNode(
374
385
  case 'BinaryExpression': {
375
386
  const op = grammar.elements[node.operator]
376
387
  const left = compile(node.left)
377
- const right = compile(node.right!)
388
+ const right = compile(node.right)
378
389
  if (op?.type === 'binaryOp' && op.evalOnDemand) {
379
390
  const { evalOnDemand } = op
380
391
  if (evalOnDemand === and) {
@@ -405,7 +416,7 @@ function compileNode(
405
416
  }
406
417
 
407
418
  case 'UnaryExpression': {
408
- const right = compile(node.right!)
419
+ const right = compile(node.right)
409
420
  const elem = grammar.elements[node.operator]
410
421
  const fn =
411
422
  elem?.type === 'unaryOp'
@@ -452,7 +463,7 @@ function compileNode(
452
463
  const key = memberKey(index(ctx))
453
464
  return subjectVal == null || key === undefined
454
465
  ? undefined
455
- : (subjectVal as Record<string | number, JexlValue>)[key]
466
+ : member(subjectVal as Context, key)
456
467
  }
457
468
  }
458
469
 
@@ -568,7 +579,7 @@ function compileNode(
568
579
  if (scope.params) {
569
580
  throw new Error('Assignment is not supported in a lambda')
570
581
  }
571
- const right = compile(node.right!)
582
+ const right = compile(node.right)
572
583
  const { slots, frame } = scope.locals!
573
584
  const slot = slots.get(node.left.value)!
574
585
  return (ctx) => (frame.locals[slot] = right(ctx))
@@ -581,23 +592,25 @@ function compileNode(
581
592
  })
582
593
  if (!scope.params) {
583
594
  const frame = scope.locals?.frame
584
- return (ctx): JexlFunction => {
595
+ return (ctx) => {
585
596
  const locals = frame?.locals
586
- return (...args) =>
597
+ return lambda((...args) =>
587
598
  body(asContext(new LambdaFrame(args, undefined, ctx, locals)))
599
+ )
588
600
  }
589
601
  }
590
- return (env): JexlFunction => {
602
+ return (env) => {
591
603
  const outer = asFrame(env)
592
- return (...args) =>
604
+ return lambda((...args) =>
593
605
  body(
594
606
  asContext(new LambdaFrame(args, outer, outer.context, outer.locals))
595
607
  )
608
+ )
596
609
  }
597
610
  }
598
611
 
599
612
  default: {
600
- throw new Error(`Corrupt AST: unknown node type '${ast.type}'`)
613
+ return unknownNode(node)
601
614
  }
602
615
  }
603
616
  }
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/index.ts CHANGED
@@ -8,7 +8,8 @@ export type { JexlOptions } from './Jexl.ts'
8
8
  export type { GetMember, VariableReader } from './grammar.ts'
9
9
  export { default as Expression } from './Expression.ts'
10
10
  export { analyze } from './analyze.ts'
11
- export { check, print, recordOf, union } from './check.ts'
11
+ export { check, recordOf, union } from './check.ts'
12
+ export { print } from './print.ts'
12
13
  export {
13
14
  callSubject,
14
15
  conditions,
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 ADDED
@@ -0,0 +1,203 @@
1
+ /*
2
+ * Jexl
3
+ * Copyright 2020 Tom Shawver
4
+ */
5
+
6
+ import { identRegex } from './Lexer.ts'
7
+ import { getGrammar } from './grammar.ts'
8
+
9
+ import type { AstNode, BinaryExpression } from './types.ts'
10
+
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
18
+ }
19
+
20
+ const LOGICAL = new Set(['||', '&&'])
21
+
22
+ // how tightly each form binds, mirroring the Parser's levels: `;`, then `=`
23
+ // and lambdas, then `?:`, then binary operators, then prefix operators
24
+ const SEQUENCE = 0
25
+ const ASSIGNMENT = 1
26
+ const CONDITIONAL = 2
27
+ const CUSTOM_BINARY = 3
28
+ const UNARY = 100
29
+
30
+ const WORDS = new Set([
31
+ 'true',
32
+ 'false',
33
+ 'null',
34
+ ...Object.keys(elements).filter((word) => identRegex.test(word))
35
+ ])
36
+
37
+ export function isName(text: string) {
38
+ return identRegex.test(text) && !WORDS.has(text)
39
+ }
40
+
41
+ export function key(name: string) {
42
+ return isName(name) ? `.${name}` : `[${quote(name)}]`
43
+ }
44
+
45
+ export function quote(text: string) {
46
+ return `'${text.replaceAll('\\', '\\\\').replaceAll("'", String.raw`\'`)}'`
47
+ }
48
+
49
+ function precedenceOf(node: AstNode) {
50
+ switch (node.type) {
51
+ case 'SequenceExpression': {
52
+ return SEQUENCE
53
+ }
54
+ case 'AssignmentExpression':
55
+ case 'Lambda': {
56
+ return ASSIGNMENT
57
+ }
58
+ case 'ConditionalExpression': {
59
+ // with no alternate, it ends only before `)`, `]`, `}`, `,`, `;` or the end
60
+ return node.alternate ? CONDITIONAL : SEQUENCE
61
+ }
62
+ case 'BinaryExpression': {
63
+ return builtIn(node.operator)?.precedence ?? CUSTOM_BINARY
64
+ }
65
+ case 'UnaryExpression': {
66
+ return UNARY
67
+ }
68
+ default: {
69
+ return Infinity
70
+ }
71
+ }
72
+ }
73
+
74
+ function within(node: AstNode, least: number) {
75
+ const text = print(node)
76
+ return precedenceOf(node) < least ? `(${text})` : text
77
+ }
78
+
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)
87
+ }
88
+ const own = op.precedence
89
+ const theirs = precedenceOf(child)
90
+ const chained =
91
+ child.type === 'BinaryExpression' &&
92
+ !!op.rightAssociative &&
93
+ !!builtIn(child.operator)?.rightAssociative
94
+ const mixesNullish =
95
+ child.type === 'BinaryExpression' &&
96
+ ((parent.operator === '??' && LOGICAL.has(child.operator)) ||
97
+ (child.operator === '??' && LOGICAL.has(parent.operator)))
98
+ const text = print(child)
99
+ return theirs < own || (theirs === own && left === chained) || mixesNullish
100
+ ? `(${text})`
101
+ : text
102
+ }
103
+
104
+ /**
105
+ * Renders a tree back to expression text, for messages and suggestions. The
106
+ * text parses back to the same tree.
107
+ */
108
+ export function print(node: AstNode): string {
109
+ switch (node.type) {
110
+ case 'Literal': {
111
+ const { value } = node
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)
123
+ }
124
+ case 'Identifier': {
125
+ return node.from
126
+ ? within(node.from, Infinity) + key(node.value)
127
+ : node.value
128
+ }
129
+ case 'FilterExpression': {
130
+ return `${within(node.subject, Infinity)}[${within(node.expr, ASSIGNMENT)}]`
131
+ }
132
+ case 'BinaryExpression': {
133
+ return `${binaryOperand(node, node.left, true)} ${node.operator} ${binaryOperand(node, node.right, false)}`
134
+ }
135
+ case 'UnaryExpression': {
136
+ const { operator } = node
137
+ const right = within(node.right, UNARY)
138
+ // the Lexer folds a minus before a digit into the number
139
+ if (operator === '-' && /^\d/.test(right)) {
140
+ return `-(${right})`
141
+ }
142
+ return identRegex.test(operator)
143
+ ? `${operator} ${right}`
144
+ : operator + right
145
+ }
146
+ case 'ConditionalExpression': {
147
+ const test = within(node.test, CUSTOM_BINARY)
148
+ const consequent = node.consequent
149
+ ? ` ${within(node.consequent, ASSIGNMENT)} `
150
+ : ''
151
+ const alternate = node.alternate
152
+ ? ` ${within(node.alternate, ASSIGNMENT)}`
153
+ : ''
154
+ return `${test} ?${consequent}:${alternate}`
155
+ }
156
+ case 'FunctionCall': {
157
+ const args = node.args.map((arg) => within(arg, ASSIGNMENT))
158
+ return `${node.name}(${args.join(', ')})`
159
+ }
160
+ case 'ArrayLiteral': {
161
+ const items = node.value.map((item) => within(item, ASSIGNMENT))
162
+ return `[${items.join(', ')}]`
163
+ }
164
+ case 'ObjectLiteral': {
165
+ const entries = Object.entries(node.value).map(
166
+ ([name, value]) =>
167
+ `${isName(name) ? name : quote(name)}: ${within(value, ASSIGNMENT)}`
168
+ )
169
+ return `{${entries.join(', ')}}`
170
+ }
171
+ case 'TemplateLiteral': {
172
+ const parts = node.parts.map((part) =>
173
+ part.type === 'static'
174
+ ? part.value
175
+ .replaceAll('\\', '\\\\')
176
+ .replaceAll('`', '\\`')
177
+ .replaceAll('${', '\\${')
178
+ : `\${${print(part.value)}}`
179
+ )
180
+ return `\`${parts.join('')}\``
181
+ }
182
+ case 'SequenceExpression': {
183
+ const { expressions } = node
184
+ const text = expressions
185
+ .map((expr) => within(expr, ASSIGNMENT))
186
+ .join('; ')
187
+ return expressions.length === 1 ? `${text};` : text
188
+ }
189
+ case 'AssignmentExpression': {
190
+ return `${node.left.value} = ${within(node.right, ASSIGNMENT)}`
191
+ }
192
+ case 'Lambda': {
193
+ const params =
194
+ node.params.length === 1
195
+ ? node.params[0]
196
+ : `(${node.params.join(', ')})`
197
+ return `${params} => ${within(node.body, ASSIGNMENT)}`
198
+ }
199
+ default: {
200
+ return '?'
201
+ }
202
+ }
203
+ }