@jbrowse/jexl 4.0.1 → 5.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 +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 +274 -97
- package/dist/evaluator/compile.js.map +1 -1
- package/dist/grammar.d.ts +19 -9
- package/dist/grammar.js +66 -40
- 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 +274 -97
- package/esm/evaluator/compile.js.map +1 -1
- package/esm/grammar.d.ts +19 -9
- package/esm/grammar.js +66 -39
- 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 +332 -100
- package/src/grammar.ts +123 -42
- 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
package/src/grammar.ts
CHANGED
|
@@ -5,6 +5,16 @@
|
|
|
5
5
|
|
|
6
6
|
/* eslint eqeqeq:0 */
|
|
7
7
|
|
|
8
|
+
import { collectionFunctions } from './collections.ts'
|
|
9
|
+
import {
|
|
10
|
+
addsPairwise,
|
|
11
|
+
anyPair,
|
|
12
|
+
isIn,
|
|
13
|
+
isList,
|
|
14
|
+
matches,
|
|
15
|
+
pairwise
|
|
16
|
+
} from './operators.ts'
|
|
17
|
+
|
|
8
18
|
import type { JexlValue } from './types.ts'
|
|
9
19
|
|
|
10
20
|
export type BinaryOpEval = (left: JexlValue, right: JexlValue) => JexlValue
|
|
@@ -40,6 +50,8 @@ export interface BinaryOp {
|
|
|
40
50
|
* which is how `-x` is distinguished from `a - x`.
|
|
41
51
|
*/
|
|
42
52
|
unaryEval?: (right: JexlValue) => JexlValue
|
|
53
|
+
/** Groups a chain from the right, so `a ^ b ^ c` is `a ^ (b ^ c)`. */
|
|
54
|
+
rightAssociative?: boolean
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
export type UnaryOpEval = (right: JexlValue) => JexlValue
|
|
@@ -52,9 +64,9 @@ export interface UnaryOp {
|
|
|
52
64
|
|
|
53
65
|
/**
|
|
54
66
|
* The punctuation elements of the grammar. Unlike operators these carry no
|
|
55
|
-
* behavior of their own; the Parser
|
|
56
|
-
*
|
|
57
|
-
*
|
|
67
|
+
* behavior of their own; the Parser gives them meaning. The type is a literal
|
|
68
|
+
* union rather than `string` so that `GrammarElement` is a discriminated union,
|
|
69
|
+
* letting operator properties be accessed without casts.
|
|
58
70
|
*/
|
|
59
71
|
export interface SimpleElement {
|
|
60
72
|
type:
|
|
@@ -69,6 +81,7 @@ export interface SimpleElement {
|
|
|
69
81
|
| 'closeParen'
|
|
70
82
|
| 'question'
|
|
71
83
|
| 'semicolon'
|
|
84
|
+
| 'arrow'
|
|
72
85
|
}
|
|
73
86
|
|
|
74
87
|
export type GrammarElement = BinaryOp | UnaryOp | SimpleElement
|
|
@@ -76,20 +89,58 @@ export type GrammarElement = BinaryOp | UnaryOp | SimpleElement
|
|
|
76
89
|
/** A registered function, as jexl calls it once the operands are evaluated. */
|
|
77
90
|
export type GrammarFn = (...args: JexlValue[]) => JexlValue
|
|
78
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Reads `key` off `subject` for `subject.key` and `subject[key]`, in place of
|
|
94
|
+
* a plain property read. Like a registered function, it is handed host values
|
|
95
|
+
* jexl has no type for, so the subject is only promised not to be nullish.
|
|
96
|
+
*/
|
|
97
|
+
export type GetMember = (
|
|
98
|
+
subject: NonNullable<unknown>,
|
|
99
|
+
key: string | number
|
|
100
|
+
) => unknown
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Called once per bare variable name as an expression compiles. A reader it
|
|
104
|
+
* returns replaces `context[name]` for that name; `undefined` keeps it.
|
|
105
|
+
*/
|
|
106
|
+
export type VariableReader = (
|
|
107
|
+
name: string
|
|
108
|
+
) => ((context: Record<string, unknown>) => unknown) | undefined
|
|
109
|
+
|
|
79
110
|
export interface Grammar {
|
|
80
111
|
elements: Record<string, GrammarElement>
|
|
81
112
|
functions: Record<string, GrammarFn>
|
|
113
|
+
getMember?: GetMember
|
|
114
|
+
variableReader?: VariableReader
|
|
82
115
|
}
|
|
83
116
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
117
|
+
const plus = (left: JexlValue, right: JexlValue) =>
|
|
118
|
+
(left as number) + (right as number)
|
|
119
|
+
const minus = (left: JexlValue, right: JexlValue) =>
|
|
120
|
+
(left as number) - (right as number)
|
|
121
|
+
const times = (left: JexlValue, right: JexlValue) =>
|
|
122
|
+
(left as number) * (right as number)
|
|
123
|
+
const divide = (left: JexlValue, right: JexlValue) =>
|
|
124
|
+
(left as number) / (right as number)
|
|
125
|
+
const floorDivide = (left: JexlValue, right: JexlValue) =>
|
|
126
|
+
Math.floor((left as number) / (right as number))
|
|
127
|
+
const modulo = (left: JexlValue, right: JexlValue) =>
|
|
128
|
+
(left as number) % (right as number)
|
|
129
|
+
const power = (left: JexlValue, right: JexlValue) =>
|
|
130
|
+
(left as number) ** (right as number)
|
|
131
|
+
const loose = (left: JexlValue, right: JexlValue) => left == right
|
|
132
|
+
// a missing value orders against nothing, as bcftools skips one; JavaScript
|
|
133
|
+
// alone would read null as 0, making [null, 0.2] < 0.05 true
|
|
134
|
+
const greater = (left: JexlValue, right: JexlValue) =>
|
|
135
|
+
left != null && right != null && (left as number) > (right as number)
|
|
136
|
+
const atLeast = (left: JexlValue, right: JexlValue) =>
|
|
137
|
+
left != null && right != null && (left as number) >= (right as number)
|
|
138
|
+
const less = (left: JexlValue, right: JexlValue) =>
|
|
139
|
+
left != null && right != null && (left as number) < (right as number)
|
|
140
|
+
const atMost = (left: JexlValue, right: JexlValue) =>
|
|
141
|
+
left != null && right != null && (left as number) <= (right as number)
|
|
142
|
+
const equals = (left: JexlValue, right: JexlValue) =>
|
|
143
|
+
isList(left, right) ? anyPair(loose, left, right) : left == right
|
|
93
144
|
|
|
94
145
|
export const getGrammar = (): Grammar => ({
|
|
95
146
|
/**
|
|
@@ -109,75 +160,113 @@ export const getGrammar = (): Grammar => ({
|
|
|
109
160
|
')': { type: 'closeParen' },
|
|
110
161
|
'?': { type: 'question' },
|
|
111
162
|
';': { type: 'semicolon' },
|
|
163
|
+
'=>': { type: 'arrow' },
|
|
112
164
|
'+': {
|
|
113
165
|
type: 'binaryOp',
|
|
114
166
|
precedence: 30,
|
|
115
|
-
eval: (left, right) =>
|
|
167
|
+
eval: (left, right) =>
|
|
168
|
+
addsPairwise(left, right)
|
|
169
|
+
? pairwise(plus, left, right)
|
|
170
|
+
: plus(left, right)
|
|
116
171
|
},
|
|
117
172
|
'-': {
|
|
118
173
|
type: 'binaryOp',
|
|
119
174
|
precedence: 30,
|
|
120
|
-
eval: (left, right) =>
|
|
121
|
-
|
|
175
|
+
eval: (left, right) =>
|
|
176
|
+
isList(left, right) ? pairwise(minus, left, right) : minus(left, right),
|
|
177
|
+
unaryEval: (right) =>
|
|
178
|
+
Array.isArray(right)
|
|
179
|
+
? right.map((value) => -(value as number))
|
|
180
|
+
: -(right as number)
|
|
122
181
|
},
|
|
123
182
|
'*': {
|
|
124
183
|
type: 'binaryOp',
|
|
125
184
|
precedence: 40,
|
|
126
|
-
eval: (left, right) =>
|
|
185
|
+
eval: (left, right) =>
|
|
186
|
+
isList(left, right) ? pairwise(times, left, right) : times(left, right)
|
|
127
187
|
},
|
|
128
188
|
'/': {
|
|
129
189
|
type: 'binaryOp',
|
|
130
190
|
precedence: 40,
|
|
131
|
-
eval: (left, right) =>
|
|
191
|
+
eval: (left, right) =>
|
|
192
|
+
isList(left, right)
|
|
193
|
+
? pairwise(divide, left, right)
|
|
194
|
+
: divide(left, right)
|
|
132
195
|
},
|
|
133
196
|
'//': {
|
|
134
197
|
type: 'binaryOp',
|
|
135
198
|
precedence: 40,
|
|
136
|
-
eval: (left, right) =>
|
|
199
|
+
eval: (left, right) =>
|
|
200
|
+
isList(left, right)
|
|
201
|
+
? pairwise(floorDivide, left, right)
|
|
202
|
+
: floorDivide(left, right)
|
|
137
203
|
},
|
|
138
204
|
'%': {
|
|
139
205
|
type: 'binaryOp',
|
|
140
206
|
precedence: 50,
|
|
141
|
-
eval: (left, right) =>
|
|
207
|
+
eval: (left, right) =>
|
|
208
|
+
isList(left, right)
|
|
209
|
+
? pairwise(modulo, left, right)
|
|
210
|
+
: modulo(left, right)
|
|
142
211
|
},
|
|
143
212
|
'^': {
|
|
144
213
|
type: 'binaryOp',
|
|
145
214
|
precedence: 50,
|
|
146
|
-
|
|
215
|
+
rightAssociative: true,
|
|
216
|
+
eval: (left, right) =>
|
|
217
|
+
isList(left, right) ? pairwise(power, left, right) : power(left, right)
|
|
147
218
|
},
|
|
148
219
|
'==': {
|
|
149
220
|
type: 'binaryOp',
|
|
150
221
|
precedence: 20,
|
|
151
|
-
eval:
|
|
222
|
+
eval: equals
|
|
152
223
|
},
|
|
153
224
|
'!=': {
|
|
154
225
|
type: 'binaryOp',
|
|
155
226
|
precedence: 20,
|
|
156
|
-
eval: (left, right) => left
|
|
227
|
+
eval: (left, right) => !equals(left, right)
|
|
228
|
+
},
|
|
229
|
+
'~': {
|
|
230
|
+
type: 'binaryOp',
|
|
231
|
+
precedence: 20,
|
|
232
|
+
eval: matches
|
|
233
|
+
},
|
|
234
|
+
'!~': {
|
|
235
|
+
type: 'binaryOp',
|
|
236
|
+
precedence: 20,
|
|
237
|
+
eval: (left, right) => !matches(left, right)
|
|
157
238
|
},
|
|
158
239
|
'>': {
|
|
159
240
|
type: 'binaryOp',
|
|
160
241
|
precedence: 20,
|
|
161
|
-
eval: (left, right) =>
|
|
242
|
+
eval: (left, right) =>
|
|
243
|
+
isList(left, right)
|
|
244
|
+
? anyPair(greater, left, right)
|
|
245
|
+
: greater(left, right)
|
|
162
246
|
},
|
|
163
247
|
'>=': {
|
|
164
248
|
type: 'binaryOp',
|
|
165
249
|
precedence: 20,
|
|
166
|
-
eval: (left, right) =>
|
|
250
|
+
eval: (left, right) =>
|
|
251
|
+
isList(left, right)
|
|
252
|
+
? anyPair(atLeast, left, right)
|
|
253
|
+
: atLeast(left, right)
|
|
167
254
|
},
|
|
168
255
|
'<': {
|
|
169
256
|
type: 'binaryOp',
|
|
170
257
|
precedence: 20,
|
|
171
|
-
eval: (left, right) =>
|
|
258
|
+
eval: (left, right) =>
|
|
259
|
+
isList(left, right) ? anyPair(less, left, right) : less(left, right)
|
|
172
260
|
},
|
|
173
261
|
'<=': {
|
|
174
262
|
type: 'binaryOp',
|
|
175
263
|
precedence: 20,
|
|
176
|
-
eval: (left, right) =>
|
|
264
|
+
eval: (left, right) =>
|
|
265
|
+
isList(left, right) ? anyPair(atMost, left, right) : atMost(left, right)
|
|
177
266
|
},
|
|
178
267
|
'&&': {
|
|
179
268
|
type: 'binaryOp',
|
|
180
|
-
precedence:
|
|
269
|
+
precedence: 11,
|
|
181
270
|
evalOnDemand: (left, right) => {
|
|
182
271
|
const leftVal = left.eval()
|
|
183
272
|
if (!leftVal) {
|
|
@@ -197,23 +286,15 @@ export const getGrammar = (): Grammar => ({
|
|
|
197
286
|
return right.eval()
|
|
198
287
|
}
|
|
199
288
|
},
|
|
289
|
+
'??': {
|
|
290
|
+
type: 'binaryOp',
|
|
291
|
+
precedence: 10,
|
|
292
|
+
evalOnDemand: (left, right) => left.eval() ?? right.eval()
|
|
293
|
+
},
|
|
200
294
|
in: {
|
|
201
295
|
type: 'binaryOp',
|
|
202
296
|
precedence: 20,
|
|
203
|
-
eval:
|
|
204
|
-
if (typeof right === 'string') {
|
|
205
|
-
// only a primitive has a meaningful substring form. An absent or
|
|
206
|
-
// structured left operand is not "in" a string, and must not be
|
|
207
|
-
// coerced to '' — every string contains the empty string, so that
|
|
208
|
-
// made `feature.missingAttr in "someString"` true for every feature.
|
|
209
|
-
return typeof left === 'string' ||
|
|
210
|
-
typeof left === 'number' ||
|
|
211
|
-
typeof left === 'boolean'
|
|
212
|
-
? right.includes(String(left))
|
|
213
|
-
: false
|
|
214
|
-
}
|
|
215
|
-
return Array.isArray(right) ? right.includes(left) : false
|
|
216
|
-
}
|
|
297
|
+
eval: isIn
|
|
217
298
|
},
|
|
218
299
|
'!': {
|
|
219
300
|
type: 'unaryOp',
|
|
@@ -243,5 +324,5 @@ export const getGrammar = (): Grammar => ({
|
|
|
243
324
|
* than throw. An error is only appropriate when the function would normally
|
|
244
325
|
* return a value, but cannot due to some other failure.
|
|
245
326
|
*/
|
|
246
|
-
functions: {}
|
|
327
|
+
functions: { ...collectionFunctions }
|
|
247
328
|
})
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,22 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export { Jexl, default } from './Jexl.ts'
|
|
7
|
+
export type { JexlOptions } from './Jexl.ts'
|
|
8
|
+
export type { GetMember, VariableReader } from './grammar.ts'
|
|
7
9
|
export { default as Expression } from './Expression.ts'
|
|
10
|
+
export { analyze } from './analyze.ts'
|
|
11
|
+
export { check, print, recordOf, union } from './check.ts'
|
|
12
|
+
export {
|
|
13
|
+
callSubject,
|
|
14
|
+
conditions,
|
|
15
|
+
fromConditions,
|
|
16
|
+
pathSubject,
|
|
17
|
+
printCondition
|
|
18
|
+
} from './conditions.ts'
|
|
8
19
|
export { default as Lexer } from './Lexer.ts'
|
|
20
|
+
export { JexlSyntaxError } from './errors.ts'
|
|
9
21
|
export { getGrammar } from './grammar.ts'
|
|
22
|
+
export type * from './analyze.ts'
|
|
23
|
+
export type * from './check.ts'
|
|
24
|
+
export type * from './conditions.ts'
|
|
10
25
|
export type * from './types.ts'
|
package/src/operators.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Jexl
|
|
3
|
+
* Copyright 2020 Tom Shawver
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { JexlValue } from './types.ts'
|
|
7
|
+
|
|
8
|
+
type Test = (left: JexlValue, right: JexlValue) => boolean
|
|
9
|
+
type Arithmetic = (left: JexlValue, right: JexlValue) => JexlValue
|
|
10
|
+
|
|
11
|
+
export function isList(left: JexlValue, right: JexlValue) {
|
|
12
|
+
return Array.isArray(left) || Array.isArray(right)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A comparison over list operands: it holds when it holds for any value, as
|
|
17
|
+
* bcftools compares a multi-valued tag, so `AF > 0.05` is true when any
|
|
18
|
+
* allele's frequency is and `CLNSIG == 'Pathogenic'` when any significance is.
|
|
19
|
+
* Each operator calls this only once {@link isList} says it must, so the
|
|
20
|
+
* comparison of two single values stays a direct call V8 can inline.
|
|
21
|
+
*/
|
|
22
|
+
export function anyPair(
|
|
23
|
+
test: Test,
|
|
24
|
+
left: JexlValue,
|
|
25
|
+
right: JexlValue
|
|
26
|
+
): boolean {
|
|
27
|
+
return Array.isArray(left)
|
|
28
|
+
? left.some((l) => anyPair(test, l, right))
|
|
29
|
+
: Array.isArray(right)
|
|
30
|
+
? right.some((r) => anyPair(test, left, r))
|
|
31
|
+
: test(left, right)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasText(value: JexlValue) {
|
|
35
|
+
return (
|
|
36
|
+
typeof value === 'string' ||
|
|
37
|
+
(Array.isArray(value) && value.some((item) => typeof item === 'string'))
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether `+` pairs its operands value by value: only when a list is involved
|
|
43
|
+
* and neither side holds text, so `REF + '>' + ALT` still joins as JavaScript
|
|
44
|
+
* joins, `'A>T,C'`, while `DP + 1` adds.
|
|
45
|
+
*/
|
|
46
|
+
export function addsPairwise(left: JexlValue, right: JexlValue) {
|
|
47
|
+
return isList(left, right) && !hasText(left) && !hasText(right)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Arithmetic over list operands, value by value, as bcftools and R apply it:
|
|
52
|
+
* `AC / AN` divides each count, and a list of one, or a lone value, pairs with
|
|
53
|
+
* every value of the other side. Lists of two other lengths have no pairing
|
|
54
|
+
* and yield undefined.
|
|
55
|
+
*/
|
|
56
|
+
export function pairwise(
|
|
57
|
+
fn: Arithmetic,
|
|
58
|
+
left: JexlValue,
|
|
59
|
+
right: JexlValue
|
|
60
|
+
): JexlValue {
|
|
61
|
+
const ls = Array.isArray(left) ? left : [left]
|
|
62
|
+
const rs = Array.isArray(right) ? right : [right]
|
|
63
|
+
if (ls.length !== rs.length && ls.length !== 1 && rs.length !== 1) {
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
const length = ls.length === 1 ? rs.length : ls.length
|
|
67
|
+
const out: JexlValue[] = new Array(length)
|
|
68
|
+
for (let i = 0; i < length; i++) {
|
|
69
|
+
out[i] = fn(
|
|
70
|
+
ls.length === 1 ? ls[0] : ls[i],
|
|
71
|
+
rs.length === 1 ? rs[0] : rs[i]
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
return out
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const patterns = new Map<string, RegExp>()
|
|
78
|
+
const MAX_PATTERNS = 1000
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The regular expression a pattern string stands for. A leading `(?i)`, the
|
|
82
|
+
* inline flag PCRE, Python and Go read, makes it case-insensitive.
|
|
83
|
+
*/
|
|
84
|
+
function regex(pattern: string) {
|
|
85
|
+
let re = patterns.get(pattern)
|
|
86
|
+
if (!re) {
|
|
87
|
+
try {
|
|
88
|
+
re = pattern.startsWith('(?i)')
|
|
89
|
+
? new RegExp(pattern.slice(4), 'i')
|
|
90
|
+
: new RegExp(pattern)
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error(`Invalid regular expression: ${pattern}`)
|
|
93
|
+
}
|
|
94
|
+
if (patterns.size >= MAX_PATTERNS) {
|
|
95
|
+
patterns.clear()
|
|
96
|
+
}
|
|
97
|
+
patterns.set(pattern, re)
|
|
98
|
+
}
|
|
99
|
+
return re
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Whether a value, or any value of a list, matches a pattern string. */
|
|
103
|
+
export function matches(value: JexlValue, pattern: JexlValue): boolean {
|
|
104
|
+
if (typeof pattern !== 'string') {
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
const re = regex(pattern)
|
|
108
|
+
const test = (v: JexlValue) =>
|
|
109
|
+
(typeof v === 'string' ||
|
|
110
|
+
typeof v === 'number' ||
|
|
111
|
+
typeof v === 'boolean') &&
|
|
112
|
+
re.test(String(v))
|
|
113
|
+
return Array.isArray(value) ? value.some(test) : test(value)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether a value is in a container: a substring of a string, a member of a
|
|
118
|
+
* list or Set, or a key of a Map or plain object. A list on the left is in
|
|
119
|
+
* the container when any of its values is.
|
|
120
|
+
*/
|
|
121
|
+
export function isIn(left: JexlValue, right: JexlValue): boolean {
|
|
122
|
+
if (Array.isArray(left)) {
|
|
123
|
+
return left.some((l) => isIn(l, right))
|
|
124
|
+
}
|
|
125
|
+
if (typeof right === 'string') {
|
|
126
|
+
// only a primitive has a meaningful substring form. An absent or
|
|
127
|
+
// structured left operand is not "in" a string, and must not be coerced
|
|
128
|
+
// to '' — every string contains the empty string
|
|
129
|
+
return typeof left === 'string' ||
|
|
130
|
+
typeof left === 'number' ||
|
|
131
|
+
typeof left === 'boolean'
|
|
132
|
+
? right.includes(String(left))
|
|
133
|
+
: false
|
|
134
|
+
}
|
|
135
|
+
if (Array.isArray(right)) {
|
|
136
|
+
return right.includes(left)
|
|
137
|
+
}
|
|
138
|
+
if (right instanceof Set || right instanceof Map) {
|
|
139
|
+
return right.has(left)
|
|
140
|
+
}
|
|
141
|
+
if (
|
|
142
|
+
typeof right === 'object' &&
|
|
143
|
+
right !== null &&
|
|
144
|
+
(typeof left === 'string' ||
|
|
145
|
+
typeof left === 'number' ||
|
|
146
|
+
typeof left === 'boolean')
|
|
147
|
+
) {
|
|
148
|
+
return Object.hasOwn(right, String(left))
|
|
149
|
+
}
|
|
150
|
+
return false
|
|
151
|
+
}
|