@witchcraft/expressit 0.0.3 → 0.1.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.
Files changed (119) hide show
  1. package/README.md +3 -1
  2. package/dist/Lexer.d.ts +146 -0
  3. package/dist/Lexer.d.ts.map +1 -0
  4. package/dist/Lexer.js +960 -0
  5. package/dist/Parser.d.ts +211 -0
  6. package/dist/Parser.d.ts.map +1 -0
  7. package/dist/Parser.js +1476 -0
  8. package/dist/ast/builders/token.js +1 -1
  9. package/dist/ast/handlers.d.ts +3 -3
  10. package/dist/ast/handlers.d.ts.map +1 -1
  11. package/dist/examples/shortcutContextParser.d.ts +1 -1
  12. package/dist/examples/shortcutContextParser.js +1 -1
  13. package/dist/helpers/errors.js +3 -3
  14. package/dist/helpers/parser/checkParserOpts.js +2 -2
  15. package/dist/helpers/parser/extractPosition.d.ts +2 -6
  16. package/dist/helpers/parser/extractPosition.d.ts.map +1 -1
  17. package/dist/helpers/parser/extractPosition.js +3 -3
  18. package/dist/helpers/parser/getUnclosedRightParenCount.d.ts +2 -3
  19. package/dist/helpers/parser/getUnclosedRightParenCount.d.ts.map +1 -1
  20. package/dist/helpers/parser/getUnclosedRightParenCount.js +4 -4
  21. package/dist/index.d.ts +1 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3 -5
  24. package/dist/package.json.js +27 -41
  25. package/dist/types/ast.d.ts +1 -7
  26. package/dist/types/ast.d.ts.map +1 -1
  27. package/dist/types/errors.d.ts +5 -17
  28. package/dist/types/errors.d.ts.map +1 -1
  29. package/dist/types/errors.js +0 -1
  30. package/dist/types/parser.d.ts.map +1 -1
  31. package/dist/utils/extractTokens.js +1 -1
  32. package/dist/utils/getCursorInfo.d.ts +2 -2
  33. package/dist/utils/getCursorInfo.d.ts.map +1 -1
  34. package/dist/utils/getCursorInfo.js +3 -6
  35. package/dist/utils/getOppositeDelimiter.d.ts.map +1 -1
  36. package/dist/utils/getOppositeDelimiter.js +1 -1
  37. package/dist/utils/prettyAst.js +7 -6
  38. package/package.json +25 -41
  39. package/src/Lexer.ts +704 -0
  40. package/src/Parser.ts +2014 -0
  41. package/src/ast/builders/array.ts +1 -1
  42. package/src/ast/builders/condition.ts +1 -1
  43. package/src/ast/builders/expression.ts +1 -1
  44. package/src/ast/builders/group.ts +1 -1
  45. package/src/ast/builders/pos.ts +1 -1
  46. package/src/ast/builders/token.ts +2 -2
  47. package/src/ast/builders/type.ts +1 -1
  48. package/src/ast/builders/variable.ts +1 -1
  49. package/src/ast/classes/ConditionNode.ts +1 -1
  50. package/src/ast/classes/ErrorToken.ts +1 -1
  51. package/src/ast/classes/ValidToken.ts +2 -2
  52. package/src/ast/handlers.ts +6 -6
  53. package/src/examples/shortcutContextParser.ts +2 -2
  54. package/src/helpers/errors.ts +4 -4
  55. package/src/helpers/general/defaultConditionNormalizer.ts +1 -1
  56. package/src/helpers/parser/checkParserOpts.ts +12 -12
  57. package/src/helpers/parser/extractPosition.ts +4 -8
  58. package/src/helpers/parser/getUnclosedRightParenCount.ts +6 -6
  59. package/src/helpers/parser/parseParserOptions.ts +1 -1
  60. package/src/index.ts +1 -2
  61. package/src/types/ast.ts +1 -8
  62. package/src/types/errors.ts +12 -22
  63. package/src/types/parser.ts +0 -1
  64. package/src/utils/extractTokens.ts +1 -1
  65. package/src/utils/getCursorInfo.ts +6 -5
  66. package/src/utils/getOppositeDelimiter.ts +5 -2
  67. package/src/utils/prettyAst.ts +4 -4
  68. package/dist/grammar/ParserBase.d.ts +0 -51
  69. package/dist/grammar/ParserBase.d.ts.map +0 -1
  70. package/dist/grammar/ParserBase.js +0 -517
  71. package/dist/grammar/createTokens.d.ts +0 -56
  72. package/dist/grammar/createTokens.d.ts.map +0 -1
  73. package/dist/grammar/createTokens.js +0 -844
  74. package/dist/grammar/index.d.ts +0 -3
  75. package/dist/grammar/index.d.ts.map +0 -1
  76. package/dist/grammar/index.js +0 -6
  77. package/dist/methods/autocomplete.d.ts +0 -18
  78. package/dist/methods/autocomplete.d.ts.map +0 -1
  79. package/dist/methods/autocomplete.js +0 -109
  80. package/dist/methods/autoreplace.d.ts +0 -13
  81. package/dist/methods/autoreplace.d.ts.map +0 -1
  82. package/dist/methods/autoreplace.js +0 -36
  83. package/dist/methods/autosuggest.d.ts +0 -28
  84. package/dist/methods/autosuggest.d.ts.map +0 -1
  85. package/dist/methods/autosuggest.js +0 -371
  86. package/dist/methods/evaluate.d.ts +0 -11
  87. package/dist/methods/evaluate.d.ts.map +0 -1
  88. package/dist/methods/evaluate.js +0 -32
  89. package/dist/methods/getBestIndex.d.ts +0 -19
  90. package/dist/methods/getBestIndex.d.ts.map +0 -1
  91. package/dist/methods/getBestIndex.js +0 -53
  92. package/dist/methods/getIndexes.d.ts +0 -17
  93. package/dist/methods/getIndexes.d.ts.map +0 -1
  94. package/dist/methods/getIndexes.js +0 -98
  95. package/dist/methods/index.d.ts +0 -9
  96. package/dist/methods/index.d.ts.map +0 -1
  97. package/dist/methods/index.js +0 -18
  98. package/dist/methods/normalize.d.ts +0 -10
  99. package/dist/methods/normalize.d.ts.map +0 -1
  100. package/dist/methods/normalize.js +0 -98
  101. package/dist/methods/validate.d.ts +0 -11
  102. package/dist/methods/validate.d.ts.map +0 -1
  103. package/dist/methods/validate.js +0 -112
  104. package/dist/parser.d.ts +0 -58
  105. package/dist/parser.d.ts.map +0 -1
  106. package/dist/parser.js +0 -137
  107. package/src/grammar/ParserBase.ts +0 -716
  108. package/src/grammar/createTokens.ts +0 -513
  109. package/src/grammar/index.ts +0 -4
  110. package/src/methods/autocomplete.ts +0 -128
  111. package/src/methods/autoreplace.ts +0 -46
  112. package/src/methods/autosuggest.ts +0 -543
  113. package/src/methods/evaluate.ts +0 -39
  114. package/src/methods/getBestIndex.ts +0 -53
  115. package/src/methods/getIndexes.ts +0 -100
  116. package/src/methods/index.ts +0 -10
  117. package/src/methods/normalize.ts +0 -137
  118. package/src/methods/validate.ts +0 -143
  119. package/src/parser.ts +0 -184
package/src/Parser.ts ADDED
@@ -0,0 +1,2014 @@
1
+ /* eslint-disable max-lines */
2
+ import { get } from "@alanscodelog/utils/get.js"
3
+ import { insert } from "@alanscodelog/utils/insert.js"
4
+ import { isArray } from "@alanscodelog/utils/isArray.js"
5
+ import { isWhitespace } from "@alanscodelog/utils/isWhitespace.js"
6
+ import { setReadOnly } from "@alanscodelog/utils/setReadOnly.js"
7
+ import type { AddParameters , DeepPartial } from "@alanscodelog/utils/types"
8
+ import { unreachable } from "@alanscodelog/utils/unreachable.js"
9
+
10
+ import { pos } from "./ast/builders/pos.js"
11
+ import { ArrayNode } from "./ast/classes/ArrayNode.js"
12
+ import { ConditionNode } from "./ast/classes/ConditionNode.js"
13
+ import { ErrorToken } from "./ast/classes/ErrorToken.js"
14
+ import { ExpressionNode } from "./ast/classes/ExpressionNode.js"
15
+ import { GroupNode } from "./ast/classes/GroupNode.js"
16
+ import { Condition, Expression } from "./ast/classes/index.js"
17
+ import { ValidToken } from "./ast/classes/ValidToken.js"
18
+ import { VariableNode } from "./ast/classes/VariableNode.js"
19
+ import * as handle from "./ast/handlers.js"
20
+ import { applyBoolean } from "./helpers/general/applyBoolean.js"
21
+ import { applyPrefix } from "./helpers/general/applyPrefix.js"
22
+ import { checkParserOpts } from "./helpers/parser/checkParserOpts.js"
23
+ import { extractPosition } from "./helpers/parser/extractPosition.js"
24
+ import { getUnclosedRightParenCount } from "./helpers/parser/getUnclosedRightParenCount.js"
25
+ import { parseParserOptions } from "./helpers/parser/parseParserOptions.js"
26
+ import { seal } from "./helpers/parser/seal.js"
27
+ import { $C, $T, Lexer,type RealTokenType, type Token, type TokenCategoryType, type TokenType } from "./Lexer.js"
28
+ import type { ParserResults, TokenBooleanTypes } from "./types/ast.js"
29
+ import { type AnyToken, type Completion, type Position, type Suggestion,SUGGESTION_TYPE, TOKEN_TYPE } from "./types/index.js"
30
+ import type { FullParserOptions, KeywordEntry, ParserOptions, ValidationQuery, ValueQuery } from "./types/parser.js"
31
+ import { extractTokens } from "./utils/extractTokens.js"
32
+ import { getCursorInfo } from "./utils/getCursorInfo.js"
33
+ import { getSurroundingErrors } from "./utils/getSurroundingErrors.js"
34
+
35
+ const OPPOSITE = {
36
+ [TOKEN_TYPE.AND]: TOKEN_TYPE.OR,
37
+ [TOKEN_TYPE.OR]: TOKEN_TYPE.AND,
38
+ }
39
+ function isEqualSet(setA: Set<any>, setB: Set<any>): boolean {
40
+ if (setA.size !== setB.size) return false
41
+ for (const key of setA) {
42
+ if (!setB.has(key)) return false
43
+ }
44
+ return true
45
+ }
46
+
47
+ const defaultNodeDirs = {
48
+ before: false,
49
+ after: false,
50
+ }
51
+
52
+ const createDefaultRequires = (partial: DeepPartial<Suggestion["requires"]> = {}): Suggestion["requires"] => ({
53
+ whitespace: {
54
+ ...defaultNodeDirs,
55
+ ...(partial.whitespace ? partial.whitespace : {}),
56
+ },
57
+ group: partial.group ?? false,
58
+ prefix: partial.prefix ?? false,
59
+ })
60
+
61
+ /** Returns if valid token requires whitespace if none between cursor and token. */
62
+ const tokenRequiresWhitespace = (validToken: ValidToken | undefined, whitespace: boolean, wordOps: KeywordEntry[]): boolean => {
63
+ if (whitespace || validToken === undefined) return false
64
+ return validToken.type === TOKEN_TYPE.VALUE ||
65
+ ([TOKEN_TYPE.AND, TOKEN_TYPE.OR, TOKEN_TYPE.NOT].includes(validToken.type) &&
66
+ wordOps.find(_ => _.value === validToken.value) !== undefined)
67
+ }
68
+ const tokenVariable = [TOKEN_TYPE.BACKTICK, TOKEN_TYPE.DOUBLEQUOTE, TOKEN_TYPE.SINGLEQUOTE, TOKEN_TYPE.VALUE, TOKEN_TYPE.REGEX]
69
+
70
+ /**
71
+ * Creates the main parser class which handles all functionality (evaluation, validation, etc).
72
+ */
73
+ export class Parser<T extends {} = {}> {
74
+ // needed for evaluate and validate so they are only checked on demand
75
+ private evaluationOptionsChecked: boolean = false
76
+
77
+ // eslint-disable-next-line @typescript-eslint/naming-convention
78
+ _checkEvaluationOptions(): void {
79
+ if (!this.evaluationOptionsChecked) {
80
+ checkParserOpts(this.options, true)
81
+ this.evaluationOptionsChecked = true
82
+ }
83
+ }
84
+
85
+ private validationOptionsChecked: boolean = false
86
+
87
+ // eslint-disable-next-line @typescript-eslint/naming-convention
88
+ _checkValidationOptions(): void {
89
+ if (!this.validationOptionsChecked) {
90
+ checkParserOpts(this.options, false, true)
91
+ this.validationOptionsChecked = true
92
+ }
93
+ }
94
+
95
+ options: FullParserOptions<T>
96
+
97
+ private readonly lexer: Lexer
98
+
99
+ private readonly $: Record<$T, RealTokenType<$T>>
100
+
101
+ private readonly $categories: Partial<Record<$C, TokenCategoryType<$C>>>
102
+
103
+ private readonly info: Pick<ReturnType<Lexer["calculateSymbolInfo"]>, "expandedSepAlsoCustom" | "customOpAlsoNegation">
104
+
105
+ constructor(options?: ParserOptions<T>) {
106
+ const opts = parseParserOptions<T>(options ?? {})
107
+ checkParserOpts<T>(opts)
108
+ this.options = opts
109
+ this.lexer = new Lexer(opts)
110
+ this.$ = this.lexer.$
111
+ this.$categories = this.lexer.$categories
112
+ this.info = {
113
+ expandedSepAlsoCustom: this.lexer.symbols.expandedSepAlsoCustom,
114
+ customOpAlsoNegation: this.lexer.symbols.customOpAlsoNegation,
115
+ }
116
+ }
117
+
118
+ state: {
119
+ rawInput: string
120
+ lexedTokens: Token<$T>[]
121
+ index: number
122
+ shift: number
123
+ } = {
124
+ rawInput: "",
125
+ lexedTokens: [],
126
+ index: 0,
127
+ shift: 0,
128
+ }
129
+
130
+ /**
131
+ * This is exposed mainly for debugging purposes. Use parse directly instead.
132
+ */
133
+ lex(input: string): {
134
+ tokens: Token<$T> []
135
+ shift: number
136
+ rawInput: string
137
+ } {
138
+ if (isWhitespace(input)) {
139
+ return { tokens: [], shift: 0, rawInput: input }
140
+ }
141
+ let lexed = this.lexer.tokenize(input)
142
+ /**
143
+ * The parser can't handle unmatched right parens (i.e. left is missing) so we just insert them and shift the locations of all the tokens. Then the parser is designed to ignore parenthesis we added at the start and just return undefined for that rule as if the parenthesis didn't exist.
144
+ */
145
+
146
+
147
+ const shift = getUnclosedRightParenCount(lexed)
148
+ const rawInput = input
149
+ if (shift) {
150
+ input = "(".repeat(shift) + input
151
+ lexed = this.lexer.tokenize(input)
152
+ }
153
+ const lexedTokens = lexed.filter(token => {
154
+ const tokenType = this.getTokenType(token.type)
155
+ if (tokenType) {
156
+ return !tokenType.skip
157
+ } else {
158
+ throw new Error(`Unknown token type ${token.type}`)
159
+ }
160
+ })
161
+ return { tokens: lexedTokens, shift, rawInput }
162
+ }
163
+
164
+ /**
165
+ * Parse an input string into an AST.
166
+ * It can also parse the result from `lex`, but that is really only for internal use.
167
+ */
168
+ parse(
169
+ input: string
170
+ | {
171
+ tokens: Token<$T> []
172
+ shift: number
173
+ rawInput: string
174
+ },
175
+ ): ParserResults {
176
+ // eslint-disable-next-line prefer-rest-params
177
+ const doSeal = arguments[1]?.seal ?? true
178
+ if (typeof input === "string" && isWhitespace(input)) {
179
+ return handle.token.value(undefined, { start: 0, end: 0 }) as any
180
+ }
181
+ const { tokens: lexedTokens, shift, rawInput } = typeof input === "string" ? this.lex(input) : input
182
+
183
+ this.state = {
184
+ rawInput,
185
+ shift,
186
+ index: -1,
187
+ lexedTokens,
188
+ }
189
+ const res = this.ruleMain()
190
+ if (doSeal) {
191
+ seal(res)
192
+ }
193
+ this.state = {
194
+ rawInput: "",
195
+ shift: 0,
196
+ index: -1,
197
+ lexedTokens: [],
198
+ }
199
+ return res
200
+ }
201
+
202
+ subParserOne?: Parser<T> & {
203
+ parse: AddParameters<Parser<T>["parse"], [{ seal: boolean }]>
204
+ }
205
+
206
+ subParserTwo?: Parser<T> & {
207
+ parse: AddParameters<Parser<T>["parse"], [{ seal: boolean }]>
208
+ }
209
+
210
+
211
+ createSubParserIfNotExists(opts: ParserOptions<T>, which: "One" | "Two" = "One"): Parser["subParserOne"] {
212
+ if (this[`subParser${which}`] === undefined) {
213
+ this[`subParser${which}`] = new Parser(opts)
214
+ }
215
+ return this[`subParser${which}`]!
216
+ }
217
+
218
+
219
+ transformCategoryToken<TC extends $C>(
220
+ token: Token,
221
+ categoryToken: TokenCategoryType<TC>,
222
+ ): Token<TC> {
223
+ return {
224
+ ...token,
225
+ type: categoryToken.type,
226
+ }
227
+ }
228
+
229
+ getCategoryTokens<TType extends $C>(
230
+ type: TType,
231
+ ): TokenCategoryType<TType>["entries"] | undefined {
232
+ return this.$categories[type as $C]?.entries as any
233
+ }
234
+
235
+ getTokenType(type: $T | $C): TokenType<$T> | undefined {
236
+ return this.$[type as any as $T] as any
237
+ }
238
+
239
+ isExactType<TType extends $T>(token: Token, type: TType): token is Token<TType> {
240
+ if (this.$[type]) {
241
+ return this.isType(token, type)
242
+ }
243
+ return false
244
+ }
245
+
246
+ isType(token: Token | undefined, type: $T | $C): boolean {
247
+ if (token === undefined) return false
248
+ if (token.type === type) return true
249
+ const tokenType = this.getTokenType(token.type)
250
+
251
+ if (tokenType?.type === type) return true
252
+ const category = this.$categories[type as $C]
253
+ if (category?.entries[token.type as $T] !== undefined) {
254
+ return true
255
+ }
256
+ return false
257
+ }
258
+
259
+ createErrorToken(type: $T, index?: number): Token {
260
+ return {
261
+ type,
262
+ value: "",
263
+ startOffset: index ?? this.state.index,
264
+ endOffset: index ?? this.state.index,
265
+ isError: true,
266
+ }
267
+ }
268
+
269
+ processToken<TDefined extends boolean = boolean>(token?: Pick<Token, "value" | "startOffset" | "endOffset">): [TDefined extends true ? string : string | undefined, Position] {
270
+ if (token === undefined) {
271
+ return [undefined as any, extractPosition({ startOffset: 0, endOffset: 0 }, this.state.shift)]
272
+ } else {
273
+ return [token.value, extractPosition(token, this.state.shift)]
274
+ }
275
+ }
276
+
277
+
278
+ peek(n = 1): Token<$T> | undefined {
279
+ return this.state.lexedTokens[this.state.index + n]
280
+ }
281
+
282
+ nextIsEof(): boolean {
283
+ return this.peek(1) === undefined
284
+ }
285
+
286
+ consumeAny(): Token<$T> {
287
+ return this.consume(this.peek(1)?.type)
288
+ }
289
+
290
+ consume<
291
+ TType extends $T | $C,
292
+ >(
293
+ type: TType | undefined,
294
+ ): Token<TType> {
295
+ if (type === undefined) {
296
+ throw new Error("type is undefined")
297
+ }
298
+ const nextToken = this.peek(1)
299
+ if (nextToken === undefined) {
300
+ throw new Error(`Reached end of input without consuming a token of type ${type}`)
301
+ }
302
+ if (this.$categories[type as $C] !== undefined) {
303
+ const categoryToken = this.$categories[type as $C]
304
+ const tokenType = categoryToken?.entries[nextToken.type as $T]
305
+ if (categoryToken && tokenType) {
306
+ this.state.index++
307
+ return this.transformCategoryToken(nextToken, categoryToken) as Token<TType>
308
+ } else {
309
+ throw new Error("here")
310
+ }
311
+ } else {
312
+ const tokenType = this.getTokenType(type as $T)
313
+ if (tokenType !== undefined) {
314
+ if (nextToken?.type === tokenType.type) {
315
+ this.state.index++
316
+ return nextToken as any
317
+ } else {
318
+ throw new Error(`Expected token type ${tokenType.type}, got ${nextToken?.type}`)
319
+ }
320
+ }
321
+ }
322
+ throw new Error(`Unknown token type ${type}`)
323
+ }
324
+
325
+ saveState(): Parser["state"] {
326
+ return { ...this.state }
327
+ }
328
+
329
+ restoreState(state: Parser["state"]): void {
330
+ this.state = state // careful, we assume this is an untouched copy
331
+ }
332
+
333
+ ruleMain(): ParserResults {
334
+ const res = this.ruleBool("OR")
335
+ if (res === undefined) {
336
+ const error = handle.token.value(undefined, { start: 0, end: 0 })
337
+ return error
338
+ }
339
+ return res
340
+ }
341
+
342
+ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
343
+ ruleBool<TType extends"AND" | "OR">(type: TType) {
344
+ const OP_TYPE = type === "AND" ? $C.OPERATOR_AND : $C.OPERATOR_OR
345
+
346
+ const pairs: any[][] = [] as any
347
+ let next = this.peek(1)
348
+
349
+ while (pairs.length < 1 || pairs[pairs.length - 1]?.[1] !== undefined) {
350
+ const exp = type === "AND" ? this.ruleCondition() : this.ruleBool("AND")
351
+ next = this.peek(1)
352
+ const canAttemptErrorRecovery = type === "AND"
353
+ ? ["error", "and"].includes(this.options.onMissingBooleanOperator)
354
+ : this.options.onMissingBooleanOperator === "or"
355
+ const extras: any[] = []
356
+ if (
357
+ canAttemptErrorRecovery
358
+ && (
359
+ this.isType(next, $C.VALUE)
360
+ || this.isType(next, $C.QUOTE_ANY)
361
+ || this.isType(next, $T.PAREN_L)
362
+ || this.isType(next, $T.EXP_PROP_OP)
363
+ || this.isType(next, $T.REGEX_START)
364
+ || this.isType(next, $T.CUSTOM_PROP_OP)
365
+ )
366
+ ) {
367
+ let state = this.saveState()
368
+ let cond = this.ruleCondition()
369
+ if (type === "AND") {
370
+ let dummyOp
371
+ while (cond !== undefined) {
372
+ if (this.options.onMissingBooleanOperator === "and") {
373
+ // the operator is missing between the previous token and this exp
374
+ const prev = this.peek(-1)!
375
+ const start = prev.endOffset! + 1
376
+ dummyOp = handle.operator.and("", pos({ start }, { fill: true }))
377
+ }
378
+ extras.push([dummyOp, cond])
379
+ state = this.saveState()
380
+ cond = this.ruleCondition()
381
+ }
382
+ // todo i don't think we need to backtrack
383
+ this.restoreState(state)
384
+ } else {
385
+ // the operator is missing between the previous token and this exp
386
+ const prev = this.peek(-1)!
387
+ const start = prev.endOffset! + 1
388
+ const dummyOp = handle.operator.or("", pos({ start }, { fill: true }))
389
+ extras.push([dummyOp, cond])
390
+ }
391
+ next = this.peek(1)
392
+ }
393
+ const sepToken = this.isType(next, OP_TYPE) && next
394
+ ? type === "AND"
395
+ ? handle.operator.and(...this.processToken<true>(this.consume(next.type)))
396
+ : handle.operator.or(...this.processToken<true>(this.consume(next.type)))
397
+ : undefined
398
+
399
+ pairs.push([
400
+ exp,
401
+ sepToken,
402
+ ])
403
+ next = this.peek(1)
404
+ for (const extra of extras) {
405
+ pairs[pairs.length - 1].splice(1, 1, extra[0])
406
+ pairs.push([extra[1]])
407
+ }
408
+ }
409
+
410
+ if (pairs.length === 0 && this.isType(this.peek(1), OP_TYPE)) {
411
+ next = this.peek(-1)
412
+ let state = this.saveState()
413
+ while (this.isType(next, $C.OPERATOR_AND)) {
414
+ const token = this.consume($C.OPERATOR_AND)
415
+ pairs.push([
416
+ undefined,
417
+ type === "AND"
418
+ ? handle.operator.and(...this.processToken<true>(token))
419
+ : handle.operator.or(...this.processToken<true>(token)),
420
+ ])
421
+ next = this.peek(-1)
422
+ while (this.isType(next, $C.VALUE) || this.isType(next, $C.QUOTE_ANY) || this.isType(next, $T.PAREN_L)) {
423
+ pairs.push([this.ruleCondition()])
424
+ next = this.peek(-1)
425
+ }
426
+ state = this.saveState()
427
+ }
428
+ this.restoreState(state)
429
+ }
430
+
431
+ if (type === "AND" && pairs.length === 0) return undefined
432
+ // handle situations like `a ||` where b is missing
433
+ let res = pairs[pairs.length - 1][0]
434
+ for (let i = pairs.length - 1; i > 0; i--) {
435
+ const before = pairs[i - 1]
436
+ if (type === "OR" && res === undefined && before === undefined) return undefined
437
+ res = handle.expression(before[0], before[1], res)
438
+ }
439
+ return res
440
+ }
441
+
442
+ ruleCondition(): ConditionNode | GroupNode | ConditionNode<boolean> | undefined {
443
+ const not = this.ruleNot()
444
+ const property = this.ruleConditionProperty()
445
+ const propVal = property?.prop?.value === undefined
446
+ ? undefined
447
+ : property.prop.value instanceof ErrorToken
448
+ ? ""
449
+ : property.prop.value.value
450
+
451
+ const propOpVal = property?.rest?.propertyOperator === undefined
452
+ ? undefined
453
+ : property.rest.propertyOperator instanceof ErrorToken
454
+ ? ""
455
+ : property.rest.propertyOperator?.value
456
+
457
+
458
+ const isExpanded = (property?.rest?.sepL ?? property?.rest?.sepR) !== undefined
459
+
460
+ const convertRegexValues = typeof this.options.regexValues === "function"
461
+ && !this.options.regexValues(propVal, propOpVal, isExpanded)
462
+
463
+ const convertArrayValues = typeof this.options.arrayValues === "function"
464
+ && !this.options.arrayValues(propVal, propOpVal, isExpanded)
465
+
466
+ let value = this.ruleConditionValue(property, { convertRegexValues, convertArrayValues })
467
+
468
+ let group
469
+ if (!(value instanceof ArrayNode)
470
+ && !isArray(value)
471
+ && (!value || this.options.prefixableGroups)
472
+ && this.isType(this.peek(1), $T.PAREN_L) // is not already plain group
473
+ ) {
474
+ group = this.rulePlainGroup({ onlyValues: property !== undefined, convertRegexValues, convertArrayValues })
475
+ }
476
+
477
+ if (isArray(value)) {
478
+ group = value
479
+ value = undefined
480
+ }
481
+ if (convertRegexValues && value instanceof VariableNode && value.quote?.left.type === TOKEN_TYPE.REGEX) {
482
+ value = handle.variable(undefined, undefined, handle.token.value(
483
+ (value.quote?.left?.value ?? "") + (value.value.value ?? "") + (value.quote?.right?.value ?? ""),
484
+ pos(value),
485
+ ), undefined) as ReturnType<Parser["ruleVariable"]>
486
+ }
487
+ if (group) {
488
+ if (property) {
489
+ return handle.condition(not, property?.prop, property?.rest, handle.group(undefined, undefined, ...group))
490
+ }
491
+ if (value) {
492
+ return handle.group(undefined, handle.condition(not, undefined, undefined, value), ...group)
493
+ }
494
+ return handle.group(not, value, ...group)
495
+ }
496
+ if ([not, property, value].every(_ => _ === undefined)) return undefined
497
+
498
+ return handle.condition(not, property?.prop, property?.rest, value as any)
499
+ }
500
+
501
+ ruleConditionValue(
502
+ property: ReturnType<Parser<T>["ruleConditionProperty"]>,
503
+ { convertRegexValues = false, convertArrayValues = false }:
504
+ { convertRegexValues?: boolean, convertArrayValues?: boolean } = {},
505
+ ): ReturnType<Parser["rulePlainGroup"]>
506
+ | ReturnType<Parser["rulePlainBracketGroup"]>
507
+ | ReturnType<Parser["ruleVariable"]>
508
+ | undefined
509
+ {
510
+ const next = this.peek(1)
511
+ const next2 = this.peek(2)
512
+ const next3 = this.peek(3)
513
+ const next4 = this.peek(4)
514
+ if (this.options.prefixableGroups
515
+ && property === undefined
516
+ && next?.type !== $T.PAREN_L // moves to parsing group below instead
517
+ && (
518
+ (
519
+ this.isType(next, $C.VALUE)
520
+ && (
521
+ this.isType(next2, $T.PAREN_L) // a(
522
+ || (this.isType(next2, $C.QUOTE_ANY) && this.isType(next3, $T.PAREN_L)) // a"(
523
+ )
524
+ )
525
+ || (
526
+ this.isType(next, $C.QUOTE_ANY)
527
+ && (
528
+ this.isType(next2, $T.PAREN_L) // "(
529
+ || (this.isType(next2, $C.VALUE)
530
+ && (
531
+ this.isType(next3, $T.PAREN_L) || // "a(
532
+ (this.isType(next3, $C.QUOTE_ANY) && this.isType(next4, $T.PAREN_L)) // "a"(
533
+ )
534
+ )
535
+ )
536
+ )
537
+ )
538
+ ) {
539
+ const res = this.ruleVariable({ unprefixed: true })
540
+ if (res) return res
541
+ }
542
+ if (!this.isType(next, $T.PAREN_L)) {
543
+ const res = this.ruleVariable({ unprefixed: false })
544
+ if (res) return res
545
+ }
546
+ if (this.isType(next, $T.PAREN_L)) {
547
+ const res = this.rulePlainGroup({ onlyValues: property !== undefined, convertRegexValues, convertArrayValues })
548
+ if (res) return res
549
+ }
550
+ if (this.isType(next, $T.BRACKET_L)) {
551
+ const res = this.rulePlainBracketGroup({ convertArrayValues })
552
+ if (res) return res
553
+ }
554
+ return undefined
555
+ }
556
+
557
+ rulePlainGroup(
558
+ { onlyValues = false, convertRegexValues = false, convertArrayValues = false }:
559
+ { onlyValues?: boolean, convertRegexValues?: boolean, convertArrayValues?: boolean } = {},
560
+ ): [
561
+ ValidToken<TOKEN_TYPE.PARENL> | undefined,
562
+ GroupNode["expression"],
563
+ ValidToken<TOKEN_TYPE.PARENR> | undefined,
564
+ ] {
565
+ const parenL = this.ruleParenL()
566
+ let parenLeftCount = 0
567
+ let start: undefined | number
568
+ let end: undefined | number
569
+ const condition = !onlyValues ? this.ruleBool("OR") : undefined
570
+
571
+ /**
572
+ * The following a bit of a hack to ignore forbidden expressions in groups when used as values (it would make no sense to do something like `prop:op(prop:op(...)))` or `prop:op:(prefix(...))`).
573
+ *
574
+ * Doing this from the tokenizer is very complicated because it would require keeping track of a lot of state since we need to know when a group follows something that even looks like a property/operator. Doing it from the parser is possible, but it would involve ignoring lots of token types and converting them.
575
+ *
576
+ * This way we just consume all input until the correct next matching paren (or EOF) and re-parse it with a restricted version of the parser, which is easier to understand.
577
+ *
578
+ * Performance wise this should not be a problem since at most we add the time of one initialization per Parser/ParserBase class instance and only on demand. After that the parser is re-used when needed for any future parse calls. Additionally it only needs to be called once for the outer group used in a property value (i.e. `prop:OP:((()))` will only cause a single "sub parse").
579
+ */
580
+
581
+ if (onlyValues && !this.nextIsEof()) {
582
+ while (
583
+ !this.nextIsEof()
584
+ && (!this.isType(this.peek(1), $T.PAREN_R) || parenLeftCount !== 0)
585
+ ) {
586
+ const token = this.consumeAny()
587
+ start ??= extractPosition(token, this.state.shift).start
588
+ if (token.type === $T.PAREN_L) {
589
+ parenLeftCount++
590
+ }
591
+ if (token.type === $T.PAREN_R) {
592
+ parenLeftCount--
593
+ }
594
+ }
595
+ }
596
+
597
+ if (start !== undefined) {
598
+ end ??= extractPosition(this.peek(0)!, this.state.shift).end
599
+ }
600
+ const parenR = this.isType(this.peek(1), $T.PAREN_R) ? this.ruleParenR() : undefined
601
+ if (start !== undefined) {
602
+ const subInput = this.state.rawInput.slice(start, end)
603
+ this.createSubParserIfNotExists({
604
+ ...this.options,
605
+ customPropertyOperators: [],
606
+ expandedPropertySeparator: undefined,
607
+ regexValues: convertRegexValues,
608
+ arrayValues: convertArrayValues,
609
+ }, "One")
610
+ const parsed = this.subParserOne!.parse(" ".repeat(start) + subInput, { seal: false })
611
+ return [parenL, parsed, parenR]
612
+ }
613
+ return [parenL, condition, parenR]
614
+ }
615
+
616
+ rulePlainBracketGroup(
617
+ { convertArrayValues = false }:
618
+ { convertArrayValues?: boolean } = {},
619
+ ): ArrayNode | VariableNode {
620
+ const bracketL = this.ruleBracketL()
621
+
622
+ const values: any[] = []
623
+
624
+ if (!convertArrayValues) {
625
+ let state = this.saveState()
626
+ let variable = this.ruleVariable({ unprefixed: false })
627
+ while (variable !== undefined) {
628
+ values.push(variable)
629
+ state = this.saveState()
630
+ variable = this.ruleVariable({ unprefixed: false })
631
+ }
632
+ this.restoreState(state)
633
+ } else if (convertArrayValues && !this.nextIsEof()) {
634
+ while (
635
+ !this.nextIsEof()
636
+ && !this.isType(this.peek(1), $T.BRACKET_R)
637
+ ) {
638
+ this.consumeAny()
639
+ }
640
+ }
641
+ const bracketR = this.isType(this.peek(1), $T.BRACKET_R) ? this.ruleBracketR() : undefined
642
+ if (bracketL === undefined) throw new Error("bracketL is undefined, peek before using rule.")
643
+ if (!convertArrayValues) {
644
+ return handle.array(bracketL, values, bracketR)
645
+ }
646
+ const start = bracketL.start
647
+ const end = bracketR?.end
648
+ /**
649
+ * Similar problem as with plain groups above.
650
+ */
651
+ const subInput = this.state.rawInput.slice(start, end)
652
+ this.createSubParserIfNotExists({
653
+ ...this.options,
654
+ customPropertyOperators: [],
655
+ expandedPropertySeparator: undefined,
656
+ arrayValues: false,
657
+ }, "Two")
658
+ const parsed = this.subParserTwo!.parse(" ".repeat(start) + subInput, { seal: false })
659
+ if (parsed instanceof ConditionNode) {
660
+ return parsed.value as ArrayNode
661
+ }
662
+ if (parsed instanceof ErrorToken || parsed instanceof ExpressionNode || parsed instanceof GroupNode) {
663
+ unreachable("parsed.value should not be an ErrorToken, ExpressionNode, or GroupNode.")
664
+ }
665
+ return parsed
666
+ }
667
+
668
+ ruleConditionProperty(): {
669
+ prop?: VariableNode
670
+ rest: Parameters<typeof handle.condition>[2]
671
+ } | undefined {
672
+ const current = this.peek(0)
673
+ const next = this.peek(1)
674
+ const next2 = this.peek(2)
675
+ if (this.isType(next, $T.EXP_PROP_OP)
676
+ || this.isType(next, $T.CUSTOM_PROP_OP)
677
+ || (this.isType(next, $T.VALUE_UNQUOTED) && (
678
+ this.isType(next2, $T.EXP_PROP_OP)
679
+ || this.isType(next2, $T.CUSTOM_PROP_OP)
680
+ ))
681
+ || (
682
+ this.info.customOpAlsoNegation
683
+ && (
684
+ this.isType(next2, $T.SYM_NOT)
685
+ || (this.isType(current, $T.SYM_NOT) && this.isType(next, $T.SYM_NOT))
686
+ )
687
+ )
688
+ ) {
689
+ return this.ruleProperty()
690
+ }
691
+ return undefined
692
+ }
693
+
694
+ ruleProperty(): {
695
+ prop?: VariableNode
696
+ rest: Parameters<typeof handle.condition>[2]
697
+ } {
698
+ const prop = this.ruleVariable({ unprefixed: true })
699
+ const next = this.peek(1)
700
+ let rest: Parameters<typeof handle.condition>[2] = {} as any
701
+ if (this.isType(next, $T.EXP_PROP_OP)) {
702
+ const sepL = handle.token.sep(...this.processToken<true>(this.consume($T.EXP_PROP_OP)))
703
+ const op = this.isType(this.peek(1), $T.VALUE_UNQUOTED)
704
+ ? handle.token.value(...this.processToken<true>(this.consume($T.VALUE_UNQUOTED)))
705
+ : undefined
706
+ const sepR = this.isType(this.peek(1), $T.EXP_PROP_OP)
707
+ ? handle.token.sep(...this.processToken<true>(this.consume($T.EXP_PROP_OP)))
708
+ : undefined
709
+ if (this.info.expandedSepAlsoCustom && op === undefined && sepR === undefined) {
710
+ setReadOnly(sepL, "type", TOKEN_TYPE.OP_CUSTOM as any)
711
+ rest = {
712
+ sepL: undefined,
713
+ sepR,
714
+ propertyOperator: sepL as any as AnyToken<TOKEN_TYPE.OP_CUSTOM>,
715
+ }
716
+ } else {
717
+ rest = { sepL, sepR, propertyOperator: op }
718
+ }
719
+ } else if (this.isType(next, $T.CUSTOM_PROP_OP)) {
720
+ const op = handle.token.custom(...this.processToken(this.consume($T.CUSTOM_PROP_OP)))
721
+ rest = { propertyOperator: op }
722
+ } else if (this.info.customOpAlsoNegation && this.isType(next, $T.SYM_NOT)) {
723
+ const op = handle.token.custom(...this.processToken(this.consume($T.SYM_NOT)))
724
+ rest = { propertyOperator: op }
725
+ }
726
+ return { prop, rest }
727
+ }
728
+
729
+ ruleVariable({
730
+ unprefixed = false,
731
+ }: {
732
+ unprefixed?: boolean
733
+ } = {}): VariableNode | undefined {
734
+ const prefix = this.ruleVariablePrefix({ onlyToken: true, unprefixed })
735
+
736
+ const next = this.peek(1)
737
+ const next2 = this.peek(2)
738
+ const next3 = this.peek(3)
739
+ // quoted values
740
+ if (next && (this.isExactType(next, $T.QUOTE_DOUBLE)
741
+ || this.isExactType(next, $T.QUOTE_SINGLE)
742
+ || this.isExactType(next, $T.QUOTE_BACKTICK)
743
+ )) {
744
+ const quoteType = next.type
745
+ if (next2?.type === quoteType) {
746
+ // value is missing
747
+ const quoteL = this.ruleQuote(quoteType)
748
+ const quoteR = this.ruleQuote(quoteType)
749
+ return handle.variable(undefined, quoteL, undefined, quoteR)
750
+ }
751
+ if (next3?.type === next.type) {
752
+ const quoteL = this.ruleQuote(quoteType)
753
+ const value = this.isType(next2, $T.VALUE_UNQUOTED) ? this.ruleValueUnquoted({ }) : this.ruleValueNot(quoteType)
754
+ const quoteR = this.ruleQuote(quoteType)
755
+ const prefixToken = prefix ? handle.token.value(...this.processToken<true>(prefix)) : undefined
756
+ return handle.variable(prefixToken, quoteL, value, quoteR)
757
+ }
758
+ }
759
+ if (this.isType(next, $C.REGEX_ANY)) {
760
+ // this is safe since the start can never match flags
761
+ const quoteL = this.ruleRegexAny() as ValidToken<TOKEN_TYPE.REGEX>
762
+ // unlike other values, regexes will swallow all input if incorrect
763
+ const maybeValue = this.peek(1)
764
+ // note the inversion (todo inverse map)
765
+ const value = this.isType(maybeValue, $T.VALUE_REGEX)
766
+ ? this.ruleValueNot($C.REGEX_ANY)
767
+ : undefined
768
+
769
+ const quoteR = this.isType(this.peek(1), $C.REGEX_ANY) ? this.ruleRegexAny() : undefined
770
+ const args = isArray(quoteR) ? quoteR : [quoteR, undefined] as const
771
+ return handle.variable(undefined, quoteL, value, args[0], args[1] as any)
772
+ }
773
+ if (this.isType(next, $T.VALUE_UNQUOTED) && this.isType(next2, $C.QUOTE_ANY)) {
774
+ const value = this.ruleValueUnquoted()
775
+ const quoteR = this.ruleValueDelimAny()
776
+ return handle.variable(undefined, undefined, value, quoteR)
777
+ }
778
+ if (this.isType(next, $C.QUOTE_ANY)) {
779
+ const quoteToken = next as Token<$T.QUOTE_BACKTICK | $T.QUOTE_DOUBLE | $T.QUOTE_SINGLE>
780
+ const quoteL = this.ruleValueDelimAny()
781
+ const maybeValue = this.peek(1)
782
+ const value = !quoteL && this.isType(maybeValue, $T.VALUE_UNQUOTED)
783
+ ? this.ruleValueUnquoted()
784
+ // todo, move inverse quote map out of ruleValueNot
785
+ : quoteL && this.isType(maybeValue, quoteToken.type.replace("QUOTE", "VALUE_FOR") as any)
786
+ ? this.ruleValueNot(quoteToken.type)
787
+ : undefined
788
+ return handle.variable(undefined, quoteL, value, undefined)
789
+ }
790
+ if (this.isType(next, $T.VALUE_UNQUOTED)) {
791
+ const value = this.ruleValueUnquoted()
792
+ return handle.variable(undefined, undefined, value, undefined)
793
+ }
794
+ return undefined
795
+ }
796
+
797
+ ruleValueDelimAny(): ValidToken<TOKEN_TYPE.SINGLEQUOTE | TOKEN_TYPE.DOUBLEQUOTE | TOKEN_TYPE.BACKTICK | TOKEN_TYPE.REGEX> | undefined {
798
+ const next = this.peek(1)!
799
+
800
+ if (this.isType(next, $C.QUOTE_ANY)) {
801
+ const type = next.value === `"` ? "double" : next.value === "'" ? "single" : next.value === "`" ? "tick" : "regex"
802
+ return handle.delimiter[type](...this.processToken(this.consume($C.QUOTE_ANY)))
803
+ }
804
+ return undefined
805
+ }
806
+
807
+ ruleRegexAny(): ValidToken<TOKEN_TYPE.REGEX> | [ValidToken<TOKEN_TYPE.REGEX>, AnyToken<TOKEN_TYPE.VALUE>] {
808
+ const value = this.consume($C.REGEX_ANY)
809
+ if (value.value.length > 1) {
810
+ // cheat a bit to extract the flags
811
+ const delim = {
812
+ value: "/",
813
+ startOffset: value.startOffset,
814
+ endOffset: value.startOffset,
815
+ }
816
+ const flags = {
817
+ value: value.value.slice(1),
818
+ startOffset: value.startOffset + 1,
819
+ endOffset: value.endOffset,
820
+ }
821
+ return [
822
+ // why the ! ??? todo
823
+ handle.delimiter.regex(...this.processToken(delim))!,
824
+ handle.token.value(...this.processToken(flags)),
825
+ ]
826
+ }
827
+ return handle.delimiter.regex(...this.processToken(value))!
828
+ }
829
+
830
+ ruleValueNot<
831
+ TType extends $T.QUOTE_SINGLE | $T.QUOTE_DOUBLE | $T.QUOTE_BACKTICK | $C.REGEX_ANY,
832
+ >(
833
+ type: TType,
834
+ ): ValidToken<
835
+ TOKEN_TYPE.VALUE
836
+ > {
837
+ const realType = {
838
+ [$T.QUOTE_SINGLE]: $C.VALUE_FOR_SINGLE,
839
+ [$T.QUOTE_DOUBLE]: $C.VALUE_FOR_DOUBLE,
840
+ [$T.QUOTE_BACKTICK]: $C.VALUE_FOR_BACKTICK,
841
+ [$C.REGEX_ANY]: $T.VALUE_REGEX,
842
+ }[type]
843
+ if (realType === undefined) {
844
+ unreachable(`Unknown quote/regex type ${type}`)
845
+ }
846
+ const value = this.consume(realType)
847
+ if (realType !== value.type) {
848
+ unreachable(`Expected value type ${realType}, got ${value.type}`)
849
+ }
850
+ return handle.token.value(...this.processToken(value)) as any
851
+ }
852
+
853
+ ruleQuote<TType extends $T.QUOTE_SINGLE | $T.QUOTE_DOUBLE | $T.QUOTE_BACKTICK >(
854
+ type: TType,
855
+ ): ValidToken<
856
+ TType extends $T.QUOTE_SINGLE
857
+ ? TOKEN_TYPE.SINGLEQUOTE
858
+ : TType extends $T.QUOTE_DOUBLE
859
+ ? TOKEN_TYPE.DOUBLEQUOTE
860
+ : TType extends $T.QUOTE_BACKTICK
861
+ ? TOKEN_TYPE.BACKTICK
862
+ : never
863
+ > {
864
+ const quote = this.peek(1)
865
+ if (type !== quote?.type) {
866
+ throw new Error(`Expected quote type ${type}, got ${quote?.type}`)
867
+ }
868
+
869
+ switch (type) {
870
+ case $T.QUOTE_SINGLE:
871
+ return handle.delimiter.single(
872
+ ...this.processToken(this.consume($T.QUOTE_SINGLE)),
873
+ ) as any
874
+ case $T.QUOTE_DOUBLE:
875
+ return handle.delimiter.double(
876
+ ...this.processToken(this.consume($T.QUOTE_DOUBLE)),
877
+ ) as any
878
+ case $T.QUOTE_BACKTICK:
879
+ return handle.delimiter.tick(
880
+ ...this.processToken(this.consume($T.QUOTE_BACKTICK)),
881
+ ) as any
882
+ }
883
+ throw new Error(`Expected quote type ${type}`)
884
+ }
885
+
886
+
887
+ ruleVariablePrefix<TOnlyToken extends boolean = false>(
888
+ {
889
+
890
+ onlyToken = false as TOnlyToken,
891
+ unprefixed = false,
892
+ }: {
893
+ onlyToken?: TOnlyToken
894
+ unprefixed?: boolean
895
+ } = {},
896
+ ): TOnlyToken extends true ? Token<$T.VALUE_UNQUOTED> | undefined : AnyToken<TOKEN_TYPE.VALUE> | undefined {
897
+ const next = this.peek(1)
898
+ const next2 = this.peek(2)
899
+ const next4 = this.peek(4)
900
+ if (!unprefixed && this.options.prefixableStrings !== undefined
901
+ && this.isType(next2, $C.QUOTE_ANY)
902
+ && next2 && this.isType(next4, next2.type)
903
+ && next && this.options.prefixableStrings.includes(next.value)
904
+ ) {
905
+ return this.ruleValueUnquoted({ onlyToken }) as any
906
+ }
907
+ if (onlyToken) return undefined as any
908
+ return handle.token.value(...this.processToken()) as any
909
+ }
910
+
911
+ ruleValueUnquoted<TOnlyToken extends boolean = false>(
912
+ {
913
+ onlyToken = false as TOnlyToken,
914
+ }: {
915
+ onlyToken?: TOnlyToken
916
+ } = {},
917
+ ): TOnlyToken extends true ? Token<$T.VALUE_UNQUOTED> : AnyToken<TOKEN_TYPE.VALUE> {
918
+ const t = this.consume($T.VALUE_UNQUOTED)
919
+ const res = onlyToken ? t : handle.token.value(...this.processToken(t))
920
+ return (res) as any
921
+ }
922
+
923
+ ruleParenL(): ValidToken<TOKEN_TYPE.PARENL> | undefined {
924
+ const next = this.peek(1)
925
+ const value = next?.type === $T.PAREN_L
926
+ ? this.consume($T.PAREN_L)
927
+ : this.createErrorToken($T.PAREN_L)
928
+ const loc = extractPosition(value, this.state.shift)
929
+ return this.state.shift === 0 || loc.start > 0
930
+ ? handle.delimiter.parenL(value.isError ? undefined : value.value, loc)
931
+ : undefined
932
+ }
933
+
934
+ ruleParenR(): ValidToken<TOKEN_TYPE.PARENR> | undefined {
935
+ const value = this.consume($T.PAREN_R)
936
+ return handle.delimiter.parenR(...this.processToken(value))
937
+ }
938
+
939
+ ruleBracketL(): ValidToken<TOKEN_TYPE.BRACKETL> | undefined {
940
+ const next = this.peek(1)
941
+ const value = next?.type === $T.BRACKET_L
942
+ ? this.consume($T.BRACKET_L)
943
+ : this.createErrorToken($T.BRACKET_L)
944
+ const loc = extractPosition(value, this.state.shift)
945
+ return this.state.shift === 0 || loc.start > 0
946
+ ? handle.delimiter.bracketL(value.isError ? undefined : value.value, loc)
947
+ : undefined
948
+ }
949
+
950
+ ruleBracketR(): ValidToken<TOKEN_TYPE.BRACKETR> | undefined {
951
+ const value = this.consume($T.BRACKET_R)
952
+ return handle.delimiter.bracketR(...this.processToken(value))
953
+ }
954
+
955
+ ruleNot(): ValidToken<TOKEN_TYPE.NOT> | undefined {
956
+ if (this.isType(this.peek(1), $C.OPERATOR_NOT)) {
957
+ const op = this.consume($C.OPERATOR_NOT)
958
+ return handle.operator.not(...this.processToken<true>(op))
959
+ }
960
+ return undefined
961
+ }
962
+
963
+ /**
964
+ * Given a list of @see Suggestion entries, the parser options, and a list of variables, prefixes, operators, etc, and the preferred quote type, returns a list of @see Completion entries.
965
+ *
966
+ * It takes care of suggesting the correct delimiters for fixes, quoting variables/prefixes if it would not be possible to parse them unquoted, and separating symbol from non-symbol (word) operators.
967
+ *
968
+ * Does not add whitespace or group requirements. The suggestion information is still in the completion if you wish to show these. But they should not be added to the completion value if using @see autoreplace which will take care of it.
969
+ *
970
+ * Is not aware of existing values. You will have to use @see getCursorInfo to understand the context in which the suggestion was made, so that, for example, you could filter out used regex flags.
971
+ */
972
+ autocomplete(
973
+ suggestions: Suggestion[],
974
+ {
975
+ values = [], arrayValues = [], variables = [], prefixes = [], properties = [], expandedPropertyOperators = [], customPropertyOperators = (this as any as Parser<T>).options.customPropertyOperators ?? [], keywords = (this as any as Parser<T>).options.keywords, regexFlags = ["i", "m", "u"], quote = "\"",
976
+ }: Partial<Record<
977
+ "variables" |
978
+ "values" |
979
+ "arrayValues" |
980
+ "prefixes" |
981
+ "properties" |
982
+ "regexFlags" |
983
+ "expandedPropertyOperators" |
984
+ "customPropertyOperators", string[]>> & {
985
+ quote?: string
986
+ keywords?: FullParserOptions<T>["keywords"]
987
+ } = {},
988
+ ): Completion[] {
989
+ const self = (this as any as Parser<T>)
990
+ return suggestions.map(suggestion => {
991
+ const type = suggestion.type
992
+ switch (type) {
993
+ case SUGGESTION_TYPE.BACKTICK: return [{ suggestion, value: "`" }]
994
+ case SUGGESTION_TYPE.DOUBLEQUOTE: return [{ suggestion, value: "\"" }]
995
+ case SUGGESTION_TYPE.SINGLEQUOTE: return [{ suggestion, value: "'" }]
996
+ case SUGGESTION_TYPE.PARENL: return [{ suggestion, value: "(" }]
997
+ case SUGGESTION_TYPE.PARENR: return [{ suggestion, value: ")" }]
998
+ case SUGGESTION_TYPE.BRAKCETR: return [{ suggestion, value: "]" }] // L not needed
999
+ case SUGGESTION_TYPE.REGEX: return [{ suggestion, value: "/" }]
1000
+ case SUGGESTION_TYPE.REGEX_FLAGS:
1001
+ return regexFlags
1002
+ .map(value => ({ suggestion, value }))
1003
+ // remove existing flags from suggestions
1004
+ .filter(completion => {
1005
+ // eslint-disable-next-line @typescript-eslint/no-shadow
1006
+ const { suggestion, value } = completion
1007
+ if (suggestion.type !== SUGGESTION_TYPE.REGEX_FLAGS) {return true}
1008
+
1009
+ const token = suggestion.cursorInfo
1010
+ const flags = token.at && (token.at.parent as VariableNode)?.quote?.flags === suggestion.cursorInfo.at
1011
+ ? token.at
1012
+ : token.next && (token.next.parent as VariableNode)?.quote?.flags === suggestion.cursorInfo.next
1013
+ ? token.next
1014
+ : token.prev && (token.prev.parent as VariableNode)?.quote?.flags === suggestion.cursorInfo.prev
1015
+ ? token.prev
1016
+ : undefined
1017
+
1018
+ if (flags?.value?.includes(value)) {return false}
1019
+ return true
1020
+ })
1021
+ case SUGGESTION_TYPE.PROPERTY: {
1022
+ return properties.map(value => ({ suggestion, value }))
1023
+ }
1024
+ case SUGGESTION_TYPE.PROPERTY_SEP: {
1025
+ return [{ suggestion, value: self.options.expandedPropertySeparator! }]
1026
+ }
1027
+ case SUGGESTION_TYPE.EXPANDED_PROPERTY_OPERATOR: {
1028
+ return expandedPropertyOperators.map(value => ({ suggestion, value }))
1029
+ }
1030
+ case SUGGESTION_TYPE.CUSTOM_PROPERTY_OPERATOR: {
1031
+ return customPropertyOperators.map(value => ({ suggestion, value }))
1032
+ }
1033
+ case SUGGESTION_TYPE.BOOLEAN_SYMBOL_OP: {
1034
+ const keywordsList = [...keywords.and, ...keywords.or]
1035
+ const symOpts = keywordsList.filter(_ => _.isSymbol)
1036
+ return symOpts.map(({ value }) => ({ suggestion, value }))
1037
+ }
1038
+ case SUGGESTION_TYPE.BOOLEAN_WORD_OP: {
1039
+ const keywordsList = [...keywords.and, ...keywords.or]
1040
+ const wordOpts = keywordsList.filter(_ => !_.isSymbol)
1041
+ return wordOpts.map(({ value }) => ({ suggestion, value }))
1042
+ }
1043
+ case SUGGESTION_TYPE.VALUE:
1044
+ case SUGGESTION_TYPE.ARRAY_VALUE:
1045
+ case SUGGESTION_TYPE.VARIABLE: {
1046
+ const arr = type === SUGGESTION_TYPE.VARIABLE
1047
+ ? variables
1048
+ : type === SUGGESTION_TYPE.ARRAY_VALUE
1049
+ ? arrayValues
1050
+ : type === SUGGESTION_TYPE.VALUE
1051
+ ? values
1052
+ : unreachable()
1053
+ return arr.map(variable => {
1054
+ // we don't need to alter options since we can just check there are no quotes (also tells us no prefixes are used) and no operators are defined
1055
+ const res = self.parse(variable)
1056
+ if (res instanceof ConditionNode &&
1057
+ res.operator === undefined &&
1058
+ res.value instanceof VariableNode &&
1059
+ res.value.quote === undefined) {
1060
+ return { suggestion, value: res.value.value.value }
1061
+ } else {
1062
+ return { suggestion, value: quote + variable.replace(new RegExp(quote, "g"), `\\${quote}`) + quote }
1063
+ }
1064
+ })
1065
+ }
1066
+ case SUGGESTION_TYPE.PREFIX: return prefixes.map(prefix => {
1067
+ const res = self.parse(prefix)
1068
+ if (res instanceof ConditionNode &&
1069
+ res.operator === undefined &&
1070
+ res.value instanceof VariableNode &&
1071
+ res.value.quote === undefined) {
1072
+ return { suggestion, value: res.value.value.value }
1073
+ } else {
1074
+ return { suggestion, value: quote + prefix.replace(new RegExp(quote, "g"), `\\${quote}`) + quote }
1075
+ }
1076
+ })
1077
+ }
1078
+ }).flat()
1079
+ }
1080
+
1081
+ /**
1082
+ * Given the input string and a @see Completion consisting of the value of the replacement and a @see Suggestion entry, returns the replacement string and the new position of the cursor.
1083
+ *
1084
+ * The value passed should be escaped if it's needed (or quoted). @see autocomplete already takes care of quoting variables if you're using it.
1085
+ */
1086
+ autoreplace(
1087
+ input: string,
1088
+ { value, suggestion }: Completion,
1089
+ ): { replacement: string, cursor: number } {
1090
+ const isQuotedLeft = ["\"", "'", "`"].includes(value[0])
1091
+ const isQuotedRight = ["\"", "'", "`"].includes(value[value.length - 1])
1092
+ if ((isQuotedLeft && !isQuotedRight) || (!isQuotedLeft && isQuotedRight)) {
1093
+ throw new Error(`Completion value must either be entirely quoted or entirely unquoted. But the left side is ${isQuotedLeft ? "quoted" : "unquoted"} and the right side is ${isQuotedRight ? "quoted" : "unquoted"}.`)
1094
+ }
1095
+ let cursor = suggestion.range.start + value.length
1096
+
1097
+ if (suggestion.requires.prefix) {
1098
+ value = suggestion.requires.prefix + (isQuotedLeft ? "" : "\"") + value + (isQuotedRight ? "" : "\"")
1099
+
1100
+ cursor += suggestion.requires.prefix.length + Number(!isQuotedLeft) + Number(!isQuotedRight)
1101
+ }
1102
+ if (suggestion.requires.group) {
1103
+ value += "()"
1104
+ cursor++
1105
+ }
1106
+
1107
+ if (suggestion.requires.whitespace.before // &&
1108
+ ) {
1109
+ value = ` ${value}`
1110
+ cursor++
1111
+ }
1112
+ if (suggestion.requires.whitespace.after // &&
1113
+ ) {
1114
+ value = `${value} `
1115
+ }
1116
+
1117
+ const replacement = insert(value, input, [suggestion.range.start, suggestion.range.end])
1118
+ return { replacement, cursor }
1119
+ }
1120
+
1121
+ /**
1122
+ * Returns a list of suggestions ( @see Suggestion ). These are not a list of autocomplete entries (with values), but more a list of entries describing possible suggestions. This list can then be passed to @see Parser["autocomplete"] to build a list to show users, from which you can then pick an entry to pass to @see Parser["autoreplace"] .
1123
+ *
1124
+ * The list returned is "unsorted", but there is still some logic to the order. Fixes for errors are suggested first, in the order returned by @see getSurroundingErrors. Regular suggestions come after in the following order: prefixes if enabled, variables, boolean symbol operators, then boolean word operators.
1125
+ *
1126
+ * When the cursor is between two tokens that have possible suggestions, only suggestion types for the token before are returned. For example:
1127
+ *
1128
+ * ```js
1129
+ * prop="val"
1130
+ * prop|="val" //returns a property suggestions to replace `prop`
1131
+ * prop=|"val" //returns a custom operator suggestion to replace `=`
1132
+ * prop="|val" //returns a value suggestion
1133
+ * ```
1134
+ *
1135
+ * And if there are no suggestions for the previous token but there are for the next ones, they are suggested:
1136
+ * ```js
1137
+ * prop:op:"val"
1138
+ * prop:op|:"val" // returns an operator suggestion
1139
+ * prop:op:|"val" // returns a value suggestion
1140
+ * prop:op|"val" // returns a suggestion for the missing separator
1141
+ * ```
1142
+ */
1143
+ autosuggest(input: string, ast: ParserResults, index: number): Suggestion[] {
1144
+ // wrapped like this because the function is HUGE
1145
+ const opts = (this as any as Parser<T>).options
1146
+ const tokens = extractTokens(ast)
1147
+ const token = getCursorInfo(input, tokens, index)
1148
+
1149
+ const wordOps = [...opts.keywords.and, ...opts.keywords.or, ...opts.keywords.not].filter(op => !op.isSymbol)
1150
+
1151
+ const canSuggestOpAfterPrev = (
1152
+ token.valid.prev && tokenVariable.includes(token.valid.prev?.type) &&
1153
+ (token.whitespace.prev || token.valid.prev.type === TOKEN_TYPE.PARENR) &&
1154
+ !token.at && token.valid.next === undefined
1155
+ )
1156
+ const canSuggestOpBeforeNext =
1157
+ (
1158
+ token.valid.next && tokenVariable.includes(token.valid.next?.type) &&
1159
+ token.whitespace.next && // no parenL allowed since check since there will already be prefix suggestions
1160
+ !token.at && token.valid.prev === undefined
1161
+ )
1162
+
1163
+ const requiresWhitespacePrev = tokenRequiresWhitespace(token.valid.prev, token.whitespace.prev, wordOps)
1164
+ const requiresWhitespaceNext = tokenRequiresWhitespace(token.valid.next, token.whitespace.next, wordOps)
1165
+
1166
+ const requiresWhitespacePrevOp = canSuggestOpAfterPrev
1167
+ ? false
1168
+ : requiresWhitespacePrev
1169
+ const requireWhitespaceNextOp = !canSuggestOpAfterPrev && canSuggestOpBeforeNext
1170
+ ? false
1171
+ : requiresWhitespaceNext
1172
+
1173
+ const suggestions: Suggestion[] = []
1174
+ if (ast instanceof ErrorToken) {
1175
+ suggestions.push({
1176
+ type: SUGGESTION_TYPE.PREFIX,
1177
+ requires: createDefaultRequires({ group: true }),
1178
+ range: pos({ start: index }, { fill: true }),
1179
+ isError: true,
1180
+ cursorInfo: token,
1181
+ })
1182
+ suggestions.push({
1183
+ type: SUGGESTION_TYPE.VARIABLE,
1184
+ requires: createDefaultRequires(),
1185
+ range: pos({ start: index }, { fill: true }),
1186
+ isError: true,
1187
+ cursorInfo: token,
1188
+ })
1189
+ } else {
1190
+ const surroundingErrors = getSurroundingErrors(tokens, token)
1191
+
1192
+ const errorTypesHandled: TOKEN_TYPE[] = []
1193
+ const errorSuggestion = {
1194
+ isError: true,
1195
+ cursorInfo: token,
1196
+ }
1197
+ const baseSuggestion = {
1198
+ isError: false,
1199
+ cursorInfo: token,
1200
+ }
1201
+ for (const error of surroundingErrors) {
1202
+ for (const type of error.expected) {
1203
+ if (errorTypesHandled.includes(type)) continue
1204
+ errorTypesHandled.push(type)
1205
+
1206
+ switch (type) {
1207
+ case TOKEN_TYPE.DOUBLEQUOTE:
1208
+ case TOKEN_TYPE.SINGLEQUOTE:
1209
+ case TOKEN_TYPE.BACKTICK: {
1210
+ const isLeft = (error.parent as VariableNode).quote!.left === error
1211
+ const isRight = (error.parent as VariableNode).quote!.right === error
1212
+ suggestions.push({
1213
+ ...errorSuggestion,
1214
+ type: type as any as SUGGESTION_TYPE,
1215
+ requires: createDefaultRequires({
1216
+ whitespace: {
1217
+ before: isRight ? false : requiresWhitespacePrev,
1218
+ after: isLeft ? false : requiresWhitespaceNext,
1219
+ },
1220
+ }),
1221
+ range: pos({ start: index }, { fill: true }),
1222
+ })
1223
+ } break
1224
+ case TOKEN_TYPE.AND:
1225
+ case TOKEN_TYPE.OR:
1226
+ suggestions.push({
1227
+ ...errorSuggestion,
1228
+ type: SUGGESTION_TYPE.BOOLEAN_SYMBOL_OP,
1229
+ requires: createDefaultRequires(),
1230
+ range: pos({ start: index }, { fill: true }),
1231
+ })
1232
+ suggestions.push({
1233
+ ...errorSuggestion,
1234
+ type: SUGGESTION_TYPE.BOOLEAN_WORD_OP,
1235
+ requires: createDefaultRequires({
1236
+ whitespace: {
1237
+ before: requiresWhitespacePrevOp,
1238
+ after: requireWhitespaceNextOp,
1239
+ },
1240
+ }),
1241
+ range: pos({ start: index }, { fill: true }),
1242
+ })
1243
+ if (type === TOKEN_TYPE.AND) errorTypesHandled.push(TOKEN_TYPE.OR)
1244
+ if (type === TOKEN_TYPE.OR) errorTypesHandled.push(TOKEN_TYPE.AND)
1245
+
1246
+ break
1247
+ case TOKEN_TYPE.PARENL:
1248
+ case TOKEN_TYPE.PARENR:
1249
+ suggestions.push({
1250
+ ...errorSuggestion,
1251
+ type: type as any as SUGGESTION_TYPE,
1252
+ requires: createDefaultRequires(),
1253
+ range: pos({ start: index }, { fill: true }),
1254
+ })
1255
+ break
1256
+ case TOKEN_TYPE.VALUE: {
1257
+ const prefixedValue = error.parent instanceof VariableNode ? error.parent?.prefix?.value : false
1258
+ const isRegexValue = error.parent instanceof VariableNode && (
1259
+ error.parent.quote?.left.type === TOKEN_TYPE.REGEX ||
1260
+ error.parent.quote?.right.type === TOKEN_TYPE.REGEX
1261
+ )
1262
+ if (!isRegexValue) {
1263
+ // both are always suggested since missing value tokens only happen for variables
1264
+ if (!prefixedValue && opts.prefixableGroups) {
1265
+ suggestions.push({
1266
+ ...errorSuggestion,
1267
+ type: SUGGESTION_TYPE.PREFIX,
1268
+ requires: createDefaultRequires({
1269
+ whitespace: {
1270
+ before: requiresWhitespacePrev,
1271
+ after: false, /* parens get inserted */
1272
+ },
1273
+ group: true, // is always needed
1274
+ }),
1275
+ range: pos({ start: index }, { fill: true }),
1276
+ })
1277
+ }
1278
+ suggestions.push({
1279
+ ...errorSuggestion,
1280
+ type: SUGGESTION_TYPE.VARIABLE,
1281
+ requires: createDefaultRequires({
1282
+ whitespace: {
1283
+ before: requiresWhitespacePrev,
1284
+ after: requiresWhitespaceNext,
1285
+ },
1286
+ prefix: prefixedValue,
1287
+ }),
1288
+ range: pos({ start: index }, { fill: true }),
1289
+ })
1290
+ }
1291
+ break
1292
+ }
1293
+ case TOKEN_TYPE.BRACKETR: {
1294
+ suggestions.push({
1295
+ ...errorSuggestion,
1296
+ type: SUGGESTION_TYPE.BRAKCETR,
1297
+ requires: createDefaultRequires(),
1298
+ range: pos({ start: index }, { fill: true }),
1299
+ })
1300
+ break
1301
+ }
1302
+ case TOKEN_TYPE.OP_EXPANDED_SEP:
1303
+ suggestions.push({
1304
+ ...errorSuggestion,
1305
+ type: SUGGESTION_TYPE.PROPERTY_SEP,
1306
+ requires: createDefaultRequires(),
1307
+ range: pos({ start: index }, { fill: true }),
1308
+ })
1309
+ break
1310
+ case TOKEN_TYPE.REGEX:
1311
+ suggestions.push({
1312
+ ...errorSuggestion,
1313
+ type: SUGGESTION_TYPE.REGEX,
1314
+ requires: createDefaultRequires(),
1315
+ range: pos({ start: index }, { fill: true }),
1316
+ })
1317
+ break
1318
+ case TOKEN_TYPE.OP_CUSTOM:
1319
+ case TOKEN_TYPE.BRACKETL:
1320
+ case TOKEN_TYPE.NOT:
1321
+ unreachable()
1322
+ }
1323
+ }
1324
+ }
1325
+
1326
+ /** The quotes are checked because of situations like `prefix|"var"`.*/
1327
+ const prevVar = token.valid.prev?.parent
1328
+ const nextVar = token.valid.next?.parent
1329
+ const prevCondition = prevVar?.parent
1330
+ const nextCondition = nextVar?.parent
1331
+ const atVar = token.at?.parent
1332
+ const atCondition = atVar?.parent
1333
+
1334
+ const isVarPrev =
1335
+ !token.whitespace.prev &&
1336
+ token.valid.prev?.type !== TOKEN_TYPE.REGEX &&
1337
+ prevVar instanceof VariableNode &&
1338
+ (
1339
+ (
1340
+ prevCondition instanceof ConditionNode &&
1341
+ prevCondition.value === prevVar &&
1342
+ (
1343
+ prevVar.quote?.right === token.valid.prev ||
1344
+ prevVar.value === token.valid.prev
1345
+ )
1346
+ ) ||
1347
+ (
1348
+ prevCondition instanceof ArrayNode
1349
+ )
1350
+ )
1351
+
1352
+ const isVarNext =
1353
+ !token.whitespace.next &&
1354
+ token.valid.next?.type !== TOKEN_TYPE.REGEX &&
1355
+ nextVar instanceof VariableNode &&
1356
+ (
1357
+ (
1358
+ nextCondition instanceof ConditionNode &&
1359
+ nextCondition.value === nextVar &&
1360
+ (
1361
+ nextVar.quote?.left === token.valid.next ||
1362
+ nextVar.value === token.valid.next
1363
+ )
1364
+ ) ||
1365
+ (
1366
+ nextCondition instanceof ArrayNode
1367
+ )
1368
+ )
1369
+
1370
+ const isVarAt = (
1371
+ (
1372
+ atVar instanceof VariableNode &&
1373
+ atCondition instanceof ConditionNode
1374
+ ) ||
1375
+ (
1376
+ prevVar instanceof VariableNode &&
1377
+ token.valid.prev === prevVar?.quote?.left) ||
1378
+
1379
+ (
1380
+ nextVar instanceof VariableNode &&
1381
+ token.valid.next === nextVar?.quote?.right
1382
+ )
1383
+ )
1384
+
1385
+ const isPropertyPrev =
1386
+ prevCondition instanceof ConditionNode &&
1387
+ prevVar !== undefined &&
1388
+ prevVar === prevCondition?.property
1389
+ const isPropertyNext =
1390
+ nextCondition instanceof ConditionNode &&
1391
+ nextVar !== undefined &&
1392
+ nextVar === nextCondition?.property
1393
+ const isPropertyAt =
1394
+ atCondition instanceof ConditionNode &&
1395
+ atVar !== undefined &&
1396
+ atVar === atCondition?.property
1397
+
1398
+ const isPropertyOperatorPrev = prevVar instanceof ConditionNode && token.valid.prev === prevVar?.propertyOperator
1399
+ const isPropertyOperatorNext = nextVar instanceof ConditionNode && token.valid.next === nextVar?.propertyOperator
1400
+ const isPropertyOperatorAt = atVar instanceof ConditionNode && token.at === atVar?.propertyOperator
1401
+
1402
+ /** Situations like `[|]` and `[|` */
1403
+ const noArrayValuesTarget = token.valid.prev?.type === TOKEN_TYPE.BRACKETL &&
1404
+ (
1405
+ token.valid.next === undefined ||
1406
+ token.valid.next?.type === TOKEN_TYPE.BRACKETR
1407
+ )
1408
+
1409
+ /** For the following, prev tokens always have priority, next suggestions are only allowed if there are not other prev suggestions. Then lastly, only one at suggestion can exist at a time so no checks needed for those. */
1410
+ const target = isVarPrev
1411
+ ? token.valid.prev
1412
+ : !noArrayValuesTarget && !isPropertyPrev && !isPropertyOperatorPrev && isVarNext
1413
+ ? token.valid.next
1414
+ : isVarAt
1415
+ ? token.at
1416
+ : undefined
1417
+
1418
+
1419
+ const propertyTarget = isPropertyPrev
1420
+ ? token.valid.prev
1421
+ : !noArrayValuesTarget && !isVarPrev && !isPropertyOperatorPrev && isPropertyNext
1422
+ ? token.valid.next
1423
+ : isPropertyAt
1424
+ ? token.at
1425
+ : undefined
1426
+
1427
+ const propOpTarget = isPropertyOperatorPrev
1428
+ ? token.valid.prev
1429
+ : !noArrayValuesTarget && !isVarPrev && !isPropertyPrev && isPropertyOperatorNext
1430
+ ? token.valid.next
1431
+ : isPropertyOperatorAt
1432
+ ? token.at
1433
+ : undefined
1434
+
1435
+
1436
+ if (target) {
1437
+ const parent = target.parent
1438
+ if (parent instanceof VariableNode) {
1439
+ const range = pos(parent)
1440
+ const condition = parent?.parent as ConditionNode
1441
+ const isValue = condition.propertyOperator !== undefined && condition.value === parent
1442
+ const maybeGroup = parent?.parent?.parent
1443
+ const isPrefix = maybeGroup instanceof GroupNode && maybeGroup.prefix === condition
1444
+
1445
+ // look at whitespace before/after the entire variable
1446
+ const varStart = getCursorInfo(input, ast, parent.start)
1447
+ const varEnd = getCursorInfo(input, ast, parent.end)
1448
+ const targetRequiresWhitespacePrev = tokenRequiresWhitespace(varStart.valid.prev, varStart.whitespace.prev, wordOps)
1449
+ const targetRequiresWhitespaceNext = tokenRequiresWhitespace(varEnd.valid.next, varEnd.whitespace.next, wordOps)
1450
+ const prefixedValue = target.parent instanceof VariableNode ? target.parent?.prefix?.value : false
1451
+
1452
+ // most of these require additional handling below
1453
+ const isSepPrev = token.prev?.type === TOKEN_TYPE.OP_EXPANDED_SEP
1454
+ const arrayValue = target.parent?.parent instanceof ArrayNode
1455
+ const isRegexFlag = target === parent.quote?.flags
1456
+
1457
+ if (!isRegexFlag && !isSepPrev && !isValue && !arrayValue && !prefixedValue && opts.prefixableGroups) {
1458
+ suggestions.push({
1459
+ ...baseSuggestion,
1460
+ type: SUGGESTION_TYPE.PREFIX,
1461
+ requires: createDefaultRequires({
1462
+ group: !isPrefix,
1463
+ whitespace: {
1464
+ before: targetRequiresWhitespacePrev && !isPrefix,
1465
+ after: false, // parens exist or get inserted
1466
+ },
1467
+ }),
1468
+ range,
1469
+ })
1470
+ }
1471
+
1472
+ if (!isRegexFlag && !isPrefix) {
1473
+ suggestions.push({
1474
+ ...baseSuggestion,
1475
+ type: arrayValue
1476
+ ? SUGGESTION_TYPE.ARRAY_VALUE
1477
+ : isValue
1478
+ ? SUGGESTION_TYPE.VALUE
1479
+ : SUGGESTION_TYPE.VARIABLE,
1480
+ requires: createDefaultRequires({
1481
+ whitespace: {
1482
+ before: targetRequiresWhitespacePrev,
1483
+ after: targetRequiresWhitespaceNext,
1484
+ },
1485
+ prefix: prefixedValue,
1486
+ }),
1487
+ range,
1488
+ })
1489
+ }
1490
+ }
1491
+ }
1492
+
1493
+ if (noArrayValuesTarget) {
1494
+ suggestions.push({
1495
+ ...baseSuggestion,
1496
+ type: SUGGESTION_TYPE.ARRAY_VALUE,
1497
+ requires: createDefaultRequires(),
1498
+ range: pos({ start: index }, { fill: true }),
1499
+ })
1500
+ }
1501
+
1502
+ if (propertyTarget) {
1503
+ suggestions.push({
1504
+ ...baseSuggestion,
1505
+ type: SUGGESTION_TYPE.PROPERTY,
1506
+ requires: createDefaultRequires(),
1507
+ range: pos(propertyTarget),
1508
+ })
1509
+ }
1510
+ if (propOpTarget) {
1511
+ suggestions.push({
1512
+ ...baseSuggestion,
1513
+ type: (propOpTarget.parent as ConditionNode).sep
1514
+ ? SUGGESTION_TYPE.EXPANDED_PROPERTY_OPERATOR
1515
+ : SUGGESTION_TYPE.CUSTOM_PROPERTY_OPERATOR
1516
+ ,
1517
+ requires: createDefaultRequires(),
1518
+ range: pos(propOpTarget),
1519
+ })
1520
+ }
1521
+
1522
+ const canSuggestValue =
1523
+ (
1524
+ (
1525
+ token.whitespace.next &&
1526
+ (
1527
+ token.whitespace.prev ||
1528
+ token.prev?.type === TOKEN_TYPE.BRACKETL ||
1529
+ token.prev?.type === TOKEN_TYPE.PARENL
1530
+ )
1531
+ ) ||
1532
+ (
1533
+ token.whitespace.prev &&
1534
+ (
1535
+ token.whitespace.next ||
1536
+ token.next?.type === TOKEN_TYPE.BRACKETR ||
1537
+ token.next?.type === TOKEN_TYPE.PARENR
1538
+ )
1539
+ )
1540
+ )
1541
+
1542
+ if (canSuggestValue) {
1543
+ const inArrayNode = [nextCondition, prevCondition, nextVar, prevVar].find(_ => _ instanceof ArrayNode) !== undefined
1544
+ const opsNotNeeded = ["and", "or"].includes(opts.onMissingBooleanOperator)
1545
+
1546
+
1547
+ if (inArrayNode || opsNotNeeded) {
1548
+ suggestions.push({
1549
+ type: inArrayNode ? SUGGESTION_TYPE.ARRAY_VALUE : SUGGESTION_TYPE.VARIABLE,
1550
+ requires: createDefaultRequires({}),
1551
+ range: pos({ start: index }, { fill: true }),
1552
+ ...baseSuggestion,
1553
+ })
1554
+ }
1555
+ // if we're not an in array node we can also suggest prefixes
1556
+ if (!inArrayNode && opsNotNeeded) {
1557
+ suggestions.push({
1558
+ ...baseSuggestion,
1559
+ type: SUGGESTION_TYPE.PREFIX,
1560
+ requires: createDefaultRequires({
1561
+ group: true,
1562
+ }),
1563
+ range: pos({ start: index }, { fill: true }),
1564
+ })
1565
+ }
1566
+ }
1567
+
1568
+ const canSuggestRegexFlags =
1569
+ // has existing flags before/after
1570
+ (
1571
+ token.at &&
1572
+ token.at === (token.at?.parent as VariableNode)?.quote?.flags
1573
+ ) ||
1574
+ (
1575
+ token.valid.prev &&
1576
+ token.valid.prev === (token.valid.prev?.parent as VariableNode)?.quote?.flags
1577
+ ) ||
1578
+ (
1579
+ token.valid.next &&
1580
+ token.valid.next === (token.valid.next?.parent as VariableNode)?.quote?.flags
1581
+ ) ||
1582
+ ( // no flags
1583
+ token.valid.prev?.type === TOKEN_TYPE.REGEX &&
1584
+ token.valid.prev === (token.valid.prev.parent as VariableNode).quote?.right
1585
+ )
1586
+
1587
+ if (canSuggestRegexFlags) {
1588
+ suggestions.push({
1589
+ ...baseSuggestion,
1590
+ type: SUGGESTION_TYPE.REGEX_FLAGS,
1591
+ requires: createDefaultRequires(),
1592
+ range: pos({ start: index }, { fill: true }),
1593
+ })
1594
+ }
1595
+
1596
+ if (canSuggestOpAfterPrev || canSuggestOpBeforeNext) {
1597
+ const range = pos({ start: index }, { fill: true })
1598
+ suggestions.push({
1599
+ ...baseSuggestion,
1600
+ type: SUGGESTION_TYPE.BOOLEAN_SYMBOL_OP,
1601
+ requires: createDefaultRequires(),
1602
+ range,
1603
+ })
1604
+ suggestions.push({
1605
+ ...baseSuggestion,
1606
+ type: SUGGESTION_TYPE.BOOLEAN_WORD_OP,
1607
+ requires: createDefaultRequires({
1608
+ whitespace: {
1609
+ before: requiresWhitespacePrevOp,
1610
+ after: requireWhitespaceNextOp,
1611
+ },
1612
+ }),
1613
+ range,
1614
+ })
1615
+ }
1616
+ }
1617
+ return suggestions
1618
+ }
1619
+
1620
+ /**
1621
+ * Evaluates a {@link Parser.normalize normalized} ast.
1622
+ *
1623
+ * How the ast is evaluated for different operators can be controlled by the {@link ParserOptions.valueComparer valueComparer} option.
1624
+ */
1625
+ evaluate(ast: Expression<any, any> | Condition<any, any>, context: Record<string, any>): boolean {
1626
+ this._checkEvaluationOptions()
1627
+ const opts = (this as any as Parser<T>).options
1628
+
1629
+ if (ast instanceof Condition) {
1630
+ const contextValue = get(context, ast.property)
1631
+ const res = opts.valueComparer({ property: ast.property, value: ast.value, operator: ast.operator }, contextValue, context)
1632
+ return ast.negate ? !res : res
1633
+ }
1634
+ if (ast instanceof Expression) {
1635
+ const left = this.evaluate(ast.left, context)
1636
+ const right = this.evaluate(ast.right, context)
1637
+
1638
+ return ast.operator === TOKEN_TYPE.AND
1639
+ ? (left && right)
1640
+ : (left || right)
1641
+ }
1642
+
1643
+ return unreachable()
1644
+ }
1645
+
1646
+ /**
1647
+ * Given the set of indexes returned by {@link getBestIndex}, the set of existing indexes in a database, and the index to sort by\*, will return a list of the best/shortest sets of indexes.
1648
+ *
1649
+ * For example, given the query `a && b && c`, `getBestIndex` will return `[Set(a), Set(b)]`.
1650
+ *
1651
+ * Suppose we have indexes on all the variables and that the user wants to sort by `c`, this function will return [`Set(c)`].
1652
+ *
1653
+ * Suppose instead we have indexes only on `a` and `b` and that the user wants to sort by `c`, this function will return [`Set(a), Set(b)`]. Either can be picked by some other criteria (e.g. size of the indexes). Sort should then be done in memory.
1654
+ *
1655
+ * And then finally, if we have no existing indexes on any of the variables, the function will return `[]`.
1656
+ *
1657
+ * Note: This is a simple algorithm and is not designed to take into account instances where entries are indexed by two or more properties as their keys (i.e. multicolumn indexes).
1658
+ *
1659
+ * \* If the sort index is not in the list of existing indexes it is not taken into account.
1660
+ */
1661
+ getBestIndexes(indexes: Set<string>[], existing: Set<string> | Map<string, number>, sortIndex: string = ""): Set<string>[] {
1662
+ indexes = indexes.filter(set => {
1663
+ for (const key of set) {
1664
+ if (!existing.has(key)) return false
1665
+ }
1666
+ return true
1667
+ })
1668
+
1669
+ let finalIndexes = indexes
1670
+
1671
+ if (existing.has(sortIndex)) {
1672
+ const indexesWithSortIndex = indexes.filter(set => set.has(sortIndex))
1673
+ if (indexesWithSortIndex.length > 0) finalIndexes = indexesWithSortIndex
1674
+ }
1675
+
1676
+
1677
+ let smallest = Infinity
1678
+ if (existing instanceof Map) {
1679
+ const scores = new Map<Set<string>, number>()
1680
+ for (const set of finalIndexes) {
1681
+ let score = 0
1682
+ for (const key of set) {
1683
+ score += existing.get(key) ?? 0
1684
+ }
1685
+ scores.set(set, score)
1686
+ smallest = score < smallest ? score : smallest
1687
+ }
1688
+ return indexes.filter(set => smallest === Infinity || scores.get(set) === smallest)
1689
+ } else {
1690
+ for (const set of finalIndexes) {
1691
+ smallest = set.size < smallest ? set.size : smallest
1692
+ }
1693
+ return indexes.filter(set => smallest === Infinity || set.size === smallest)
1694
+ }
1695
+ }
1696
+
1697
+ /**
1698
+ * Returns a list of the different sets of keys that need to be indexed to run a normalized query on a database and hit an existing index.
1699
+ *
1700
+ * For example, the expression `a || b` requires both `a` AND `b` be indexed to use an index. The function would return `[Set(a, b)]`.
1701
+ *
1702
+ * On the otherhand, the expression `a && b` only requires `a` OR `b` to be indexed (`[Set(a), Set(b)]`) If at least one is indexed, the rest of the filtering can be done in memory. There is no need to in memory filter the entire database.
1703
+ *
1704
+ * Now take a more complicated query like `(a && b) || (a && c)`. This only requires `a` be indexed, or both `b` AND `c`. (`[Set(a)], [Set(b), Set(c)]`).
1705
+ *
1706
+ * Queries like `(a || b) && (a || c)` would require all the variables to be indexed `[Set(a), Set(b), Set(c)]`.
1707
+ */
1708
+ getIndexes(ast: Condition | Expression): Set<string>[] {
1709
+ if (ast instanceof Condition) {
1710
+ return [new Set(ast.property.join("."))]
1711
+ }
1712
+ if (ast instanceof Expression) {
1713
+ const left = this.getIndexes(ast.left)
1714
+ const right = this.getIndexes(ast.right)
1715
+
1716
+ if (ast.operator === TOKEN_TYPE.AND) {
1717
+ const sets: Set<string>[] = []
1718
+ const allKeys: Set<string> = new Set()
1719
+
1720
+ for (const leftSet of left) {
1721
+ const exists = sets.find(set => isEqualSet(set, leftSet))
1722
+ if (exists) continue
1723
+ sets.push(leftSet)
1724
+ for (const key of leftSet) {
1725
+ allKeys.add(key)
1726
+ }
1727
+ }
1728
+ for (const rightSet of right) {
1729
+ const exists = sets.find(set => isEqualSet(set, rightSet))
1730
+ if (exists) continue
1731
+ sets.push(rightSet)
1732
+ for (const key of rightSet) {
1733
+ allKeys.add(key)
1734
+ }
1735
+ }
1736
+
1737
+ const commonKeys: Set<string> = new Set()
1738
+
1739
+ // eslint-disable-next-line no-labels
1740
+ outerCheck: for (const key of allKeys) {
1741
+ for (const set of sets) {
1742
+ // eslint-disable-next-line no-labels
1743
+ if (!set.has(key)) continue outerCheck
1744
+ }
1745
+ commonKeys.add(key)
1746
+ }
1747
+ if (commonKeys.size > 0) {
1748
+ return [commonKeys, allKeys]
1749
+ } else {
1750
+ return sets
1751
+ }
1752
+ }
1753
+ if (ast.operator === TOKEN_TYPE.OR) {
1754
+ for (const rightSet of right) {
1755
+ for (const leftSet of left) {
1756
+ if (isEqualSet(leftSet, rightSet)) {
1757
+ return [rightSet]
1758
+ }
1759
+ }
1760
+ }
1761
+ const res = new Set<string>()
1762
+ for (const leftSet of left) {
1763
+ for (const key of leftSet) {
1764
+ res.add(key)
1765
+ }
1766
+ }
1767
+ for (const rightSet of right) {
1768
+ for (const key of rightSet) {
1769
+ res.add(key)
1770
+ }
1771
+ }
1772
+ return [res]
1773
+ }
1774
+ }
1775
+
1776
+ return unreachable()
1777
+ }
1778
+
1779
+ /**
1780
+ * Normalizes the ast by applying {@link GroupNode GroupNodes} and converting {@link ConditionNode ConditionNodes} to {@link NormalizedConditionNode NormalizedConditionNodes}.
1781
+ */
1782
+ normalize<TType extends string, TValue>(ast: ParserResults): Condition<TType, TValue> | Expression<TType, TValue> {
1783
+ this._checkEvaluationOptions()
1784
+ const opts = (this as any as Parser<T>).options
1785
+ if (ast instanceof ErrorToken || !ast.valid) {
1786
+ throw new Error("AST node must be valid.")
1787
+ }
1788
+ // eslint-disable-next-line prefer-rest-params
1789
+ const prefix: string | undefined = arguments[1]
1790
+ // eslint-disable-next-line prefer-rest-params
1791
+ const groupValue: boolean | undefined = arguments[2]
1792
+ // eslint-disable-next-line prefer-rest-params
1793
+ let operator: string | undefined = arguments[3]
1794
+
1795
+ const self_ = this as any as Parser & { normalize: AddParameters<Parser["normalize"], [typeof prefix, typeof groupValue, typeof operator]> }
1796
+
1797
+ if (ast instanceof ConditionNode) {
1798
+ if (!(ast.value instanceof GroupNode)) {
1799
+ const isValue = ast.value instanceof ArrayNode || (ast.value as VariableNode)?.quote?.left.type === TOKEN_TYPE.REGEX
1800
+ let name = ast.property
1801
+ ? unescape(ast.property.value.value)
1802
+ : isValue
1803
+ // the property might be missing, whether this is valid or not is up to the user
1804
+ // e.g. if prefix is defined this would make some sense
1805
+ ? undefined
1806
+ : unescape((ast.value as VariableNode)?.value.value)
1807
+ // some ancestor node went through the else block because it was a group node (e.g. prop:op(val))
1808
+ // so the "prefix" we passed is actually the name of the property (e.g. prop) and the value is the name we're getting here (e.g. val)
1809
+ const isNested = operator !== undefined
1810
+ if (prefix !== undefined && !isNested) {
1811
+ name = name ? applyPrefix(prefix, name, opts.prefixApplier) : prefix
1812
+ }
1813
+ let value: any
1814
+ if (isNested) {
1815
+ value = name ?? true
1816
+ name = prefix
1817
+ } else {
1818
+ value = ast.value instanceof ArrayNode
1819
+ ? ast.value.values.map(val => unescape(val.value.value))
1820
+ : (ast.value as VariableNode)?.quote?.left.type === TOKEN_TYPE.REGEX
1821
+ ? ast.value.value.value
1822
+ : ast.property && ast.value instanceof VariableNode
1823
+ ? unescape(ast.value.value.value)
1824
+ : true
1825
+ }
1826
+ const propertyKeys = name ? opts.keyParser(name) : []
1827
+
1828
+ const boolValue = applyBoolean(groupValue, ast.operator === undefined)
1829
+ const valuePrefix = ast.value instanceof VariableNode && ast.value.prefix
1830
+ ? unescape(ast.value.prefix.value)
1831
+ : undefined
1832
+ // one or the other might be defined, but never both since nested properties (e.g. `prop:op(prop:op(...))`) are not allowed
1833
+ operator ??= ast.propertyOperator?.value
1834
+ const isRegex = (ast.value as VariableNode)?.quote?.left.type === TOKEN_TYPE.REGEX
1835
+ const isQuoted = (ast.value as VariableNode)?.quote !== undefined
1836
+ const isExpanded = ast.sep !== undefined
1837
+ const regexFlags = (ast.value as VariableNode)?.quote?.flags?.value
1838
+ const query: ValueQuery = {
1839
+ value,
1840
+ operator,
1841
+ prefix: valuePrefix,
1842
+ regexFlags,
1843
+ property: propertyKeys,
1844
+ isRegex,
1845
+ isQuoted,
1846
+ isExpanded,
1847
+ isNegated: !boolValue,
1848
+ condition: ast,
1849
+ }
1850
+ const res = opts.conditionNormalizer(query)
1851
+ return new Condition({ property: propertyKeys, ...res })
1852
+ } else {
1853
+ let name = unescape((ast.property as VariableNode).value.value) // this is always a variable node
1854
+ if (prefix !== undefined) {
1855
+ name = applyPrefix(prefix, name, opts.prefixApplier)
1856
+ }
1857
+ const boolValue = applyBoolean(groupValue, ast.operator === undefined)
1858
+ // other operator is never defined see comments in other block above
1859
+ // eslint-disable-next-line @typescript-eslint/no-shadow
1860
+ const operator = ast.propertyOperator?.value
1861
+ // this call will at some point lead us to the above block with isNested = true
1862
+ return self_.normalize(ast.value, name, boolValue, operator) as any
1863
+ }
1864
+ }
1865
+
1866
+ if (ast instanceof GroupNode) {
1867
+ const _prefix = ast.prefix instanceof ConditionNode && ast.prefix.value instanceof VariableNode
1868
+ ? unescape(ast.prefix.value.value.value)
1869
+ : undefined // we do not want to apply not tokens
1870
+ const _groupValue = ast.prefix instanceof ConditionNode
1871
+ ? ast.prefix.operator === undefined
1872
+ : !(ast.prefix instanceof ValidToken)
1873
+
1874
+ const applied = applyPrefix(prefix, _prefix ?? "", opts.prefixApplier)
1875
+
1876
+ return self_.normalize(ast.expression as any, applied, applyBoolean(groupValue, _groupValue), operator) as any
1877
+ }
1878
+ if (ast instanceof ExpressionNode) {
1879
+ const left = self_.normalize(ast.left, prefix, groupValue, operator)
1880
+ const right = self_.normalize(ast.right, prefix, groupValue, operator)
1881
+
1882
+ // apply De Morgan's laws if group prefix was negative
1883
+ // the values are already flipped, we just have to flip the operator
1884
+ const type: TokenBooleanTypes = (groupValue === false ? OPPOSITE[ast.operator.type] : ast.operator.type) as TokenBooleanTypes
1885
+ return new Expression<TType, TValue>({ operator: type, left: left as any, right: right as any })
1886
+ }
1887
+ return unreachable()
1888
+ }
1889
+
1890
+ /**
1891
+ * Allows pre-validating ASTs for syntax highlighting purposes.
1892
+ * Works similar to evaluate. Internally it will use the prefixApplier, keyParser, and valueValidator (instead of comparer).
1893
+ *
1894
+ * The context does not need to be passed. If it's not passed, the function will not attempt to get the values (so it will not error) and the contextValue param of the valueValidator will be undefined.
1895
+ */
1896
+ validate(ast: ParserResults, context?: Record<string, any>): (Position & T)[] {
1897
+ const self = (this as any as Parser<T>)
1898
+ self._checkValidationOptions()
1899
+ const opts = self.options
1900
+ // see evaluate function, this method is practically identical, except we don't keep track of the real value (since we are not evaluating) and the actual nodes/tokens are passed to the valueValidator, not just the string values.
1901
+ if (ast instanceof ErrorToken || !ast.valid) {
1902
+ throw new Error("AST node must be valid.")
1903
+ }
1904
+ /** Handle hidden recursive version of the function. */
1905
+ // eslint-disable-next-line prefer-rest-params
1906
+ const prefix: string | undefined = arguments[2]
1907
+ // eslint-disable-next-line prefer-rest-params
1908
+ const groupValue: boolean | undefined = arguments[3]
1909
+ // eslint-disable-next-line prefer-rest-params
1910
+ const results: (Position & T)[] = arguments[4] ?? []
1911
+ // eslint-disable-next-line prefer-rest-params
1912
+ const prefixes: VariableNode[] = arguments[5] ?? []
1913
+ // eslint-disable-next-line prefer-rest-params
1914
+ let operator: ValidToken<TOKEN_TYPE.VALUE | TOKEN_TYPE.OP_CUSTOM> | undefined = arguments[6]
1915
+
1916
+ const self_ = this as any as Parser & { validate: AddParameters<Parser["validate"], [typeof prefix, typeof groupValue, typeof results, typeof prefixes, typeof operator]> }
1917
+
1918
+ if (ast instanceof ConditionNode) {
1919
+ if (!(ast.value instanceof GroupNode)) {
1920
+ const isValue = ast.value instanceof ArrayNode || (ast.value as VariableNode)?.quote?.left.type === TOKEN_TYPE.REGEX
1921
+ const nameNode = ast.property
1922
+ ? ast.property as VariableNode
1923
+ : isValue
1924
+ ? undefined
1925
+ : ast.value as VariableNode
1926
+
1927
+ let name = nameNode ? unescape(nameNode.value.value) : undefined
1928
+ const isNested = operator !== undefined
1929
+ if (prefix !== undefined && !isNested) {
1930
+ name = name ? applyPrefix(prefix, name, opts.prefixApplier) : prefix
1931
+ }
1932
+ let value: any
1933
+ let propertyNodes: VariableNode[] = []
1934
+
1935
+ if (isNested) {
1936
+ value = name
1937
+ name = prefix
1938
+ propertyNodes = [...prefixes]
1939
+ } else {
1940
+ propertyNodes = [...prefixes, ...(nameNode ? [nameNode] : [])]
1941
+ value = ast.value instanceof ArrayNode
1942
+ ? ast.value.values
1943
+ : (ast.value as VariableNode)?.quote?.left.type === TOKEN_TYPE.REGEX
1944
+ ? ast.value
1945
+ : ast.property && ast.value instanceof VariableNode
1946
+ ? ast.value
1947
+ : true
1948
+ }
1949
+ const propertyKeys = name ? opts.keyParser(name) : []
1950
+ const contextValue = context !== undefined ? get(context, propertyKeys) : undefined
1951
+
1952
+ const boolValue = applyBoolean(groupValue, ast.operator === undefined)
1953
+ const valuePrefix = ast.value instanceof VariableNode && ast.value.prefix
1954
+ ? ast.value.prefix
1955
+ : undefined
1956
+ operator ??= ast.propertyOperator as ValidToken<TOKEN_TYPE.VALUE | TOKEN_TYPE.OP_CUSTOM>
1957
+ const isRegex = (ast.value as VariableNode)?.quote?.left.type === TOKEN_TYPE.REGEX
1958
+ const isQuoted = (ast.value as VariableNode)?.quote !== undefined
1959
+ const isExpanded = ast.sep !== undefined
1960
+ const regexFlags = (ast.value as VariableNode)?.quote?.flags
1961
+ const query: ValidationQuery = {
1962
+ value,
1963
+ operator,
1964
+ prefix: valuePrefix,
1965
+ prefixes,
1966
+ property: propertyNodes,
1967
+ propertyKeys,
1968
+ propertyName: name,
1969
+ regexFlags,
1970
+ isRegex,
1971
+ isNegated: !boolValue,
1972
+ isQuoted,
1973
+ isExpanded,
1974
+ condition: ast,
1975
+ }
1976
+ const res = opts.valueValidator(contextValue, query, context)
1977
+ if (res && !isArray(res)) throw new Error("The valueValidator must return an array or nothing/undefined")
1978
+ if (res) { for (const entry of res) results.push(entry) }
1979
+ } else {
1980
+ let name = unescape((ast.property as VariableNode).value.value) // this is always a variable node
1981
+ if (prefix !== undefined) {
1982
+ name = applyPrefix(prefix, name, opts.prefixApplier)
1983
+ }
1984
+
1985
+ const boolValue = applyBoolean(groupValue, ast.operator === undefined)
1986
+
1987
+ if (ast.property) prefixes.push((ast.property as any))
1988
+ // eslint-disable-next-line @typescript-eslint/no-shadow
1989
+ const operator = ast.propertyOperator as ValidToken<TOKEN_TYPE.VALUE | TOKEN_TYPE.OP_CUSTOM>
1990
+ self_.validate(ast.value, context, name, boolValue, results, prefixes, operator)
1991
+ }
1992
+ }
1993
+
1994
+ if (ast instanceof GroupNode) {
1995
+ const _prefix = ast.prefix instanceof ConditionNode && ast.prefix.value instanceof VariableNode
1996
+ ? ast.prefix.value
1997
+ : undefined // we do not want to apply not tokens
1998
+ if (_prefix) prefixes.push(_prefix)
1999
+
2000
+ const _groupValue = ast.prefix instanceof ConditionNode
2001
+ ? ast.prefix.operator === undefined
2002
+ : !(ast.prefix instanceof ValidToken)
2003
+
2004
+ self_.validate(ast.expression as any, context, applyPrefix(prefix, _prefix?.value.value ?? "", opts.prefixApplier), applyBoolean(groupValue, _groupValue), results, prefixes, operator)
2005
+ }
2006
+ if (ast instanceof ExpressionNode) {
2007
+ // prefixes must be spread because we don't want the left branch (if it goes deeper) to affect the right
2008
+ self_.validate(ast.left, context, prefix, groupValue, results, [...prefixes], operator)
2009
+ self_.validate(ast.right, context, prefix, groupValue, results, [...prefixes], operator)
2010
+ }
2011
+ return results
2012
+ }
2013
+ }
2014
+