@witchcraft/expressit 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- 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 +140 -0
- package/dist/Parser.d.ts.map +1 -0
- package/dist/Parser.js +668 -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/ast/index.d.ts.map +1 -1
- package/dist/examples/index.d.ts +2 -0
- package/dist/examples/index.d.ts.map +1 -0
- package/dist/examples/index.js +4 -0
- package/dist/examples/shortcutContextParser.d.ts +2 -1
- package/dist/examples/shortcutContextParser.d.ts.map +1 -1
- package/dist/examples/shortcutContextParser.js +9 -5
- package/dist/helpers/errors.d.ts.map +1 -1
- package/dist/helpers/errors.js +3 -1
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/parser/checkParserOpts.d.ts.map +1 -1
- package/dist/helpers/parser/checkParserOpts.js +3 -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/methods/autocomplete.d.ts.map +1 -1
- package/dist/methods/autocomplete.js +1 -1
- package/dist/methods/autoreplace.js +1 -1
- package/dist/methods/autosuggest.js +1 -1
- package/dist/methods/evaluate.d.ts.map +1 -1
- package/dist/methods/evaluate.js +3 -1
- package/dist/methods/getIndexes.d.ts.map +1 -1
- package/dist/methods/getIndexes.js +2 -1
- package/dist/methods/normalize.d.ts +0 -2
- package/dist/methods/normalize.d.ts.map +1 -1
- package/dist/methods/normalize.js +2 -3
- package/dist/methods/validate.d.ts.map +1 -1
- package/dist/methods/validate.js +3 -1
- package/dist/package.json.js +44 -37
- package/dist/types/ast.d.ts +2 -8
- 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 +6 -2
- 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 -2
- package/dist/utils/getOppositeDelimiter.d.ts.map +1 -1
- package/dist/utils/getOppositeDelimiter.js +1 -1
- package/dist/utils/prettyAst.d.ts.map +1 -1
- package/dist/utils/prettyAst.js +15 -9
- package/package.json +42 -37
- package/src/Lexer.ts +704 -0
- package/src/Parser.ts +972 -0
- package/src/ast/builders/array.ts +2 -2
- 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/index.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/classes/index.ts +1 -1
- package/src/ast/handlers.ts +6 -6
- package/src/ast/index.ts +2 -2
- package/src/examples/index.ts +3 -0
- package/src/examples/shortcutContextParser.ts +11 -6
- package/src/helpers/errors.ts +5 -3
- package/src/helpers/general/defaultConditionNormalizer.ts +1 -1
- package/src/helpers/general/index.ts +1 -1
- package/src/helpers/index.ts +3 -2
- package/src/helpers/parser/checkParserOpts.ts +13 -12
- package/src/helpers/parser/extractPosition.ts +4 -8
- package/src/helpers/parser/getUnclosedRightParenCount.ts +6 -6
- package/src/helpers/parser/index.ts +1 -1
- package/src/helpers/parser/parseParserOptions.ts +1 -1
- package/src/index.ts +2 -2
- package/src/methods/autocomplete.ts +5 -5
- package/src/methods/autoreplace.ts +2 -2
- package/src/methods/autosuggest.ts +3 -3
- package/src/methods/evaluate.ts +4 -2
- package/src/methods/getIndexes.ts +2 -1
- package/src/methods/normalize.ts +3 -4
- package/src/methods/validate.ts +4 -2
- package/src/types/ast.ts +2 -9
- package/src/types/errors.ts +12 -22
- package/src/types/parser.ts +6 -4
- package/src/utils/extractTokens.ts +1 -1
- package/src/utils/getCursorInfo.ts +6 -4
- package/src/utils/getOppositeDelimiter.ts +5 -2
- package/src/utils/prettyAst.ts +5 -3
- package/dist/examples/advancedValueComparer.d.ts +0 -3
- package/dist/examples/advancedValueComparer.d.ts.map +0 -1
- package/dist/examples/advancedValueComparer.js +0 -28
- package/dist/grammar/ParserBase.d.ts +0 -51
- package/dist/grammar/ParserBase.d.ts.map +0 -1
- package/dist/grammar/ParserBase.js +0 -516
- package/dist/grammar/createTokens.d.ts +0 -56
- package/dist/grammar/createTokens.d.ts.map +0 -1
- package/dist/grammar/createTokens.js +0 -843
- 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/parser.d.ts +0 -58
- package/dist/parser.d.ts.map +0 -1
- package/dist/parser.js +0 -136
- package/src/examples/advancedValueComparer.ts +0 -31
- package/src/grammar/ParserBase.ts +0 -715
- package/src/grammar/createTokens.ts +0 -512
- package/src/grammar/index.ts +0 -4
- package/src/parser.ts +0 -183
package/src/Parser.ts
ADDED
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
import { isArray } from "@alanscodelog/utils/isArray"
|
|
2
|
+
import { isWhitespace } from "@alanscodelog/utils/isWhitespace"
|
|
3
|
+
import { mixin } from "@alanscodelog/utils/mixin"
|
|
4
|
+
import { setReadOnly } from "@alanscodelog/utils/setReadOnly"
|
|
5
|
+
import type { AddParameters, Mixin } from "@alanscodelog/utils/types"
|
|
6
|
+
import { unreachable } from "@alanscodelog/utils/unreachable"
|
|
7
|
+
|
|
8
|
+
import { pos } from "./ast/builders/pos.js"
|
|
9
|
+
import { ArrayNode } from "./ast/classes/ArrayNode.js"
|
|
10
|
+
import { ConditionNode } from "./ast/classes/ConditionNode.js"
|
|
11
|
+
import { ErrorToken } from "./ast/classes/ErrorToken.js"
|
|
12
|
+
import { ExpressionNode } from "./ast/classes/ExpressionNode.js"
|
|
13
|
+
import { GroupNode } from "./ast/classes/GroupNode.js"
|
|
14
|
+
import type { ValidToken } from "./ast/classes/ValidToken.js"
|
|
15
|
+
import { VariableNode } from "./ast/classes/VariableNode.js"
|
|
16
|
+
import * as handle from "./ast/handlers.js"
|
|
17
|
+
import { checkParserOpts } from "./helpers/parser/checkParserOpts.js"
|
|
18
|
+
import { extractPosition } from "./helpers/parser/extractPosition.js"
|
|
19
|
+
import { getUnclosedRightParenCount } from "./helpers/parser/getUnclosedRightParenCount.js"
|
|
20
|
+
import { parseParserOptions } from "./helpers/parser/parseParserOptions.js"
|
|
21
|
+
import { seal } from "./helpers/parser/seal.js"
|
|
22
|
+
import { $C, $T, Lexer,type RealTokenType, type Token, type TokenCategoryType, type TokenType } from "./Lexer.js"
|
|
23
|
+
import { AutocompleteMixin } from "./methods/autocomplete.js"
|
|
24
|
+
import { AutoreplaceMixin } from "./methods/autoreplace.js"
|
|
25
|
+
import { Autosuggest } from "./methods/autosuggest.js"
|
|
26
|
+
import { EvaluateMixin } from "./methods/evaluate.js"
|
|
27
|
+
import { GetBestIndexesMixin } from "./methods/getBestIndex.js"
|
|
28
|
+
import { GetIndexMixin } from "./methods/getIndexes.js"
|
|
29
|
+
import { NormalizeMixin } from "./methods/normalize.js"
|
|
30
|
+
import { ValidateMixin } from "./methods/validate.js"
|
|
31
|
+
import type { ParserResults } from "./types/ast.js"
|
|
32
|
+
import { type AnyToken, type Position, TOKEN_TYPE } from "./types/index.js"
|
|
33
|
+
import type { FullParserOptions, ParserOptions } from "./types/parser.js"
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The parser's methods are often long and have a lot of documentation per method, so it's methods have been split into mixins. They can be found in the `./methods` folder.
|
|
37
|
+
*
|
|
38
|
+
* Writing from within any of these methods is like writing a method from here except:
|
|
39
|
+
* - `this` calls are wrapped in `(this as any as Parser<T>)`
|
|
40
|
+
* - private method/property access requires `// @ts-expect-error`.
|
|
41
|
+
* - recursion with hidden parameters requires re-typing this (see evaluate/validate for examples) since otherwise if we only retyped the function it would become unbound from `this`.
|
|
42
|
+
*
|
|
43
|
+
* Docs work like normal (on methods). From the outside, users of the library cannot even tell the class is composed of mixins.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
export interface Parser<T extends {} = {}> extends Mixin<
|
|
48
|
+
| AutocompleteMixin<T>
|
|
49
|
+
| AutoreplaceMixin
|
|
50
|
+
| Autosuggest<T>
|
|
51
|
+
| EvaluateMixin<T>
|
|
52
|
+
| ValidateMixin<T>
|
|
53
|
+
| NormalizeMixin<T>
|
|
54
|
+
| GetIndexMixin<T>
|
|
55
|
+
| GetBestIndexesMixin
|
|
56
|
+
>,
|
|
57
|
+
AutocompleteMixin<T>,
|
|
58
|
+
AutoreplaceMixin,
|
|
59
|
+
Autosuggest<T>,
|
|
60
|
+
EvaluateMixin<T>,
|
|
61
|
+
ValidateMixin < T >,
|
|
62
|
+
NormalizeMixin<T>,
|
|
63
|
+
GetIndexMixin<T>,
|
|
64
|
+
GetBestIndexesMixin
|
|
65
|
+
{}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Creates the main parser class which handles all functionality (evaluation, validation, etc).
|
|
70
|
+
*/
|
|
71
|
+
export class Parser<T extends {} = {}> {
|
|
72
|
+
// needed for evaluate and validate so they are only checked on demand
|
|
73
|
+
private evaluationOptionsChecked: boolean = false
|
|
74
|
+
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
76
|
+
_checkEvaluationOptions(): void {
|
|
77
|
+
if (!this.evaluationOptionsChecked) {
|
|
78
|
+
checkParserOpts(this.options, true)
|
|
79
|
+
this.evaluationOptionsChecked = true
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private validationOptionsChecked: boolean = false
|
|
84
|
+
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
86
|
+
_checkValidationOptions(): void {
|
|
87
|
+
if (!this.validationOptionsChecked) {
|
|
88
|
+
checkParserOpts(this.options, false, true)
|
|
89
|
+
this.validationOptionsChecked = true
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
options: FullParserOptions<T>
|
|
94
|
+
|
|
95
|
+
private readonly lexer: Lexer
|
|
96
|
+
|
|
97
|
+
private readonly $: Record<$T, RealTokenType<$T>>
|
|
98
|
+
|
|
99
|
+
private readonly $categories: Partial<Record<$C, TokenCategoryType<$C>>>
|
|
100
|
+
|
|
101
|
+
private readonly info: Pick<ReturnType<Lexer["calculateSymbolInfo"]>, "expandedSepAlsoCustom" | "customOpAlsoNegation">
|
|
102
|
+
|
|
103
|
+
constructor(options?: ParserOptions<T>) {
|
|
104
|
+
const opts = parseParserOptions<T>(options ?? {})
|
|
105
|
+
checkParserOpts<T>(opts)
|
|
106
|
+
this.options = opts
|
|
107
|
+
this.lexer = new Lexer(opts)
|
|
108
|
+
this.$ = this.lexer.$
|
|
109
|
+
this.$categories = this.lexer.$categories
|
|
110
|
+
this.info = {
|
|
111
|
+
expandedSepAlsoCustom: this.lexer.symbols.expandedSepAlsoCustom,
|
|
112
|
+
customOpAlsoNegation: this.lexer.symbols.customOpAlsoNegation,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
state: {
|
|
117
|
+
rawInput: string
|
|
118
|
+
lexedTokens: Token<$T>[]
|
|
119
|
+
index: number
|
|
120
|
+
shift: number
|
|
121
|
+
} = {
|
|
122
|
+
rawInput: "",
|
|
123
|
+
lexedTokens: [],
|
|
124
|
+
index: 0,
|
|
125
|
+
shift: 0,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* This is exposed mainly for debugging purposes. Use parse directly instead.
|
|
130
|
+
*/
|
|
131
|
+
lex(input: string): {
|
|
132
|
+
tokens: Token<$T> []
|
|
133
|
+
shift: number
|
|
134
|
+
rawInput: string
|
|
135
|
+
} {
|
|
136
|
+
if (isWhitespace(input)) {
|
|
137
|
+
return { tokens: [], shift: 0, rawInput: input }
|
|
138
|
+
}
|
|
139
|
+
let lexed = this.lexer.tokenize(input)
|
|
140
|
+
/**
|
|
141
|
+
* 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.
|
|
142
|
+
*/
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
const shift = getUnclosedRightParenCount(lexed)
|
|
146
|
+
const rawInput = input
|
|
147
|
+
if (shift) {
|
|
148
|
+
input = "(".repeat(shift) + input
|
|
149
|
+
lexed = this.lexer.tokenize(input)
|
|
150
|
+
}
|
|
151
|
+
const lexedTokens = lexed.filter(token => {
|
|
152
|
+
const tokenType = this.getTokenType(token.type)
|
|
153
|
+
if (tokenType) {
|
|
154
|
+
return !tokenType.skip
|
|
155
|
+
} else {
|
|
156
|
+
throw new Error(`Unknown token type ${token.type}`)
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
return { tokens: lexedTokens, shift, rawInput }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Parse an input string into an AST.
|
|
164
|
+
* It can also parse the result from `lex`, but that is really only for internal use.
|
|
165
|
+
*/
|
|
166
|
+
parse(
|
|
167
|
+
input: string
|
|
168
|
+
| {
|
|
169
|
+
tokens: Token<$T> []
|
|
170
|
+
shift: number
|
|
171
|
+
rawInput: string
|
|
172
|
+
},
|
|
173
|
+
): ParserResults {
|
|
174
|
+
// eslint-disable-next-line prefer-rest-params
|
|
175
|
+
const doSeal = arguments[1]?.seal ?? true
|
|
176
|
+
if (typeof input === "string" && isWhitespace(input)) {
|
|
177
|
+
return handle.token.value(undefined, { start: 0, end: 0 }) as any
|
|
178
|
+
}
|
|
179
|
+
const { tokens: lexedTokens, shift, rawInput } = typeof input === "string" ? this.lex(input) : input
|
|
180
|
+
|
|
181
|
+
this.state = {
|
|
182
|
+
rawInput,
|
|
183
|
+
shift,
|
|
184
|
+
index: -1,
|
|
185
|
+
lexedTokens,
|
|
186
|
+
}
|
|
187
|
+
const res = this.ruleMain()
|
|
188
|
+
if (doSeal) {
|
|
189
|
+
seal(res)
|
|
190
|
+
}
|
|
191
|
+
this.state = {
|
|
192
|
+
rawInput: "",
|
|
193
|
+
shift: 0,
|
|
194
|
+
index: -1,
|
|
195
|
+
lexedTokens: [],
|
|
196
|
+
}
|
|
197
|
+
return res
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
subParserOne?: Parser<T> & {
|
|
201
|
+
parse: AddParameters<Parser<T>["parse"], [{ seal: boolean }]>
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
subParserTwo?: Parser<T> & {
|
|
205
|
+
parse: AddParameters<Parser<T>["parse"], [{ seal: boolean }]>
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
createSubParserIfNotExists(opts: ParserOptions<T>, which: "One" | "Two" = "One"): Parser["subParserOne"] {
|
|
210
|
+
if (this[`subParser${which}`] === undefined) {
|
|
211
|
+
this[`subParser${which}`] = new Parser(opts)
|
|
212
|
+
}
|
|
213
|
+
return this[`subParser${which}`]!
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
transformCategoryToken<TC extends $C>(
|
|
218
|
+
token: Token,
|
|
219
|
+
categoryToken: TokenCategoryType<TC>,
|
|
220
|
+
): Token<TC> {
|
|
221
|
+
return {
|
|
222
|
+
...token,
|
|
223
|
+
type: categoryToken.type,
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
getCategoryTokens<TType extends $C>(
|
|
228
|
+
type: TType,
|
|
229
|
+
): TokenCategoryType<TType>["entries"] | undefined {
|
|
230
|
+
return this.$categories[type as $C]?.entries as any
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
getTokenType(type: $T | $C): TokenType<$T> | undefined {
|
|
234
|
+
return this.$[type as any as $T] as any
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
isExactType<TType extends $T>(token: Token, type: TType): token is Token<TType> {
|
|
238
|
+
if (this.$[type]) {
|
|
239
|
+
return this.isType(token, type)
|
|
240
|
+
}
|
|
241
|
+
return false
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
isType(token: Token | undefined, type: $T | $C): boolean {
|
|
245
|
+
if (token === undefined) return false
|
|
246
|
+
if (token.type === type) return true
|
|
247
|
+
const tokenType = this.getTokenType(token.type)
|
|
248
|
+
|
|
249
|
+
if (tokenType?.type === type) return true
|
|
250
|
+
const category = this.$categories[type as $C]
|
|
251
|
+
if (category?.entries[token.type as $T] !== undefined) {
|
|
252
|
+
return true
|
|
253
|
+
}
|
|
254
|
+
return false
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
createErrorToken(type: $T, index?: number): Token {
|
|
258
|
+
return {
|
|
259
|
+
type,
|
|
260
|
+
value: "",
|
|
261
|
+
startOffset: index ?? this.state.index,
|
|
262
|
+
endOffset: index ?? this.state.index,
|
|
263
|
+
isError: true,
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
processToken<TDefined extends boolean = boolean>(token?: Pick<Token, "value" | "startOffset" | "endOffset">): [TDefined extends true ? string : string | undefined, Position] {
|
|
268
|
+
if (token === undefined) {
|
|
269
|
+
return [undefined as any, extractPosition({ startOffset: 0, endOffset: 0 }, this.state.shift)]
|
|
270
|
+
} else {
|
|
271
|
+
return [token.value, extractPosition(token, this.state.shift)]
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
peek(n = 1): Token<$T> | undefined {
|
|
277
|
+
return this.state.lexedTokens[this.state.index + n]
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
nextIsEof(): boolean {
|
|
281
|
+
return this.peek(1) === undefined
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
consumeAny(): Token<$T> {
|
|
285
|
+
return this.consume(this.peek(1)?.type)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
consume<
|
|
289
|
+
TType extends $T | $C,
|
|
290
|
+
>(
|
|
291
|
+
type: TType | undefined,
|
|
292
|
+
): Token<TType> {
|
|
293
|
+
if (type === undefined) {
|
|
294
|
+
throw new Error("type is undefined")
|
|
295
|
+
}
|
|
296
|
+
const nextToken = this.peek(1)
|
|
297
|
+
if (nextToken === undefined) {
|
|
298
|
+
throw new Error(`Reached end of input without consuming a token of type ${type}`)
|
|
299
|
+
}
|
|
300
|
+
if (this.$categories[type as $C] !== undefined) {
|
|
301
|
+
const categoryToken = this.$categories[type as $C]
|
|
302
|
+
const tokenType = categoryToken?.entries[nextToken.type as $T]
|
|
303
|
+
if (categoryToken && tokenType) {
|
|
304
|
+
this.state.index++
|
|
305
|
+
return this.transformCategoryToken(nextToken, categoryToken) as Token<TType>
|
|
306
|
+
} else {
|
|
307
|
+
throw new Error("here")
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
const tokenType = this.getTokenType(type as $T)
|
|
311
|
+
if (tokenType !== undefined) {
|
|
312
|
+
if (nextToken?.type === tokenType.type) {
|
|
313
|
+
this.state.index++
|
|
314
|
+
return nextToken as any
|
|
315
|
+
} else {
|
|
316
|
+
throw new Error(`Expected token type ${tokenType.type}, got ${nextToken?.type}`)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
throw new Error(`Unknown token type ${type}`)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
saveState(): Parser["state"] {
|
|
324
|
+
return { ...this.state }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
restoreState(state: Parser["state"]): void {
|
|
328
|
+
this.state = state // careful, we assume this is an untouched copy
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
ruleMain(): ParserResults {
|
|
332
|
+
const res = this.ruleBool("OR")
|
|
333
|
+
if (res === undefined) {
|
|
334
|
+
const error = handle.token.value(undefined, { start: 0, end: 0 })
|
|
335
|
+
return error
|
|
336
|
+
}
|
|
337
|
+
return res
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
341
|
+
ruleBool<TType extends"AND" | "OR">(type: TType) {
|
|
342
|
+
const OP_TYPE = type === "AND" ? $C.OPERATOR_AND : $C.OPERATOR_OR
|
|
343
|
+
|
|
344
|
+
const pairs: any[][] = [] as any
|
|
345
|
+
let next = this.peek(1)
|
|
346
|
+
|
|
347
|
+
while (pairs.length < 1 || pairs[pairs.length - 1]?.[1] !== undefined) {
|
|
348
|
+
const exp = type === "AND" ? this.ruleCondition() : this.ruleBool("AND")
|
|
349
|
+
next = this.peek(1)
|
|
350
|
+
const canAttemptErrorRecovery = type === "AND"
|
|
351
|
+
? ["error", "and"].includes(this.options.onMissingBooleanOperator)
|
|
352
|
+
: this.options.onMissingBooleanOperator === "or"
|
|
353
|
+
const extras: any[] = []
|
|
354
|
+
if (
|
|
355
|
+
canAttemptErrorRecovery
|
|
356
|
+
&& (
|
|
357
|
+
this.isType(next, $C.VALUE)
|
|
358
|
+
|| this.isType(next, $C.QUOTE_ANY)
|
|
359
|
+
|| this.isType(next, $T.PAREN_L)
|
|
360
|
+
|| this.isType(next, $T.EXP_PROP_OP)
|
|
361
|
+
|| this.isType(next, $T.REGEX_START)
|
|
362
|
+
|| this.isType(next, $T.CUSTOM_PROP_OP)
|
|
363
|
+
)
|
|
364
|
+
) {
|
|
365
|
+
let state = this.saveState()
|
|
366
|
+
let cond = this.ruleCondition()
|
|
367
|
+
if (type === "AND") {
|
|
368
|
+
let dummyOp
|
|
369
|
+
while (cond !== undefined) {
|
|
370
|
+
if (this.options.onMissingBooleanOperator === "and") {
|
|
371
|
+
// the operator is missing between the previous token and this exp
|
|
372
|
+
const prev = this.peek(-1)!
|
|
373
|
+
const start = prev.endOffset! + 1
|
|
374
|
+
dummyOp = handle.operator.and("", pos({ start }, { fill: true }))
|
|
375
|
+
}
|
|
376
|
+
extras.push([dummyOp, cond])
|
|
377
|
+
state = this.saveState()
|
|
378
|
+
cond = this.ruleCondition()
|
|
379
|
+
}
|
|
380
|
+
// todo i don't think we need to backtrack
|
|
381
|
+
this.restoreState(state)
|
|
382
|
+
} else {
|
|
383
|
+
// the operator is missing between the previous token and this exp
|
|
384
|
+
const prev = this.peek(-1)!
|
|
385
|
+
const start = prev.endOffset! + 1
|
|
386
|
+
const dummyOp = handle.operator.or("", pos({ start }, { fill: true }))
|
|
387
|
+
extras.push([dummyOp, cond])
|
|
388
|
+
}
|
|
389
|
+
next = this.peek(1)
|
|
390
|
+
}
|
|
391
|
+
const sepToken = this.isType(next, OP_TYPE) && next
|
|
392
|
+
? type === "AND"
|
|
393
|
+
? handle.operator.and(...this.processToken<true>(this.consume(next.type)))
|
|
394
|
+
: handle.operator.or(...this.processToken<true>(this.consume(next.type)))
|
|
395
|
+
: undefined
|
|
396
|
+
|
|
397
|
+
pairs.push([
|
|
398
|
+
exp,
|
|
399
|
+
sepToken,
|
|
400
|
+
])
|
|
401
|
+
next = this.peek(1)
|
|
402
|
+
for (const extra of extras) {
|
|
403
|
+
pairs[pairs.length - 1].splice(1, 1, extra[0])
|
|
404
|
+
pairs.push([extra[1]])
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (pairs.length === 0 && this.isType(this.peek(1), OP_TYPE)) {
|
|
409
|
+
next = this.peek(-1)
|
|
410
|
+
let state = this.saveState()
|
|
411
|
+
while (this.isType(next, $C.OPERATOR_AND)) {
|
|
412
|
+
const token = this.consume($C.OPERATOR_AND)
|
|
413
|
+
pairs.push([
|
|
414
|
+
undefined,
|
|
415
|
+
type === "AND"
|
|
416
|
+
? handle.operator.and(...this.processToken<true>(token))
|
|
417
|
+
: handle.operator.or(...this.processToken<true>(token)),
|
|
418
|
+
])
|
|
419
|
+
next = this.peek(-1)
|
|
420
|
+
while (this.isType(next, $C.VALUE) || this.isType(next, $C.QUOTE_ANY) || this.isType(next, $T.PAREN_L)) {
|
|
421
|
+
pairs.push([this.ruleCondition()])
|
|
422
|
+
next = this.peek(-1)
|
|
423
|
+
}
|
|
424
|
+
state = this.saveState()
|
|
425
|
+
}
|
|
426
|
+
this.restoreState(state)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (type === "AND" && pairs.length === 0) return undefined
|
|
430
|
+
// handle situations like `a ||` where b is missing
|
|
431
|
+
let res = pairs[pairs.length - 1][0]
|
|
432
|
+
for (let i = pairs.length - 1; i > 0; i--) {
|
|
433
|
+
const before = pairs[i - 1]
|
|
434
|
+
if (type === "OR" && res === undefined && before === undefined) return undefined
|
|
435
|
+
res = handle.expression(before[0], before[1], res)
|
|
436
|
+
}
|
|
437
|
+
return res
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
ruleCondition(): ConditionNode | GroupNode | ConditionNode<boolean> | undefined {
|
|
441
|
+
const not = this.ruleNot()
|
|
442
|
+
const property = this.ruleConditionProperty()
|
|
443
|
+
const propVal = property?.prop?.value === undefined
|
|
444
|
+
? undefined
|
|
445
|
+
: property.prop.value instanceof ErrorToken
|
|
446
|
+
? ""
|
|
447
|
+
: property.prop.value.value
|
|
448
|
+
|
|
449
|
+
const propOpVal = property?.rest?.propertyOperator === undefined
|
|
450
|
+
? undefined
|
|
451
|
+
: property.rest.propertyOperator instanceof ErrorToken
|
|
452
|
+
? ""
|
|
453
|
+
: property.rest.propertyOperator?.value
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
const isExpanded = (property?.rest?.sepL ?? property?.rest?.sepR) !== undefined
|
|
457
|
+
|
|
458
|
+
const convertRegexValues = typeof this.options.regexValues === "function"
|
|
459
|
+
&& !this.options.regexValues(propVal, propOpVal, isExpanded)
|
|
460
|
+
|
|
461
|
+
const convertArrayValues = typeof this.options.arrayValues === "function"
|
|
462
|
+
&& !this.options.arrayValues(propVal, propOpVal, isExpanded)
|
|
463
|
+
|
|
464
|
+
let value = this.ruleConditionValue(property, { convertRegexValues, convertArrayValues })
|
|
465
|
+
|
|
466
|
+
let group
|
|
467
|
+
if (!(value instanceof ArrayNode)
|
|
468
|
+
&& !isArray(value)
|
|
469
|
+
&& (!value || this.options.prefixableGroups)
|
|
470
|
+
&& this.isType(this.peek(1), $T.PAREN_L) // is not already plain group
|
|
471
|
+
) {
|
|
472
|
+
group = this.rulePlainGroup({ onlyValues: property !== undefined, convertRegexValues, convertArrayValues })
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (isArray(value)) {
|
|
476
|
+
group = value
|
|
477
|
+
value = undefined
|
|
478
|
+
}
|
|
479
|
+
if (convertRegexValues && value instanceof VariableNode && value.quote?.left.type === TOKEN_TYPE.REGEX) {
|
|
480
|
+
value = handle.variable(undefined, undefined, handle.token.value(
|
|
481
|
+
(value.quote?.left?.value ?? "") + (value.value.value ?? "") + (value.quote?.right?.value ?? ""),
|
|
482
|
+
pos(value),
|
|
483
|
+
), undefined) as ReturnType<Parser["ruleVariable"]>
|
|
484
|
+
}
|
|
485
|
+
if (group) {
|
|
486
|
+
if (property) {
|
|
487
|
+
return handle.condition(not, property?.prop, property?.rest, handle.group(undefined, undefined, ...group))
|
|
488
|
+
}
|
|
489
|
+
if (value) {
|
|
490
|
+
return handle.group(undefined, handle.condition(not, undefined, undefined, value), ...group)
|
|
491
|
+
}
|
|
492
|
+
return handle.group(not, value, ...group)
|
|
493
|
+
}
|
|
494
|
+
if ([not, property, value].every(_ => _ === undefined)) return undefined
|
|
495
|
+
|
|
496
|
+
return handle.condition(not, property?.prop, property?.rest, value as any)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
ruleConditionValue(
|
|
500
|
+
property: ReturnType<Parser<T>["ruleConditionProperty"]>,
|
|
501
|
+
{ convertRegexValues = false, convertArrayValues = false }:
|
|
502
|
+
{ convertRegexValues?: boolean, convertArrayValues?: boolean } = {},
|
|
503
|
+
): ReturnType<Parser["rulePlainGroup"]>
|
|
504
|
+
| ReturnType<Parser["rulePlainBracketGroup"]>
|
|
505
|
+
| ReturnType<Parser["ruleVariable"]>
|
|
506
|
+
| undefined
|
|
507
|
+
{
|
|
508
|
+
const next = this.peek(1)
|
|
509
|
+
const next2 = this.peek(2)
|
|
510
|
+
const next3 = this.peek(3)
|
|
511
|
+
const next4 = this.peek(4)
|
|
512
|
+
if (this.options.prefixableGroups
|
|
513
|
+
&& property === undefined
|
|
514
|
+
&& next?.type !== $T.PAREN_L // moves to parsing group below instead
|
|
515
|
+
&& (
|
|
516
|
+
(
|
|
517
|
+
this.isType(next, $C.VALUE)
|
|
518
|
+
&& (
|
|
519
|
+
this.isType(next2, $T.PAREN_L) // a(
|
|
520
|
+
|| (this.isType(next2, $C.QUOTE_ANY) && this.isType(next3, $T.PAREN_L)) // a"(
|
|
521
|
+
)
|
|
522
|
+
)
|
|
523
|
+
|| (
|
|
524
|
+
this.isType(next, $C.QUOTE_ANY)
|
|
525
|
+
&& (
|
|
526
|
+
this.isType(next2, $T.PAREN_L) // "(
|
|
527
|
+
|| (this.isType(next2, $C.VALUE)
|
|
528
|
+
&& (
|
|
529
|
+
this.isType(next3, $T.PAREN_L) || // "a(
|
|
530
|
+
(this.isType(next3, $C.QUOTE_ANY) && this.isType(next4, $T.PAREN_L)) // "a"(
|
|
531
|
+
)
|
|
532
|
+
)
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
)
|
|
536
|
+
) {
|
|
537
|
+
const res = this.ruleVariable({ unprefixed: true })
|
|
538
|
+
if (res) return res
|
|
539
|
+
}
|
|
540
|
+
if (!this.isType(next, $T.PAREN_L)) {
|
|
541
|
+
const res = this.ruleVariable({ unprefixed: false })
|
|
542
|
+
if (res) return res
|
|
543
|
+
}
|
|
544
|
+
if (this.isType(next, $T.PAREN_L)) {
|
|
545
|
+
const res = this.rulePlainGroup({ onlyValues: property !== undefined, convertRegexValues, convertArrayValues })
|
|
546
|
+
if (res) return res
|
|
547
|
+
}
|
|
548
|
+
if (this.isType(next, $T.BRACKET_L)) {
|
|
549
|
+
const res = this.rulePlainBracketGroup({ convertArrayValues })
|
|
550
|
+
if (res) return res
|
|
551
|
+
}
|
|
552
|
+
return undefined
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
rulePlainGroup(
|
|
556
|
+
{ onlyValues = false, convertRegexValues = false, convertArrayValues = false }:
|
|
557
|
+
{ onlyValues?: boolean, convertRegexValues?: boolean, convertArrayValues?: boolean } = {},
|
|
558
|
+
): [
|
|
559
|
+
ValidToken<TOKEN_TYPE.PARENL> | undefined,
|
|
560
|
+
GroupNode["expression"],
|
|
561
|
+
ValidToken<TOKEN_TYPE.PARENR> | undefined,
|
|
562
|
+
] {
|
|
563
|
+
const parenL = this.ruleParenL()
|
|
564
|
+
let parenLeftCount = 0
|
|
565
|
+
let start: undefined | number
|
|
566
|
+
let end: undefined | number
|
|
567
|
+
const condition = !onlyValues ? this.ruleBool("OR") : undefined
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* 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(...))`).
|
|
571
|
+
*
|
|
572
|
+
* 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.
|
|
573
|
+
*
|
|
574
|
+
* 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.
|
|
575
|
+
*
|
|
576
|
+
* 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").
|
|
577
|
+
*/
|
|
578
|
+
|
|
579
|
+
if (onlyValues && !this.nextIsEof()) {
|
|
580
|
+
while (
|
|
581
|
+
!this.nextIsEof()
|
|
582
|
+
&& (!this.isType(this.peek(1), $T.PAREN_R) || parenLeftCount !== 0)
|
|
583
|
+
) {
|
|
584
|
+
const token = this.consumeAny()
|
|
585
|
+
start ??= extractPosition(token, this.state.shift).start
|
|
586
|
+
if (token.type === $T.PAREN_L) {
|
|
587
|
+
parenLeftCount++
|
|
588
|
+
}
|
|
589
|
+
if (token.type === $T.PAREN_R) {
|
|
590
|
+
parenLeftCount--
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (start !== undefined) {
|
|
596
|
+
end ??= extractPosition(this.peek(0)!, this.state.shift).end
|
|
597
|
+
}
|
|
598
|
+
const parenR = this.isType(this.peek(1), $T.PAREN_R) ? this.ruleParenR() : undefined
|
|
599
|
+
if (start !== undefined) {
|
|
600
|
+
const subInput = this.state.rawInput.slice(start, end)
|
|
601
|
+
this.createSubParserIfNotExists({
|
|
602
|
+
...this.options,
|
|
603
|
+
customPropertyOperators: [],
|
|
604
|
+
expandedPropertySeparator: undefined,
|
|
605
|
+
regexValues: convertRegexValues,
|
|
606
|
+
arrayValues: convertArrayValues,
|
|
607
|
+
}, "One")
|
|
608
|
+
const parsed = this.subParserOne!.parse(" ".repeat(start) + subInput, { seal: false })
|
|
609
|
+
return [parenL, parsed, parenR]
|
|
610
|
+
}
|
|
611
|
+
return [parenL, condition, parenR]
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
rulePlainBracketGroup(
|
|
615
|
+
{ convertArrayValues = false }:
|
|
616
|
+
{ convertArrayValues?: boolean } = {},
|
|
617
|
+
): ArrayNode | VariableNode {
|
|
618
|
+
const bracketL = this.ruleBracketL()
|
|
619
|
+
|
|
620
|
+
const values: any[] = []
|
|
621
|
+
|
|
622
|
+
if (!convertArrayValues) {
|
|
623
|
+
let state = this.saveState()
|
|
624
|
+
let variable = this.ruleVariable({ unprefixed: false })
|
|
625
|
+
while (variable !== undefined) {
|
|
626
|
+
values.push(variable)
|
|
627
|
+
state = this.saveState()
|
|
628
|
+
variable = this.ruleVariable({ unprefixed: false })
|
|
629
|
+
}
|
|
630
|
+
this.restoreState(state)
|
|
631
|
+
} else if (convertArrayValues && !this.nextIsEof()) {
|
|
632
|
+
while (
|
|
633
|
+
!this.nextIsEof()
|
|
634
|
+
&& !this.isType(this.peek(1), $T.BRACKET_R)
|
|
635
|
+
) {
|
|
636
|
+
this.consumeAny()
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const bracketR = this.isType(this.peek(1), $T.BRACKET_R) ? this.ruleBracketR() : undefined
|
|
640
|
+
if (bracketL === undefined) throw new Error("bracketL is undefined, peek before using rule.")
|
|
641
|
+
if (!convertArrayValues) {
|
|
642
|
+
return handle.array(bracketL, values, bracketR)
|
|
643
|
+
}
|
|
644
|
+
const start = bracketL.start
|
|
645
|
+
const end = bracketR?.end
|
|
646
|
+
/**
|
|
647
|
+
* Similar problem as with plain groups above.
|
|
648
|
+
*/
|
|
649
|
+
const subInput = this.state.rawInput.slice(start, end)
|
|
650
|
+
this.createSubParserIfNotExists({
|
|
651
|
+
...this.options,
|
|
652
|
+
customPropertyOperators: [],
|
|
653
|
+
expandedPropertySeparator: undefined,
|
|
654
|
+
arrayValues: false,
|
|
655
|
+
}, "Two")
|
|
656
|
+
const parsed = this.subParserTwo!.parse(" ".repeat(start) + subInput, { seal: false })
|
|
657
|
+
if (parsed instanceof ConditionNode) {
|
|
658
|
+
return parsed.value as ArrayNode
|
|
659
|
+
}
|
|
660
|
+
if (parsed instanceof ErrorToken || parsed instanceof ExpressionNode || parsed instanceof GroupNode) {
|
|
661
|
+
unreachable("parsed.value should not be an ErrorToken, ExpressionNode, or GroupNode.")
|
|
662
|
+
}
|
|
663
|
+
return parsed
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
ruleConditionProperty(): {
|
|
667
|
+
prop?: VariableNode
|
|
668
|
+
rest: Parameters<typeof handle.condition>[2]
|
|
669
|
+
} | undefined {
|
|
670
|
+
const current = this.peek(0)
|
|
671
|
+
const next = this.peek(1)
|
|
672
|
+
const next2 = this.peek(2)
|
|
673
|
+
if (this.isType(next, $T.EXP_PROP_OP)
|
|
674
|
+
|| this.isType(next, $T.CUSTOM_PROP_OP)
|
|
675
|
+
|| (this.isType(next, $T.VALUE_UNQUOTED) && (
|
|
676
|
+
this.isType(next2, $T.EXP_PROP_OP)
|
|
677
|
+
|| this.isType(next2, $T.CUSTOM_PROP_OP)
|
|
678
|
+
))
|
|
679
|
+
|| (
|
|
680
|
+
this.info.customOpAlsoNegation
|
|
681
|
+
&& (
|
|
682
|
+
this.isType(next2, $T.SYM_NOT)
|
|
683
|
+
|| (this.isType(current, $T.SYM_NOT) && this.isType(next, $T.SYM_NOT))
|
|
684
|
+
)
|
|
685
|
+
)
|
|
686
|
+
) {
|
|
687
|
+
return this.ruleProperty()
|
|
688
|
+
}
|
|
689
|
+
return undefined
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
ruleProperty(): {
|
|
693
|
+
prop?: VariableNode
|
|
694
|
+
rest: Parameters<typeof handle.condition>[2]
|
|
695
|
+
} {
|
|
696
|
+
const prop = this.ruleVariable({ unprefixed: true })
|
|
697
|
+
const next = this.peek(1)
|
|
698
|
+
let rest: Parameters<typeof handle.condition>[2] = {} as any
|
|
699
|
+
if (this.isType(next, $T.EXP_PROP_OP)) {
|
|
700
|
+
const sepL = handle.token.sep(...this.processToken<true>(this.consume($T.EXP_PROP_OP)))
|
|
701
|
+
const op = this.isType(this.peek(1), $T.VALUE_UNQUOTED)
|
|
702
|
+
? handle.token.value(...this.processToken<true>(this.consume($T.VALUE_UNQUOTED)))
|
|
703
|
+
: undefined
|
|
704
|
+
const sepR = this.isType(this.peek(1), $T.EXP_PROP_OP)
|
|
705
|
+
? handle.token.sep(...this.processToken<true>(this.consume($T.EXP_PROP_OP)))
|
|
706
|
+
: undefined
|
|
707
|
+
if (this.info.expandedSepAlsoCustom && op === undefined && sepR === undefined) {
|
|
708
|
+
setReadOnly(sepL, "type", TOKEN_TYPE.OP_CUSTOM as any)
|
|
709
|
+
rest = {
|
|
710
|
+
sepL: undefined,
|
|
711
|
+
sepR,
|
|
712
|
+
propertyOperator: sepL as any as AnyToken<TOKEN_TYPE.OP_CUSTOM>,
|
|
713
|
+
}
|
|
714
|
+
} else {
|
|
715
|
+
rest = { sepL, sepR, propertyOperator: op }
|
|
716
|
+
}
|
|
717
|
+
} else if (this.isType(next, $T.CUSTOM_PROP_OP)) {
|
|
718
|
+
const op = handle.token.custom(...this.processToken(this.consume($T.CUSTOM_PROP_OP)))
|
|
719
|
+
rest = { propertyOperator: op }
|
|
720
|
+
} else if (this.info.customOpAlsoNegation && this.isType(next, $T.SYM_NOT)) {
|
|
721
|
+
const op = handle.token.custom(...this.processToken(this.consume($T.SYM_NOT)))
|
|
722
|
+
rest = { propertyOperator: op }
|
|
723
|
+
}
|
|
724
|
+
return { prop, rest }
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
ruleVariable({
|
|
728
|
+
unprefixed = false,
|
|
729
|
+
}: {
|
|
730
|
+
unprefixed?: boolean
|
|
731
|
+
} = {}): VariableNode | undefined {
|
|
732
|
+
const prefix = this.ruleVariablePrefix({ onlyToken: true, unprefixed })
|
|
733
|
+
|
|
734
|
+
const next = this.peek(1)
|
|
735
|
+
const next2 = this.peek(2)
|
|
736
|
+
const next3 = this.peek(3)
|
|
737
|
+
// quoted values
|
|
738
|
+
if (next && (this.isExactType(next, $T.QUOTE_DOUBLE)
|
|
739
|
+
|| this.isExactType(next, $T.QUOTE_SINGLE)
|
|
740
|
+
|| this.isExactType(next, $T.QUOTE_BACKTICK)
|
|
741
|
+
)) {
|
|
742
|
+
const quoteType = next.type
|
|
743
|
+
if (next2?.type === quoteType) {
|
|
744
|
+
// value is missing
|
|
745
|
+
const quoteL = this.ruleQuote(quoteType)
|
|
746
|
+
const quoteR = this.ruleQuote(quoteType)
|
|
747
|
+
return handle.variable(undefined, quoteL, undefined, quoteR)
|
|
748
|
+
}
|
|
749
|
+
if (next3?.type === next.type) {
|
|
750
|
+
const quoteL = this.ruleQuote(quoteType)
|
|
751
|
+
const value = this.isType(next2, $T.VALUE_UNQUOTED) ? this.ruleValueUnquoted({ }) : this.ruleValueNot(quoteType)
|
|
752
|
+
const quoteR = this.ruleQuote(quoteType)
|
|
753
|
+
const prefixToken = prefix ? handle.token.value(...this.processToken<true>(prefix)) : undefined
|
|
754
|
+
return handle.variable(prefixToken, quoteL, value, quoteR)
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (this.isType(next, $C.REGEX_ANY)) {
|
|
758
|
+
// this is safe since the start can never match flags
|
|
759
|
+
const quoteL = this.ruleRegexAny() as ValidToken<TOKEN_TYPE.REGEX>
|
|
760
|
+
// unlike other values, regexes will swallow all input if incorrect
|
|
761
|
+
const maybeValue = this.peek(1)
|
|
762
|
+
// note the inversion (todo inverse map)
|
|
763
|
+
const value = this.isType(maybeValue, $T.VALUE_REGEX)
|
|
764
|
+
? this.ruleValueNot($C.REGEX_ANY)
|
|
765
|
+
: undefined
|
|
766
|
+
|
|
767
|
+
const quoteR = this.isType(this.peek(1), $C.REGEX_ANY) ? this.ruleRegexAny() : undefined
|
|
768
|
+
const args = isArray(quoteR) ? quoteR : [quoteR, undefined] as const
|
|
769
|
+
return handle.variable(undefined, quoteL, value, args[0], args[1] as any)
|
|
770
|
+
}
|
|
771
|
+
if (this.isType(next, $T.VALUE_UNQUOTED) && this.isType(next2, $C.QUOTE_ANY)) {
|
|
772
|
+
const value = this.ruleValueUnquoted()
|
|
773
|
+
const quoteR = this.ruleValueDelimAny()
|
|
774
|
+
return handle.variable(undefined, undefined, value, quoteR)
|
|
775
|
+
}
|
|
776
|
+
if (this.isType(next, $C.QUOTE_ANY)) {
|
|
777
|
+
const quoteToken = next as Token<$T.QUOTE_BACKTICK | $T.QUOTE_DOUBLE | $T.QUOTE_SINGLE>
|
|
778
|
+
const quoteL = this.ruleValueDelimAny()
|
|
779
|
+
const maybeValue = this.peek(1)
|
|
780
|
+
const value = !quoteL && this.isType(maybeValue, $T.VALUE_UNQUOTED)
|
|
781
|
+
? this.ruleValueUnquoted()
|
|
782
|
+
// todo, move inverse quote map out of ruleValueNot
|
|
783
|
+
: quoteL && this.isType(maybeValue, quoteToken.type.replace("QUOTE", "VALUE_FOR") as any)
|
|
784
|
+
? this.ruleValueNot(quoteToken.type)
|
|
785
|
+
: undefined
|
|
786
|
+
return handle.variable(undefined, quoteL, value, undefined)
|
|
787
|
+
}
|
|
788
|
+
if (this.isType(next, $T.VALUE_UNQUOTED)) {
|
|
789
|
+
const value = this.ruleValueUnquoted()
|
|
790
|
+
return handle.variable(undefined, undefined, value, undefined)
|
|
791
|
+
}
|
|
792
|
+
return undefined
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
ruleValueDelimAny(): ValidToken<TOKEN_TYPE.SINGLEQUOTE | TOKEN_TYPE.DOUBLEQUOTE | TOKEN_TYPE.BACKTICK | TOKEN_TYPE.REGEX> | undefined {
|
|
796
|
+
const next = this.peek(1)!
|
|
797
|
+
|
|
798
|
+
if (this.isType(next, $C.QUOTE_ANY)) {
|
|
799
|
+
const type = next.value === `"` ? "double" : next.value === "'" ? "single" : next.value === "`" ? "tick" : "regex"
|
|
800
|
+
return handle.delimiter[type](...this.processToken(this.consume($C.QUOTE_ANY)))
|
|
801
|
+
}
|
|
802
|
+
return undefined
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
ruleRegexAny(): ValidToken<TOKEN_TYPE.REGEX> | [ValidToken<TOKEN_TYPE.REGEX>, AnyToken<TOKEN_TYPE.VALUE>] {
|
|
806
|
+
const value = this.consume($C.REGEX_ANY)
|
|
807
|
+
if (value.value.length > 1) {
|
|
808
|
+
// cheat a bit to extract the flags
|
|
809
|
+
const delim = {
|
|
810
|
+
value: "/",
|
|
811
|
+
startOffset: value.startOffset,
|
|
812
|
+
endOffset: value.startOffset,
|
|
813
|
+
}
|
|
814
|
+
const flags = {
|
|
815
|
+
value: value.value.slice(1),
|
|
816
|
+
startOffset: value.startOffset + 1,
|
|
817
|
+
endOffset: value.endOffset,
|
|
818
|
+
}
|
|
819
|
+
return [
|
|
820
|
+
// why the ! ??? todo
|
|
821
|
+
handle.delimiter.regex(...this.processToken(delim))!,
|
|
822
|
+
handle.token.value(...this.processToken(flags)),
|
|
823
|
+
]
|
|
824
|
+
}
|
|
825
|
+
return handle.delimiter.regex(...this.processToken(value))!
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
ruleValueNot<
|
|
829
|
+
TType extends $T.QUOTE_SINGLE | $T.QUOTE_DOUBLE | $T.QUOTE_BACKTICK | $C.REGEX_ANY,
|
|
830
|
+
>(
|
|
831
|
+
type: TType,
|
|
832
|
+
): ValidToken<
|
|
833
|
+
TOKEN_TYPE.VALUE
|
|
834
|
+
> {
|
|
835
|
+
const realType = {
|
|
836
|
+
[$T.QUOTE_SINGLE]: $C.VALUE_FOR_SINGLE,
|
|
837
|
+
[$T.QUOTE_DOUBLE]: $C.VALUE_FOR_DOUBLE,
|
|
838
|
+
[$T.QUOTE_BACKTICK]: $C.VALUE_FOR_BACKTICK,
|
|
839
|
+
[$C.REGEX_ANY]: $T.VALUE_REGEX,
|
|
840
|
+
}[type]
|
|
841
|
+
if (realType === undefined) {
|
|
842
|
+
unreachable(`Unknown quote/regex type ${type}`)
|
|
843
|
+
}
|
|
844
|
+
const value = this.consume(realType)
|
|
845
|
+
if (realType !== value.type) {
|
|
846
|
+
unreachable(`Expected value type ${realType}, got ${value.type}`)
|
|
847
|
+
}
|
|
848
|
+
return handle.token.value(...this.processToken(value)) as any
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
ruleQuote<TType extends $T.QUOTE_SINGLE | $T.QUOTE_DOUBLE | $T.QUOTE_BACKTICK >(
|
|
852
|
+
type: TType,
|
|
853
|
+
): ValidToken<
|
|
854
|
+
TType extends $T.QUOTE_SINGLE
|
|
855
|
+
? TOKEN_TYPE.SINGLEQUOTE
|
|
856
|
+
: TType extends $T.QUOTE_DOUBLE
|
|
857
|
+
? TOKEN_TYPE.DOUBLEQUOTE
|
|
858
|
+
: TType extends $T.QUOTE_BACKTICK
|
|
859
|
+
? TOKEN_TYPE.BACKTICK
|
|
860
|
+
: never
|
|
861
|
+
> {
|
|
862
|
+
const quote = this.peek(1)
|
|
863
|
+
if (type !== quote?.type) {
|
|
864
|
+
throw new Error(`Expected quote type ${type}, got ${quote?.type}`)
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
switch (type) {
|
|
868
|
+
case $T.QUOTE_SINGLE:
|
|
869
|
+
return handle.delimiter.single(
|
|
870
|
+
...this.processToken(this.consume($T.QUOTE_SINGLE)),
|
|
871
|
+
) as any
|
|
872
|
+
case $T.QUOTE_DOUBLE:
|
|
873
|
+
return handle.delimiter.double(
|
|
874
|
+
...this.processToken(this.consume($T.QUOTE_DOUBLE)),
|
|
875
|
+
) as any
|
|
876
|
+
case $T.QUOTE_BACKTICK:
|
|
877
|
+
return handle.delimiter.tick(
|
|
878
|
+
...this.processToken(this.consume($T.QUOTE_BACKTICK)),
|
|
879
|
+
) as any
|
|
880
|
+
}
|
|
881
|
+
throw new Error(`Expected quote type ${type}`)
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
ruleVariablePrefix<TOnlyToken extends boolean = false>(
|
|
886
|
+
{
|
|
887
|
+
|
|
888
|
+
onlyToken = false as TOnlyToken,
|
|
889
|
+
unprefixed = false,
|
|
890
|
+
}: {
|
|
891
|
+
onlyToken?: TOnlyToken
|
|
892
|
+
unprefixed?: boolean
|
|
893
|
+
} = {},
|
|
894
|
+
): TOnlyToken extends true ? Token<$T.VALUE_UNQUOTED> | undefined : AnyToken<TOKEN_TYPE.VALUE> | undefined {
|
|
895
|
+
const next = this.peek(1)
|
|
896
|
+
const next2 = this.peek(2)
|
|
897
|
+
const next4 = this.peek(4)
|
|
898
|
+
if (!unprefixed && this.options.prefixableStrings !== undefined
|
|
899
|
+
&& this.isType(next2, $C.QUOTE_ANY)
|
|
900
|
+
&& next2 && this.isType(next4, next2.type)
|
|
901
|
+
&& next && this.options.prefixableStrings.includes(next.value)
|
|
902
|
+
) {
|
|
903
|
+
return this.ruleValueUnquoted({ onlyToken }) as any
|
|
904
|
+
}
|
|
905
|
+
if (onlyToken) return undefined as any
|
|
906
|
+
return handle.token.value(...this.processToken()) as any
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
ruleValueUnquoted<TOnlyToken extends boolean = false>(
|
|
910
|
+
{
|
|
911
|
+
onlyToken = false as TOnlyToken,
|
|
912
|
+
}: {
|
|
913
|
+
onlyToken?: TOnlyToken
|
|
914
|
+
} = {},
|
|
915
|
+
): TOnlyToken extends true ? Token<$T.VALUE_UNQUOTED> : AnyToken<TOKEN_TYPE.VALUE> {
|
|
916
|
+
const t = this.consume($T.VALUE_UNQUOTED)
|
|
917
|
+
const res = onlyToken ? t : handle.token.value(...this.processToken(t))
|
|
918
|
+
return (res) as any
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
ruleParenL(): ValidToken<TOKEN_TYPE.PARENL> | undefined {
|
|
922
|
+
const next = this.peek(1)
|
|
923
|
+
const value = next?.type === $T.PAREN_L
|
|
924
|
+
? this.consume($T.PAREN_L)
|
|
925
|
+
: this.createErrorToken($T.PAREN_L)
|
|
926
|
+
const loc = extractPosition(value, this.state.shift)
|
|
927
|
+
return this.state.shift === 0 || loc.start > 0
|
|
928
|
+
? handle.delimiter.parenL(value.isError ? undefined : value.value, loc)
|
|
929
|
+
: undefined
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
ruleParenR(): ValidToken<TOKEN_TYPE.PARENR> | undefined {
|
|
933
|
+
const value = this.consume($T.PAREN_R)
|
|
934
|
+
return handle.delimiter.parenR(...this.processToken(value))
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
ruleBracketL(): ValidToken<TOKEN_TYPE.BRACKETL> | undefined {
|
|
938
|
+
const next = this.peek(1)
|
|
939
|
+
const value = next?.type === $T.BRACKET_L
|
|
940
|
+
? this.consume($T.BRACKET_L)
|
|
941
|
+
: this.createErrorToken($T.BRACKET_L)
|
|
942
|
+
const loc = extractPosition(value, this.state.shift)
|
|
943
|
+
return this.state.shift === 0 || loc.start > 0
|
|
944
|
+
? handle.delimiter.bracketL(value.isError ? undefined : value.value, loc)
|
|
945
|
+
: undefined
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
ruleBracketR(): ValidToken<TOKEN_TYPE.BRACKETR> | undefined {
|
|
949
|
+
const value = this.consume($T.BRACKET_R)
|
|
950
|
+
return handle.delimiter.bracketR(...this.processToken(value))
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
ruleNot(): ValidToken<TOKEN_TYPE.NOT> | undefined {
|
|
954
|
+
if (this.isType(this.peek(1), $C.OPERATOR_NOT)) {
|
|
955
|
+
const op = this.consume($C.OPERATOR_NOT)
|
|
956
|
+
return handle.operator.not(...this.processToken<true>(op))
|
|
957
|
+
}
|
|
958
|
+
return undefined
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
mixin(Parser, [
|
|
963
|
+
AutocompleteMixin,
|
|
964
|
+
AutoreplaceMixin,
|
|
965
|
+
Autosuggest,
|
|
966
|
+
EvaluateMixin,
|
|
967
|
+
ValidateMixin,
|
|
968
|
+
NormalizeMixin,
|
|
969
|
+
GetIndexMixin,
|
|
970
|
+
GetBestIndexesMixin,
|
|
971
|
+
])
|
|
972
|
+
|