@jbrowse/jexl 4.0.1 → 5.0.1
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 +163 -20
- package/dist/Expression.d.ts +16 -0
- package/dist/Expression.js +28 -4
- package/dist/Expression.js.map +1 -1
- package/dist/Jexl.d.ts +20 -8
- package/dist/Jexl.js +34 -15
- package/dist/Jexl.js.map +1 -1
- package/dist/Lexer.d.ts +4 -3
- package/dist/Lexer.js +29 -21
- package/dist/Lexer.js.map +1 -1
- package/dist/analyze.d.ts +84 -0
- package/dist/analyze.js +259 -0
- package/dist/analyze.js.map +1 -0
- package/dist/check.d.ts +109 -0
- package/dist/check.js +1267 -0
- package/dist/check.js.map +1 -0
- package/dist/collections.d.ts +3 -0
- package/dist/collections.js +140 -0
- package/dist/collections.js.map +1 -0
- package/dist/conditions.d.ts +62 -0
- package/dist/conditions.js +207 -0
- package/dist/conditions.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -0
- package/dist/evaluator/compile.d.ts +10 -7
- package/dist/evaluator/compile.js +292 -99
- package/dist/evaluator/compile.js.map +1 -1
- package/dist/grammar.d.ts +22 -9
- package/dist/grammar.js +83 -55
- package/dist/grammar.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/dist/operators.d.ts +34 -0
- package/dist/operators.js +131 -0
- package/dist/operators.js.map +1 -0
- package/dist/parser/Parser.d.ts +48 -91
- package/dist/parser/Parser.js +347 -221
- package/dist/parser/Parser.js.map +1 -1
- package/dist/types.d.ts +11 -5
- package/esm/Expression.d.ts +16 -0
- package/esm/Expression.js +28 -4
- package/esm/Expression.js.map +1 -1
- package/esm/Jexl.d.ts +20 -8
- package/esm/Jexl.js +34 -15
- package/esm/Jexl.js.map +1 -1
- package/esm/Lexer.d.ts +4 -3
- package/esm/Lexer.js +29 -21
- package/esm/Lexer.js.map +1 -1
- package/esm/analyze.d.ts +84 -0
- package/esm/analyze.js +256 -0
- package/esm/analyze.js.map +1 -0
- package/esm/check.d.ts +109 -0
- package/esm/check.js +1260 -0
- package/esm/check.js.map +1 -0
- package/esm/collections.d.ts +3 -0
- package/esm/collections.js +137 -0
- package/esm/collections.js.map +1 -0
- package/esm/conditions.d.ts +62 -0
- package/esm/conditions.js +200 -0
- package/esm/conditions.js.map +1 -0
- package/esm/errors.d.ts +5 -0
- package/esm/errors.js +14 -0
- package/esm/errors.js.map +1 -0
- package/esm/evaluator/compile.d.ts +10 -7
- package/esm/evaluator/compile.js +292 -99
- package/esm/evaluator/compile.js.map +1 -1
- package/esm/grammar.d.ts +22 -9
- package/esm/grammar.js +79 -53
- package/esm/grammar.js.map +1 -1
- package/esm/index.d.ts +9 -0
- package/esm/index.js +4 -0
- package/esm/index.js.map +1 -1
- package/esm/operators.d.ts +34 -0
- package/esm/operators.js +123 -0
- package/esm/operators.js.map +1 -0
- package/esm/parser/Parser.d.ts +48 -91
- package/esm/parser/Parser.js +349 -192
- package/esm/parser/Parser.js.map +1 -1
- package/esm/types.d.ts +11 -5
- package/package.json +1 -1
- package/src/Expression.ts +34 -4
- package/src/Jexl.ts +52 -16
- package/src/Lexer.ts +32 -21
- package/src/analyze.ts +378 -0
- package/src/check.ts +1682 -0
- package/src/collections.ts +148 -0
- package/src/conditions.ts +262 -0
- package/src/errors.ts +15 -0
- package/src/evaluator/compile.ts +351 -102
- package/src/grammar.ts +137 -55
- package/src/index.ts +15 -0
- package/src/operators.ts +151 -0
- package/src/parser/Parser.ts +389 -199
- package/src/types.ts +13 -3
- package/dist/parser/handlers.d.ts +0 -115
- package/dist/parser/handlers.js +0 -352
- package/dist/parser/handlers.js.map +0 -1
- package/dist/parser/states.d.ts +0 -47
- package/dist/parser/states.js +0 -176
- package/dist/parser/states.js.map +0 -1
- package/esm/parser/handlers.d.ts +0 -115
- package/esm/parser/handlers.js +0 -328
- package/esm/parser/handlers.js.map +0 -1
- package/esm/parser/states.d.ts +0 -47
- package/esm/parser/states.js +0 -140
- package/esm/parser/states.js.map +0 -1
- package/src/parser/handlers.ts +0 -372
- package/src/parser/states.ts +0 -159
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Jexl
|
|
3
|
+
* Copyright 2020 Tom Shawver
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { GrammarFn } from './grammar.ts'
|
|
7
|
+
import type { JexlFunction, JexlValue } from './types.ts'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A list as these functions read it. A lone value is a list of one and a
|
|
11
|
+
* missing one is empty, so that a field which is sometimes a scalar and
|
|
12
|
+
* sometimes an array, as VCF INFO fields are, needs no special case. A Set,
|
|
13
|
+
* a Map or a plain object, such as samples keyed by name, gives its values.
|
|
14
|
+
*/
|
|
15
|
+
function toList(value: JexlValue): JexlValue[] {
|
|
16
|
+
if (value == null) {
|
|
17
|
+
return []
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
return value
|
|
21
|
+
}
|
|
22
|
+
const held = value as unknown
|
|
23
|
+
if (held instanceof Set || held instanceof Map) {
|
|
24
|
+
return [...(held.values() as Iterable<JexlValue>)]
|
|
25
|
+
}
|
|
26
|
+
return isPlainObject(value) ? Object.values(value) : [value]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isPlainObject(value: JexlValue): value is Record<string, JexlValue> {
|
|
30
|
+
if (typeof value !== 'object' || value === null) {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
const proto = Object.getPrototypeOf(value) as unknown
|
|
34
|
+
return proto === Object.prototype || proto === null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const identity = (value: JexlValue) => value
|
|
38
|
+
|
|
39
|
+
function callback(name: string, fn: JexlValue): JexlFunction {
|
|
40
|
+
if (fn === undefined) {
|
|
41
|
+
return identity
|
|
42
|
+
}
|
|
43
|
+
if (typeof fn !== 'function') {
|
|
44
|
+
throw new TypeError(`${name}() expects a lambda, such as x => x > 1`)
|
|
45
|
+
}
|
|
46
|
+
return fn
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The numbers a list holds, each read through `fn` when there is one. A
|
|
51
|
+
* boolean counts as 1 or 0, so `mean(samples, s => s.GQ > 90)` is the
|
|
52
|
+
* fraction that pass; anything else that is not a number is skipped, as
|
|
53
|
+
* bcftools skips a missing value.
|
|
54
|
+
*/
|
|
55
|
+
function numbers(name: string, list: JexlValue, fn: JexlValue) {
|
|
56
|
+
const read = callback(name, fn)
|
|
57
|
+
const out: number[] = []
|
|
58
|
+
for (const item of toList(list)) {
|
|
59
|
+
const value = read(item)
|
|
60
|
+
if (typeof value === 'number' && !Number.isNaN(value)) {
|
|
61
|
+
out.push(value)
|
|
62
|
+
} else if (typeof value === 'boolean') {
|
|
63
|
+
out.push(Number(value))
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The numbers `min` and `max` compare: a list and an optional lambda, as the
|
|
71
|
+
* other aggregates take, or any number of values and lists, as `Math.max`
|
|
72
|
+
* takes.
|
|
73
|
+
*/
|
|
74
|
+
function extremes(name: string, args: JexlValue[]) {
|
|
75
|
+
const last = args.at(-1)
|
|
76
|
+
if (typeof last !== 'function') {
|
|
77
|
+
return numbers(name, args.flatMap(toList), undefined)
|
|
78
|
+
}
|
|
79
|
+
if (args.length > 2) {
|
|
80
|
+
throw new TypeError(`${name}() takes a list and a lambda, or values`)
|
|
81
|
+
}
|
|
82
|
+
return numbers(name, args[0], last)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sum(values: number[]) {
|
|
86
|
+
let total = 0
|
|
87
|
+
for (const value of values) {
|
|
88
|
+
total += value
|
|
89
|
+
}
|
|
90
|
+
return total
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function ascending(a: JexlValue, b: JexlValue) {
|
|
94
|
+
return (a as number) < (b as number)
|
|
95
|
+
? -1
|
|
96
|
+
: (a as number) > (b as number)
|
|
97
|
+
? 1
|
|
98
|
+
: 0
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The functions every Jexl instance starts with. A host may replace any. */
|
|
102
|
+
export const collectionFunctions: Record<string, GrammarFn> = {
|
|
103
|
+
any: (list, fn) => toList(list).some(callback('any', fn)),
|
|
104
|
+
all: (list, fn) => toList(list).every(callback('all', fn)),
|
|
105
|
+
count: (list, fn) =>
|
|
106
|
+
fn === undefined
|
|
107
|
+
? toList(list).length
|
|
108
|
+
: toList(list).filter(callback('count', fn)).length,
|
|
109
|
+
map: (list, fn) => toList(list).map(callback('map', fn)),
|
|
110
|
+
filter: (list, fn) => toList(list).filter(callback('filter', fn)),
|
|
111
|
+
find: (list, fn) => toList(list).find(callback('find', fn)),
|
|
112
|
+
sort: (list, fn) => {
|
|
113
|
+
const compare = fn === undefined ? ascending : callback('sort', fn)
|
|
114
|
+
return [...toList(list)].sort((a, b) => compare(a, b) as number)
|
|
115
|
+
},
|
|
116
|
+
sum: (list, fn) => sum(numbers('sum', list, fn)),
|
|
117
|
+
mean: (list, fn) => {
|
|
118
|
+
const values = numbers('mean', list, fn)
|
|
119
|
+
return values.length > 0 ? sum(values) / values.length : undefined
|
|
120
|
+
},
|
|
121
|
+
median: (list, fn) => {
|
|
122
|
+
const values = numbers('median', list, fn).sort((a, b) => a - b)
|
|
123
|
+
const mid = values.length >> 1
|
|
124
|
+
return values.length === 0
|
|
125
|
+
? undefined
|
|
126
|
+
: values.length % 2
|
|
127
|
+
? values[mid]
|
|
128
|
+
: (values[mid - 1]! + values[mid]!) / 2
|
|
129
|
+
},
|
|
130
|
+
min: (...args) => {
|
|
131
|
+
const values = extremes('min', args)
|
|
132
|
+
return values.length > 0 ? Math.min(...values) : undefined
|
|
133
|
+
},
|
|
134
|
+
max: (...args) => {
|
|
135
|
+
const values = extremes('max', args)
|
|
136
|
+
return values.length > 0 ? Math.max(...values) : undefined
|
|
137
|
+
},
|
|
138
|
+
reduce: (list, fn, ...initial) => {
|
|
139
|
+
const items = toList(list)
|
|
140
|
+
const step = callback('reduce', fn)
|
|
141
|
+
const seeded = initial.length > 0
|
|
142
|
+
let acc = seeded ? initial[0] : items[0]
|
|
143
|
+
for (let i = seeded ? 0 : 1; i < items.length; i++) {
|
|
144
|
+
acc = step(acc, items[i])
|
|
145
|
+
}
|
|
146
|
+
return acc
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Jexl
|
|
3
|
+
* Copyright 2020 Tom Shawver
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { analyze } from './analyze.ts'
|
|
7
|
+
import { print } from './check.ts'
|
|
8
|
+
|
|
9
|
+
import type { PathKey } from './analyze.ts'
|
|
10
|
+
import type { AstNode, AstNodeUnion } from './types.ts'
|
|
11
|
+
|
|
12
|
+
export type Scalar = string | number | boolean | null
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What a condition tests: a path off the row (`feature.INFO.DP`, or
|
|
16
|
+
* `get(feature, 'score')` through an accessor), or a host function of the row
|
|
17
|
+
* such as `maf(feature)` or `genotypeCount(feature, 'het')`.
|
|
18
|
+
*/
|
|
19
|
+
export type Subject =
|
|
20
|
+
| { kind: 'path'; path: PathKey[]; node: AstNode }
|
|
21
|
+
| { kind: 'call'; name: string; args: Scalar[]; node: AstNode }
|
|
22
|
+
|
|
23
|
+
export type Condition =
|
|
24
|
+
| {
|
|
25
|
+
subject: Subject
|
|
26
|
+
op: '==' | '!=' | '<' | '<=' | '>' | '>='
|
|
27
|
+
value: Scalar
|
|
28
|
+
}
|
|
29
|
+
| { subject: Subject; op: '~' | '!~'; value: string }
|
|
30
|
+
| { subject: Subject; op: 'in' | '!in'; value: Scalar[] }
|
|
31
|
+
| { subject: Subject; op: 'has' | '!has'; value: string }
|
|
32
|
+
| { subject: Subject; op: 'set' | '!set' }
|
|
33
|
+
|
|
34
|
+
export interface ConditionOptions {
|
|
35
|
+
/** The variable holding the record, e.g. `feature`. */
|
|
36
|
+
row: string
|
|
37
|
+
/** Functions that read a path off the row, as {@link analyze} takes them. */
|
|
38
|
+
accessors?: Record<string, readonly PathKey[]>
|
|
39
|
+
/** Host functions of the row, e.g. `maf`, that a condition may test. */
|
|
40
|
+
calls?: readonly string[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const COMPARE = new Set(['==', '!=', '<', '<=', '>', '>='])
|
|
44
|
+
const FLIP: Record<string, Condition['op']> = {
|
|
45
|
+
'==': '==',
|
|
46
|
+
'!=': '!=',
|
|
47
|
+
'<': '>',
|
|
48
|
+
'<=': '>=',
|
|
49
|
+
'>': '<',
|
|
50
|
+
'>=': '<='
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function scalar(ast: AstNode): { value: Scalar } | undefined {
|
|
54
|
+
const node = ast as AstNodeUnion
|
|
55
|
+
if (node.type === 'Literal') {
|
|
56
|
+
return { value: node.value }
|
|
57
|
+
}
|
|
58
|
+
return node.type === 'TemplateLiteral' &&
|
|
59
|
+
node.parts.every((part) => part.type === 'static')
|
|
60
|
+
? { value: node.parts.map((part) => part.value).join('') }
|
|
61
|
+
: undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isRow(ast: AstNode | undefined, row: string) {
|
|
65
|
+
const node = ast as AstNodeUnion | undefined
|
|
66
|
+
return node?.type === 'Identifier' && !node.from && node.value === row
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function subjectOf(ast: AstNode, opts: ConditionOptions): Subject | undefined {
|
|
70
|
+
const node = ast as AstNodeUnion
|
|
71
|
+
if (
|
|
72
|
+
node.type === 'FunctionCall' &&
|
|
73
|
+
opts.calls?.includes(node.name) &&
|
|
74
|
+
isRow(node.args[0], opts.row)
|
|
75
|
+
) {
|
|
76
|
+
const rest = node.args.slice(1).map(scalar)
|
|
77
|
+
return rest.every(Boolean)
|
|
78
|
+
? {
|
|
79
|
+
kind: 'call',
|
|
80
|
+
name: node.name,
|
|
81
|
+
args: rest.map((arg) => arg!.value),
|
|
82
|
+
node
|
|
83
|
+
}
|
|
84
|
+
: undefined
|
|
85
|
+
}
|
|
86
|
+
const accessor =
|
|
87
|
+
node.type === 'FunctionCall' &&
|
|
88
|
+
Object.hasOwn(opts.accessors ?? {}, node.name)
|
|
89
|
+
if (
|
|
90
|
+
node.type !== 'Identifier' &&
|
|
91
|
+
node.type !== 'FilterExpression' &&
|
|
92
|
+
!accessor
|
|
93
|
+
) {
|
|
94
|
+
return undefined
|
|
95
|
+
}
|
|
96
|
+
const read = analyze(node, { row: opts.row, accessors: opts.accessors })
|
|
97
|
+
const [first] = read.reads
|
|
98
|
+
const literalAccess =
|
|
99
|
+
accessor &&
|
|
100
|
+
read.reads.length === 1 &&
|
|
101
|
+
read.calls.length === 1 &&
|
|
102
|
+
read.calls[0]!.args.slice(1).every((arg) => arg.type === 'literal')
|
|
103
|
+
return (read.bare || literalAccess) &&
|
|
104
|
+
first?.root === opts.row &&
|
|
105
|
+
!first.dynamic &&
|
|
106
|
+
first.path.length > 0
|
|
107
|
+
? { kind: 'path', path: first.path, node }
|
|
108
|
+
: undefined
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function condition(
|
|
112
|
+
ast: AstNode,
|
|
113
|
+
opts: ConditionOptions
|
|
114
|
+
): Condition | undefined {
|
|
115
|
+
const node = ast as AstNodeUnion
|
|
116
|
+
if (node.type === 'UnaryExpression' && node.operator === '!') {
|
|
117
|
+
const inner = condition(node.right!, opts)
|
|
118
|
+
// only forms whose negation reads as a condition of its own; `!(QUAL > 30)`
|
|
119
|
+
// holds where QUAL is missing, which `QUAL <= 30` does not
|
|
120
|
+
return inner?.op === 'in' || inner?.op === 'has' || inner?.op === 'set'
|
|
121
|
+
? ({ ...inner, op: `!${inner.op}` } as Condition)
|
|
122
|
+
: undefined
|
|
123
|
+
}
|
|
124
|
+
if (node.type !== 'BinaryExpression') {
|
|
125
|
+
const subject = subjectOf(node, opts)
|
|
126
|
+
return subject && { subject, op: 'set' }
|
|
127
|
+
}
|
|
128
|
+
const { operator, left } = node
|
|
129
|
+
const right = node.right!
|
|
130
|
+
if (COMPARE.has(operator)) {
|
|
131
|
+
const subject = subjectOf(left, opts)
|
|
132
|
+
const value = scalar(right)
|
|
133
|
+
if (subject && value) {
|
|
134
|
+
return { subject, op: operator as '==', value: value.value }
|
|
135
|
+
}
|
|
136
|
+
const flipped = subjectOf(right, opts)
|
|
137
|
+
const leftValue = scalar(left)
|
|
138
|
+
return flipped && leftValue
|
|
139
|
+
? {
|
|
140
|
+
subject: flipped,
|
|
141
|
+
op: FLIP[operator] as '==',
|
|
142
|
+
value: leftValue.value
|
|
143
|
+
}
|
|
144
|
+
: undefined
|
|
145
|
+
}
|
|
146
|
+
if (operator === '~' || operator === '!~') {
|
|
147
|
+
const subject = subjectOf(left, opts)
|
|
148
|
+
const value = scalar(right)
|
|
149
|
+
return subject && typeof value?.value === 'string'
|
|
150
|
+
? { subject, op: operator, value: value.value }
|
|
151
|
+
: undefined
|
|
152
|
+
}
|
|
153
|
+
if (operator === 'in') {
|
|
154
|
+
const subject = subjectOf(left, opts)
|
|
155
|
+
const list = right as AstNodeUnion
|
|
156
|
+
if (subject && list.type === 'ArrayLiteral') {
|
|
157
|
+
const values = list.value.map(scalar)
|
|
158
|
+
return values.length > 0 && values.every(Boolean)
|
|
159
|
+
? { subject, op: 'in', value: values.map((value) => value!.value) }
|
|
160
|
+
: undefined
|
|
161
|
+
}
|
|
162
|
+
const key = scalar(left)
|
|
163
|
+
const container = subjectOf(right, opts)
|
|
164
|
+
return container && typeof key?.value === 'string'
|
|
165
|
+
? { subject: container, op: 'has', value: key.value }
|
|
166
|
+
: undefined
|
|
167
|
+
}
|
|
168
|
+
return undefined
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Reads an expression as conditions all of which must hold — `a && b && …`,
|
|
173
|
+
* each comparing a field of the row with a literal — or answers undefined when
|
|
174
|
+
* any part is something else. A filter editor shows the conditions as rows and
|
|
175
|
+
* keeps anything undefined as text, so a line it cannot read is never
|
|
176
|
+
* misread.
|
|
177
|
+
*/
|
|
178
|
+
export function conditions(
|
|
179
|
+
ast: AstNode | null,
|
|
180
|
+
opts: ConditionOptions
|
|
181
|
+
): Condition[] | undefined {
|
|
182
|
+
const node = ast as AstNodeUnion | null
|
|
183
|
+
if (!node) {
|
|
184
|
+
return undefined
|
|
185
|
+
}
|
|
186
|
+
if (node.type === 'BinaryExpression' && node.operator === '&&') {
|
|
187
|
+
const left = conditions(node.left, opts)
|
|
188
|
+
const right = left && conditions(node.right!, opts)
|
|
189
|
+
return left && right && [...left, ...right]
|
|
190
|
+
}
|
|
191
|
+
const found = condition(node, opts)
|
|
192
|
+
return found && [found]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const literal = (value: Scalar) =>
|
|
196
|
+
print({ type: 'Literal', value } as AstNodeUnion)
|
|
197
|
+
|
|
198
|
+
/** A subject reading `path` off the row, for a condition built from a picker. */
|
|
199
|
+
export function pathSubject(row: string, path: PathKey[]): Subject {
|
|
200
|
+
let node: AstNode = { type: 'Identifier', value: row } as AstNodeUnion
|
|
201
|
+
for (const key of path) {
|
|
202
|
+
node =
|
|
203
|
+
typeof key === 'string'
|
|
204
|
+
? ({ type: 'Identifier', value: key, from: node } as AstNodeUnion)
|
|
205
|
+
: ({
|
|
206
|
+
type: 'FilterExpression',
|
|
207
|
+
subject: node,
|
|
208
|
+
expr: { type: 'Literal', value: key }
|
|
209
|
+
} as AstNodeUnion)
|
|
210
|
+
}
|
|
211
|
+
return { kind: 'path', path, node }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** A subject calling a host function of the row, such as `maf(feature)`. */
|
|
215
|
+
export function callSubject(
|
|
216
|
+
row: string,
|
|
217
|
+
name: string,
|
|
218
|
+
args: Scalar[] = []
|
|
219
|
+
): Subject {
|
|
220
|
+
const node = {
|
|
221
|
+
type: 'FunctionCall',
|
|
222
|
+
name,
|
|
223
|
+
args: [
|
|
224
|
+
{ type: 'Identifier', value: row },
|
|
225
|
+
...args.map((value) => ({ type: 'Literal', value }))
|
|
226
|
+
]
|
|
227
|
+
} as AstNodeUnion
|
|
228
|
+
return { kind: 'call', name, args, node }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Writes one condition as expression text that {@link conditions} reads back. */
|
|
232
|
+
export function printCondition(c: Condition) {
|
|
233
|
+
const subject = print(c.subject.node)
|
|
234
|
+
switch (c.op) {
|
|
235
|
+
case 'set': {
|
|
236
|
+
return subject
|
|
237
|
+
}
|
|
238
|
+
case '!set': {
|
|
239
|
+
return `!${subject}`
|
|
240
|
+
}
|
|
241
|
+
case 'has': {
|
|
242
|
+
return `${literal(c.value)} in ${subject}`
|
|
243
|
+
}
|
|
244
|
+
case '!has': {
|
|
245
|
+
return `!(${literal(c.value)} in ${subject})`
|
|
246
|
+
}
|
|
247
|
+
case 'in': {
|
|
248
|
+
return `${subject} in [${c.value.map(literal).join(', ')}]`
|
|
249
|
+
}
|
|
250
|
+
case '!in': {
|
|
251
|
+
return `!(${subject} in [${c.value.map(literal).join(', ')}])`
|
|
252
|
+
}
|
|
253
|
+
default: {
|
|
254
|
+
return `${subject} ${c.op} ${literal(c.value)}`
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Writes conditions as one expression requiring all of them. */
|
|
260
|
+
export function fromConditions(list: readonly Condition[]) {
|
|
261
|
+
return list.map(printCondition).join(' && ')
|
|
262
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Jexl
|
|
3
|
+
* Copyright 2020 Tom Shawver
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** A malformed expression. `offset` is where in the source it went wrong. */
|
|
7
|
+
export class JexlSyntaxError extends Error {
|
|
8
|
+
offset: number
|
|
9
|
+
|
|
10
|
+
constructor(message: string, offset: number) {
|
|
11
|
+
super(message)
|
|
12
|
+
this.name = 'JexlSyntaxError'
|
|
13
|
+
this.offset = offset
|
|
14
|
+
}
|
|
15
|
+
}
|