@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.
- package/README.md +3 -1
- package/dist/Lexer.d.ts +146 -0
- package/dist/Lexer.d.ts.map +1 -0
- package/dist/Lexer.js +960 -0
- package/dist/Parser.d.ts +211 -0
- package/dist/Parser.d.ts.map +1 -0
- package/dist/Parser.js +1476 -0
- package/dist/ast/builders/token.js +1 -1
- package/dist/ast/handlers.d.ts +3 -3
- package/dist/ast/handlers.d.ts.map +1 -1
- package/dist/examples/shortcutContextParser.d.ts +1 -1
- package/dist/examples/shortcutContextParser.js +1 -1
- package/dist/helpers/errors.js +3 -3
- package/dist/helpers/parser/checkParserOpts.js +2 -2
- package/dist/helpers/parser/extractPosition.d.ts +2 -6
- package/dist/helpers/parser/extractPosition.d.ts.map +1 -1
- package/dist/helpers/parser/extractPosition.js +3 -3
- package/dist/helpers/parser/getUnclosedRightParenCount.d.ts +2 -3
- package/dist/helpers/parser/getUnclosedRightParenCount.d.ts.map +1 -1
- package/dist/helpers/parser/getUnclosedRightParenCount.js +4 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -5
- package/dist/package.json.js +27 -41
- package/dist/types/ast.d.ts +1 -7
- package/dist/types/ast.d.ts.map +1 -1
- package/dist/types/errors.d.ts +5 -17
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/errors.js +0 -1
- package/dist/types/parser.d.ts.map +1 -1
- package/dist/utils/extractTokens.js +1 -1
- package/dist/utils/getCursorInfo.d.ts +2 -2
- package/dist/utils/getCursorInfo.d.ts.map +1 -1
- package/dist/utils/getCursorInfo.js +3 -6
- package/dist/utils/getOppositeDelimiter.d.ts.map +1 -1
- package/dist/utils/getOppositeDelimiter.js +1 -1
- package/dist/utils/prettyAst.js +7 -6
- package/package.json +25 -41
- package/src/Lexer.ts +704 -0
- package/src/Parser.ts +2014 -0
- package/src/ast/builders/array.ts +1 -1
- package/src/ast/builders/condition.ts +1 -1
- package/src/ast/builders/expression.ts +1 -1
- package/src/ast/builders/group.ts +1 -1
- package/src/ast/builders/pos.ts +1 -1
- package/src/ast/builders/token.ts +2 -2
- package/src/ast/builders/type.ts +1 -1
- package/src/ast/builders/variable.ts +1 -1
- package/src/ast/classes/ConditionNode.ts +1 -1
- package/src/ast/classes/ErrorToken.ts +1 -1
- package/src/ast/classes/ValidToken.ts +2 -2
- package/src/ast/handlers.ts +6 -6
- package/src/examples/shortcutContextParser.ts +2 -2
- package/src/helpers/errors.ts +4 -4
- package/src/helpers/general/defaultConditionNormalizer.ts +1 -1
- package/src/helpers/parser/checkParserOpts.ts +12 -12
- package/src/helpers/parser/extractPosition.ts +4 -8
- package/src/helpers/parser/getUnclosedRightParenCount.ts +6 -6
- package/src/helpers/parser/parseParserOptions.ts +1 -1
- package/src/index.ts +1 -2
- package/src/types/ast.ts +1 -8
- package/src/types/errors.ts +12 -22
- package/src/types/parser.ts +0 -1
- package/src/utils/extractTokens.ts +1 -1
- package/src/utils/getCursorInfo.ts +6 -5
- package/src/utils/getOppositeDelimiter.ts +5 -2
- package/src/utils/prettyAst.ts +4 -4
- package/dist/grammar/ParserBase.d.ts +0 -51
- package/dist/grammar/ParserBase.d.ts.map +0 -1
- package/dist/grammar/ParserBase.js +0 -517
- package/dist/grammar/createTokens.d.ts +0 -56
- package/dist/grammar/createTokens.d.ts.map +0 -1
- package/dist/grammar/createTokens.js +0 -844
- package/dist/grammar/index.d.ts +0 -3
- package/dist/grammar/index.d.ts.map +0 -1
- package/dist/grammar/index.js +0 -6
- package/dist/methods/autocomplete.d.ts +0 -18
- package/dist/methods/autocomplete.d.ts.map +0 -1
- package/dist/methods/autocomplete.js +0 -109
- package/dist/methods/autoreplace.d.ts +0 -13
- package/dist/methods/autoreplace.d.ts.map +0 -1
- package/dist/methods/autoreplace.js +0 -36
- package/dist/methods/autosuggest.d.ts +0 -28
- package/dist/methods/autosuggest.d.ts.map +0 -1
- package/dist/methods/autosuggest.js +0 -371
- package/dist/methods/evaluate.d.ts +0 -11
- package/dist/methods/evaluate.d.ts.map +0 -1
- package/dist/methods/evaluate.js +0 -32
- package/dist/methods/getBestIndex.d.ts +0 -19
- package/dist/methods/getBestIndex.d.ts.map +0 -1
- package/dist/methods/getBestIndex.js +0 -53
- package/dist/methods/getIndexes.d.ts +0 -17
- package/dist/methods/getIndexes.d.ts.map +0 -1
- package/dist/methods/getIndexes.js +0 -98
- package/dist/methods/index.d.ts +0 -9
- package/dist/methods/index.d.ts.map +0 -1
- package/dist/methods/index.js +0 -18
- package/dist/methods/normalize.d.ts +0 -10
- package/dist/methods/normalize.d.ts.map +0 -1
- package/dist/methods/normalize.js +0 -98
- package/dist/methods/validate.d.ts +0 -11
- package/dist/methods/validate.d.ts.map +0 -1
- package/dist/methods/validate.js +0 -112
- package/dist/parser.d.ts +0 -58
- package/dist/parser.d.ts.map +0 -1
- package/dist/parser.js +0 -137
- package/src/grammar/ParserBase.ts +0 -716
- package/src/grammar/createTokens.ts +0 -513
- package/src/grammar/index.ts +0 -4
- package/src/methods/autocomplete.ts +0 -128
- package/src/methods/autoreplace.ts +0 -46
- package/src/methods/autosuggest.ts +0 -543
- package/src/methods/evaluate.ts +0 -39
- package/src/methods/getBestIndex.ts +0 -53
- package/src/methods/getIndexes.ts +0 -100
- package/src/methods/index.ts +0 -10
- package/src/methods/normalize.ts +0 -137
- package/src/methods/validate.ts +0 -143
- 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
|
+
|