@witchcraft/expressit 0.2.0 → 0.2.2
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 +5 -2
- package/dist/Lexer.d.ts +3 -5
- package/dist/Lexer.d.ts.map +1 -1
- package/dist/Lexer.js +21 -40
- package/dist/Parser.d.ts +1 -1
- package/dist/Parser.d.ts.map +1 -1
- package/dist/Parser.js +35 -64
- package/dist/ast/builders/condition.js +2 -4
- package/dist/ast/builders/delim.d.ts +1 -1
- package/dist/ast/builders/delim.d.ts.map +1 -1
- package/dist/ast/builders/delim.js +2 -4
- package/dist/ast/builders/expression.js +1 -2
- package/dist/ast/builders/pos.js +3 -6
- package/dist/ast/builders/variable.js +2 -4
- package/dist/ast/error.js +1 -2
- package/dist/ast/handlers.d.ts +2 -2
- package/dist/ast/handlers.d.ts.map +1 -1
- package/dist/ast/handlers.js +7 -11
- package/dist/defaults/defaultKeyParser.js +1 -2
- package/dist/examples/ParserWithSqlSupport.d.ts +62 -0
- package/dist/examples/ParserWithSqlSupport.d.ts.map +1 -0
- package/dist/examples/ParserWithSqlSupport.js +269 -0
- package/dist/examples/{shortcutContextParser.d.ts → ShortcutContextParser.d.ts} +5 -5
- package/dist/examples/ShortcutContextParser.d.ts.map +1 -0
- package/dist/examples/{shortcutContextParser.js → ShortcutContextParser.js} +6 -12
- package/dist/examples/index.d.ts +2 -1
- package/dist/examples/index.d.ts.map +1 -1
- package/dist/examples/index.js +3 -1
- package/dist/internal/ExpressitError.js +3 -5
- package/dist/internal/checkParserOpts.d.ts +1 -1
- package/dist/internal/checkParserOpts.d.ts.map +1 -1
- package/dist/internal/checkParserOpts.js +8 -16
- package/dist/internal/escapeVariableOrPrefix.js +4 -8
- package/dist/internal/parseParserOptions.d.ts +1 -1
- package/dist/internal/parseParserOptions.d.ts.map +1 -1
- package/dist/package.json.js +14 -147
- package/dist/types/parser.d.ts +6 -6
- package/dist/types/parser.d.ts.map +1 -1
- package/dist/utils/generateParentsMap.js +20 -40
- package/dist/utils/getCursorInfo.js +9 -17
- package/dist/utils/getOppositeDelimiter.js +3 -6
- package/dist/utils/prettyAst.js +1 -2
- package/package.json +25 -23
- package/src/Lexer.ts +5 -4
- package/src/Parser.ts +9 -3
- package/src/ast/builders/delim.ts +1 -1
- package/src/ast/handlers.ts +3 -3
- package/src/examples/ParserWithSqlSupport.ts +365 -0
- package/src/examples/{shortcutContextParser.ts → ShortcutContextParser.ts} +14 -14
- package/src/examples/index.ts +2 -1
- package/src/internal/ExpressitError.ts +2 -2
- package/src/internal/checkParserOpts.ts +3 -3
- package/src/internal/parseParserOptions.ts +2 -2
- package/src/types/parser.ts +6 -6
- package/src/utils/getCursorInfo.ts +1 -1
- package/dist/examples/shortcutContextParser.d.ts.map +0 -1
- package/dist/global.d.js +0 -1
- package/dist/package.js +0 -7
- package/src/global.d.ts +0 -4
- package/src/package.js +0 -11
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { unreachable } from "@alanscodelog/utils/unreachable.js"
|
|
2
|
+
|
|
3
|
+
import { Parser } from "../Parser.js"
|
|
4
|
+
import { AST_TYPE, type NormalizedCondition, type NormalizedExpression, type Position,TOKEN_TYPE } from "../types/ast.js"
|
|
5
|
+
|
|
6
|
+
export interface BasePropertyDefinition {
|
|
7
|
+
name: string
|
|
8
|
+
/** Supported types are: string, boolean, int, float, date. */
|
|
9
|
+
type: string
|
|
10
|
+
/** Return how to access the column in the WHERE condition of the SQL query, useful for json properties. It is up to you to properly quote the value if needed. */
|
|
11
|
+
transformToColumn?: (key: string, name: string) => string
|
|
12
|
+
/**
|
|
13
|
+
* A function that can be used to transform the value before it is inserted into the SQL query. Useful for types like arrays. It is up to you to properly escape values if you use the second parameter which contains the unescaped value.
|
|
14
|
+
*/
|
|
15
|
+
transformValue?: (value: any, unescapedValue: any) => any | any[]
|
|
16
|
+
isArray?: boolean
|
|
17
|
+
/** If undefined, it's assumed all operators are supported. This should only include the final operator (see {@link BaseOperatorDefinition.operator}), it doesn't need to include it's aliases. */
|
|
18
|
+
supportedOperators?: string[]
|
|
19
|
+
/** Further transform the value after the basic parsing has been done. Useful to, for example, parse other strings (e.g. now, today, tomorrow) as dates. */
|
|
20
|
+
postParse?: (value: any) => any
|
|
21
|
+
}
|
|
22
|
+
export interface BaseOperatorDefinition {
|
|
23
|
+
/** The final operator to use in the SQL query and while evaluating. */
|
|
24
|
+
operator: string
|
|
25
|
+
/** All aliases the user can use to specify the operator. They need not include the real final operator. */
|
|
26
|
+
operators: string[]
|
|
27
|
+
/** All negated aliases to the operator. If an operator is listed here, it will be used to "invert" a condition when normalizing. e.g. < can list >= as a negated operator. This greatly simplifies queries. */
|
|
28
|
+
negatedOperators?: string[]
|
|
29
|
+
/** How to compare the value when evualuating a condition. This is only used if using `evaluate`. */
|
|
30
|
+
valueComparer: (condition: any, contextValue: any) => boolean
|
|
31
|
+
}
|
|
32
|
+
export type BaseErrorTokenTypes =
|
|
33
|
+
| "invalidKey"
|
|
34
|
+
| "unknownProperty"
|
|
35
|
+
| "unknownOperator"
|
|
36
|
+
| "unknownOperatorForType"
|
|
37
|
+
| "invalidValueType"
|
|
38
|
+
/*
|
|
39
|
+
* Creates an example parser with a `toSql` method that can be used to convert an AST to a a safe SQL query.
|
|
40
|
+
*
|
|
41
|
+
* The parser assumes all column names (i.e. the propertyDefinitions) and operators (i.e. operatorDefinitions) are vetted and safe to use in the SQL queries. They are double quoted (unless transformToColumn is specificed) so you can use special characters if needed. You can use the `transformToColumn` option to specify a custom name (e.g. for json columns) that will be left as is.
|
|
42
|
+
*
|
|
43
|
+
* Values are escaped with the given `sqlEscapeValue`. If using drizzle, for simple tables and types, you can just do and it'll be safe. More complex types, such as arrays, or json properties are more comlicated and you'll probably want to specify the definition's transformValue option. See the examples tests for examples.
|
|
44
|
+
* ```ts
|
|
45
|
+
* const parser = new ParserWithSqlSupport(
|
|
46
|
+
* propertyDefinitions,
|
|
47
|
+
* {
|
|
48
|
+
* sqlEscapeValue: (value: string) => sql`${value}`
|
|
49
|
+
* }
|
|
50
|
+
* )
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
*/
|
|
54
|
+
export class ParserWithSqlSupport<TErrorToken extends
|
|
55
|
+
Position & {
|
|
56
|
+
type: BaseErrorTokenTypes
|
|
57
|
+
message?: string
|
|
58
|
+
} =
|
|
59
|
+
Position & {
|
|
60
|
+
type: BaseErrorTokenTypes
|
|
61
|
+
message?: string
|
|
62
|
+
},
|
|
63
|
+
TPropertyDefinition extends BasePropertyDefinition = BasePropertyDefinition,
|
|
64
|
+
TPropertyDefinitions extends Record<string, TPropertyDefinition> = Record<string, TPropertyDefinition>,
|
|
65
|
+
TOperatorDefinition extends BaseOperatorDefinition = BaseOperatorDefinition,
|
|
66
|
+
TOperatorDefinitions extends Record<string, TOperatorDefinition> = Record<string, TOperatorDefinition>,
|
|
67
|
+
TSqlEscapeValue extends (value: string) => any | ReturnType<TSqlEscapeValue> = (value: string) => any,
|
|
68
|
+
> extends Parser<TErrorToken> {
|
|
69
|
+
sqlEscapeValue: TSqlEscapeValue
|
|
70
|
+
|
|
71
|
+
operatorMap: Record<string, string>
|
|
72
|
+
|
|
73
|
+
constructor(
|
|
74
|
+
public propertyDefinitions: TPropertyDefinitions,
|
|
75
|
+
public operatorDefinitions: TOperatorDefinitions,
|
|
76
|
+
{ sqlEscapeValue }: { sqlEscapeValue: TSqlEscapeValue }
|
|
77
|
+
) {
|
|
78
|
+
const operators = []
|
|
79
|
+
const operatorMap: Record<string, string> = {}
|
|
80
|
+
for (const value of Object.values(operatorDefinitions)) {
|
|
81
|
+
for (const operator of value.operators) {
|
|
82
|
+
operatorMap[operator] = value.operator
|
|
83
|
+
operators.push(operator)
|
|
84
|
+
}
|
|
85
|
+
if (value.negatedOperators) {
|
|
86
|
+
for (const operator of value.negatedOperators) {
|
|
87
|
+
operatorMap[operator] = value.operator
|
|
88
|
+
operators.push(operator)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
super({
|
|
93
|
+
arrayValues: true,
|
|
94
|
+
regexValues: false,
|
|
95
|
+
keywords: {
|
|
96
|
+
and: [{ isSymbol: true, value: "&&" }],
|
|
97
|
+
or: [{ isSymbol: true, value: "||" }],
|
|
98
|
+
not: [{ isSymbol: true, value: "!" }],
|
|
99
|
+
},
|
|
100
|
+
customPropertyOperators: operators,
|
|
101
|
+
prefixableGroups: false,
|
|
102
|
+
valueComparer: (condition, contextValue, _context) => {
|
|
103
|
+
if (typeof condition.value !== typeof contextValue) {
|
|
104
|
+
throw new Error(`Expected type of property ${condition.property[0]} to be the same type as the context value ${contextValue} (${typeof contextValue}). If the ast has been validated this is likely because the type of the context value is incorrect.`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const prop = condition.property[0]
|
|
108
|
+
if (!prop) unreachable("Did you validate the ast before evaluating it?")
|
|
109
|
+
|
|
110
|
+
const propDefinition = propertyDefinitions[prop]
|
|
111
|
+
const operatorDefinition = condition.operator && operatorDefinitions[condition.operator]
|
|
112
|
+
if (!operatorDefinition) unreachable("Did you validate the ast before evaluating it?")
|
|
113
|
+
|
|
114
|
+
const isSupported = !propDefinition.supportedOperators?.includes(condition.operator!)
|
|
115
|
+
if (!isSupported) unreachable("Did you validate the ast before evaluating it?")
|
|
116
|
+
|
|
117
|
+
const res = operatorDefinition.valueComparer(condition, contextValue)
|
|
118
|
+
return res
|
|
119
|
+
},
|
|
120
|
+
valueValidator: (_contextValue, query): TErrorToken[] | void => {
|
|
121
|
+
const prop = query.propertyKeys[0]
|
|
122
|
+
let tokens: TErrorToken[] = []
|
|
123
|
+
const propDefinition = propertyDefinitions[prop]
|
|
124
|
+
if (!propDefinition) {
|
|
125
|
+
tokens = tokens.concat(query.property.map(token => ({
|
|
126
|
+
start: token.start,
|
|
127
|
+
end: token.end,
|
|
128
|
+
type: "unknownProperty",
|
|
129
|
+
})) as TErrorToken[])
|
|
130
|
+
return tokens
|
|
131
|
+
}
|
|
132
|
+
const op = query.operator
|
|
133
|
+
const opKey = op && operatorMap[op.value]
|
|
134
|
+
if (!op || !opKey) {
|
|
135
|
+
tokens.push({
|
|
136
|
+
start: (op ?? query.condition)?.start,
|
|
137
|
+
end: (op ?? query.condition)?.end,
|
|
138
|
+
type: "unknownOperator",
|
|
139
|
+
} as TErrorToken)
|
|
140
|
+
} else {
|
|
141
|
+
if (propDefinition.supportedOperators && !propDefinition.supportedOperators?.includes(opKey)) {
|
|
142
|
+
tokens.push({
|
|
143
|
+
start: query.condition.start,
|
|
144
|
+
end: query.condition.end,
|
|
145
|
+
type: "unknownOperatorForType",
|
|
146
|
+
} as TErrorToken)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const val = query.value
|
|
151
|
+
if (Array.isArray(val)) {
|
|
152
|
+
for (const v of val) {
|
|
153
|
+
if (v.type !== "VARIABLE") unreachable()
|
|
154
|
+
const res = convertAndValidateValue(query.isQuoted, v.value.value, prop, propertyDefinitions, { isArray: true })
|
|
155
|
+
if (res instanceof Error) {
|
|
156
|
+
if (v.type !== "VARIABLE") unreachable()
|
|
157
|
+
const token = v
|
|
158
|
+
tokens.push({
|
|
159
|
+
start: token.start,
|
|
160
|
+
end: token.end,
|
|
161
|
+
type: "invalidValueType",
|
|
162
|
+
message: res.message,
|
|
163
|
+
} as TErrorToken)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (tokens.length > 0) return tokens
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (val?.type !== "VARIABLE") unreachable()
|
|
171
|
+
const value = val.value.value
|
|
172
|
+
const res = convertAndValidateValue(query.isQuoted, value, prop, propertyDefinitions)
|
|
173
|
+
if (res instanceof Error) {
|
|
174
|
+
if (!query.value || query.value.type !== "VARIABLE") unreachable()
|
|
175
|
+
const token = query.value.value
|
|
176
|
+
tokens.push({
|
|
177
|
+
start: token.start,
|
|
178
|
+
end: token.end,
|
|
179
|
+
type: "invalidValueType",
|
|
180
|
+
message: res.message,
|
|
181
|
+
} as TErrorToken)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (tokens.length > 0) return tokens
|
|
185
|
+
},
|
|
186
|
+
conditionNormalizer(query) {
|
|
187
|
+
const prop = query.property?.[0]
|
|
188
|
+
if (!prop) unreachable("Did you validate the ast before normalizing it?")
|
|
189
|
+
const propDefinition = propertyDefinitions[prop]
|
|
190
|
+
|
|
191
|
+
let finalValue
|
|
192
|
+
if (Array.isArray(query.value)) {
|
|
193
|
+
const values = []
|
|
194
|
+
if (query.condition.value.type !== AST_TYPE.ARRAY) unreachable()
|
|
195
|
+
const raw = query.condition.value.values
|
|
196
|
+
for (let i = 0; i < query.value.length; i += 1) {
|
|
197
|
+
const token = raw[i]
|
|
198
|
+
const val = query.value[i]
|
|
199
|
+
const isQuoted = !!token.quote
|
|
200
|
+
const res = convertAndValidateValue(isQuoted, val, prop, propertyDefinitions, { isArray: true })
|
|
201
|
+
if (res instanceof Error) throw res
|
|
202
|
+
values.push(res)
|
|
203
|
+
}
|
|
204
|
+
finalValue = values
|
|
205
|
+
} else {
|
|
206
|
+
finalValue = convertAndValidateValue(query.isQuoted, query.value, prop, propertyDefinitions)
|
|
207
|
+
if (propDefinition.isArray) {
|
|
208
|
+
finalValue = [finalValue]
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let finalOperator: any = query.operator
|
|
213
|
+
if (finalValue instanceof Error) throw finalValue
|
|
214
|
+
const opKey = query.operator && operatorMap[query.operator]
|
|
215
|
+
if (!opKey) unreachable("Did you validate the ast before normalizing it?")
|
|
216
|
+
|
|
217
|
+
const operatorDefinition = opKey && operatorDefinitions[opKey]
|
|
218
|
+
if (!operatorDefinition) unreachable("Did you validate the ast before normalizing it?")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
const isNegatableOperator = operatorDefinition.negatedOperators?.includes(query.operator!)
|
|
222
|
+
|
|
223
|
+
finalOperator = operatorDefinition.operator
|
|
224
|
+
let isNegated = query.isNegated
|
|
225
|
+
if (isNegatableOperator) {
|
|
226
|
+
isNegated = !isNegated
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { value: finalValue, operator: finalOperator, negate: isNegated }
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
this.operatorMap = operatorMap
|
|
233
|
+
this.sqlEscapeValue = sqlEscapeValue
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
toSql<T>(
|
|
237
|
+
ast: NormalizedExpression<any, any> | NormalizedCondition<any, any>,
|
|
238
|
+
/**
|
|
239
|
+
* Optionally convert the raw strings to something else. If uding drizzle, you can pass sql.raw here. So later you can just sql.join the return of the function:
|
|
240
|
+
*
|
|
241
|
+
* ```ts
|
|
242
|
+
* sql.join([
|
|
243
|
+
* sql.raw(db.select().from(someTable).toSQL().sql),
|
|
244
|
+
* sql.raw(`where`),
|
|
245
|
+
* ...parser.toSql(ast, sql.raw)
|
|
246
|
+
* ], sql` `)
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
wrapStrings?: (value: string) => T,
|
|
250
|
+
): (ReturnType<TSqlEscapeValue> | typeof wrapStrings extends undefined ? string : T)[] {
|
|
251
|
+
this._checkEvaluationOptions()
|
|
252
|
+
const chunks = []
|
|
253
|
+
|
|
254
|
+
if (ast.type === AST_TYPE.NORMALIZED_CONDITION) {
|
|
255
|
+
const prop = ast.property?.[0]
|
|
256
|
+
const definition = this.propertyDefinitions[prop]
|
|
257
|
+
const value = ast.value
|
|
258
|
+
const col = definition.transformToColumn?.(prop, definition.name) ?? `"${prop}"`
|
|
259
|
+
const op = ast.operator
|
|
260
|
+
if (ast.negate) {
|
|
261
|
+
chunks.push(wrapStrings?.(`NOT(`) ?? `NOT(`)
|
|
262
|
+
}
|
|
263
|
+
chunks.push(wrapStrings?.(`${col} `) ?? `${col} `)
|
|
264
|
+
chunks.push(wrapStrings?.(`${op} `) ?? `${op} `)
|
|
265
|
+
const val = this.sqlEscapeValue(value)
|
|
266
|
+
if (definition.transformValue) {
|
|
267
|
+
const transformed = definition.transformValue(val, value)
|
|
268
|
+
if (Array.isArray(transformed)) {
|
|
269
|
+
chunks.push(...transformed)
|
|
270
|
+
} else {
|
|
271
|
+
chunks.push(transformed)
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
chunks.push(val)
|
|
275
|
+
}
|
|
276
|
+
if (ast.negate) {
|
|
277
|
+
chunks.push(wrapStrings?.(`)`) ?? `)`)
|
|
278
|
+
}
|
|
279
|
+
return chunks
|
|
280
|
+
}
|
|
281
|
+
if (ast.type === AST_TYPE.NORMALIZED_EXPRESSION) {
|
|
282
|
+
const left = this.toSql(ast.left, wrapStrings)
|
|
283
|
+
const right = this.toSql(ast.right, wrapStrings)
|
|
284
|
+
const op = ast.operator === TOKEN_TYPE.AND ? "AND" : "OR"
|
|
285
|
+
chunks.push(wrapStrings?.(`(`) ?? `(`)
|
|
286
|
+
chunks.push(...left)
|
|
287
|
+
chunks.push(wrapStrings?.(` ${op} `) ?? ` ${op} `)
|
|
288
|
+
chunks.push(...right)
|
|
289
|
+
chunks.push(wrapStrings?.(`)`) ?? `)`)
|
|
290
|
+
return chunks as any
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return unreachable()
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
export function createTypeError(prop: string, type: string, isArray: boolean): Error {
|
|
297
|
+
if (isArray) {
|
|
298
|
+
return new Error(`Property ${prop} must contain items of type ${type}.`)
|
|
299
|
+
}
|
|
300
|
+
return new Error(`Property ${prop} must be of type ${type}.`)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function convertAndValidateValue(
|
|
304
|
+
isQuoted: boolean,
|
|
305
|
+
value: any,
|
|
306
|
+
prop: string,
|
|
307
|
+
propertyDefinitions: Record<string, BasePropertyDefinition>,
|
|
308
|
+
{ isArray = false }: { isArray?: boolean } = {},
|
|
309
|
+
): any {
|
|
310
|
+
let finalValue: any = value
|
|
311
|
+
let isFloat = false
|
|
312
|
+
const propDefinition = propertyDefinitions[prop]
|
|
313
|
+
|
|
314
|
+
if (typeof value === "string" && !isQuoted) {
|
|
315
|
+
if (finalValue === "true") {
|
|
316
|
+
finalValue = true
|
|
317
|
+
} else if (finalValue === "false") {
|
|
318
|
+
finalValue = false
|
|
319
|
+
} else {
|
|
320
|
+
const asNum = parseInt(value, 10)
|
|
321
|
+
if (!isNaN(asNum)) {
|
|
322
|
+
finalValue = asNum
|
|
323
|
+
} else {
|
|
324
|
+
const asFloat = parseFloat(value)
|
|
325
|
+
if (!isNaN(asFloat)) {
|
|
326
|
+
finalValue = asFloat
|
|
327
|
+
isFloat = true
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const type = propDefinition.type
|
|
333
|
+
finalValue = propDefinition.postParse?.(finalValue) ?? finalValue
|
|
334
|
+
|
|
335
|
+
switch (type) {
|
|
336
|
+
case "integer":
|
|
337
|
+
case "float": {
|
|
338
|
+
if (typeof finalValue !== "number" || (type === "float" && !isFloat) || (type === "integer" && isFloat)) {
|
|
339
|
+
return createTypeError(prop, type, isArray)
|
|
340
|
+
}
|
|
341
|
+
break
|
|
342
|
+
}
|
|
343
|
+
case "string":
|
|
344
|
+
case "boolean": {
|
|
345
|
+
if (typeof finalValue !== propDefinition.type) {
|
|
346
|
+
return createTypeError(prop, type, isArray)
|
|
347
|
+
}
|
|
348
|
+
break
|
|
349
|
+
}
|
|
350
|
+
case "date": {
|
|
351
|
+
if (finalValue instanceof Date) {
|
|
352
|
+
break
|
|
353
|
+
}
|
|
354
|
+
const maybeDate = new Date(finalValue)
|
|
355
|
+
if (isNaN(maybeDate.getTime())) {
|
|
356
|
+
return createTypeError(prop, "date", isArray)
|
|
357
|
+
} else {
|
|
358
|
+
finalValue = maybeDate
|
|
359
|
+
}
|
|
360
|
+
break
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return finalValue
|
|
365
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { Parser } from "../Parser.js"
|
|
2
|
+
import type { Position } from "../types/ast.js"
|
|
3
|
+
import type { ValueQuery } from "../types/parser.js"
|
|
4
|
+
|
|
1
5
|
/* TODO TOUPDATE */
|
|
2
6
|
/**
|
|
3
7
|
* A pre-configured parser for parsing shortcut contexts (similar to VSCode's [when clause contexts](https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts)).
|
|
@@ -9,15 +13,11 @@
|
|
|
9
13
|
* The validate function will return a list of positions with a list of errors which includes handling invalid or duplicate regex flags.
|
|
10
14
|
*/
|
|
11
15
|
|
|
12
|
-
import { Parser } from "../Parser.js"
|
|
13
|
-
import type { Position } from "../types/ast.js"
|
|
14
|
-
import type { ValueQuery } from "../types/parser.js"
|
|
15
|
-
|
|
16
16
|
|
|
17
|
-
export class ShortcutContextParser<
|
|
17
|
+
export class ShortcutContextParser<TErrorTokens extends
|
|
18
18
|
Position & { type: ("invalidKey" | "unregexableKey" | "invalidRegexFlag" | "duplicateRegexFlag") } =
|
|
19
19
|
Position & { type: ("invalidKey" | "unregexableKey" | "invalidRegexFlag" | "duplicateRegexFlag") },
|
|
20
|
-
> extends Parser<
|
|
20
|
+
> extends Parser<TErrorTokens> {
|
|
21
21
|
validKeys: string[] = []
|
|
22
22
|
|
|
23
23
|
regexablekeys: string[] = []
|
|
@@ -49,21 +49,21 @@ export class ShortcutContextParser<T extends
|
|
|
49
49
|
}
|
|
50
50
|
return contextValue === condition.value
|
|
51
51
|
},
|
|
52
|
-
valueValidator: (_contextValue, query):
|
|
53
|
-
let tokens:
|
|
52
|
+
valueValidator: (_contextValue, query): TErrorTokens[] | void => {
|
|
53
|
+
let tokens: TErrorTokens[] = []
|
|
54
54
|
if (!this.validKeys.includes(query.propertyName!)) {
|
|
55
55
|
tokens = tokens.concat(query.property.map(token => ({
|
|
56
56
|
start: token.start,
|
|
57
57
|
end: token.end,
|
|
58
58
|
type: "invalidKey",
|
|
59
|
-
})) as
|
|
59
|
+
})) as TErrorTokens[])
|
|
60
60
|
}
|
|
61
61
|
if (query.isRegex && !this.regexablekeys.includes(query.propertyName!)) {
|
|
62
62
|
tokens = tokens.concat(query.property.map(token => ({
|
|
63
63
|
start: token.start,
|
|
64
64
|
end: token.end,
|
|
65
65
|
type: "unregexableKey",
|
|
66
|
-
})) as
|
|
66
|
+
})) as TErrorTokens[])
|
|
67
67
|
}
|
|
68
68
|
if (query.regexFlags) {
|
|
69
69
|
const chars = query.regexFlags.value.split("")
|
|
@@ -75,14 +75,14 @@ export class ShortcutContextParser<T extends
|
|
|
75
75
|
start: start + i,
|
|
76
76
|
end: start + i + 1,
|
|
77
77
|
type: "duplicateRegexFlag",
|
|
78
|
-
} as
|
|
78
|
+
} as TErrorTokens)
|
|
79
79
|
}
|
|
80
80
|
if (!validRegexFlags.includes(char)) {
|
|
81
81
|
tokens.push({
|
|
82
82
|
start: start + i,
|
|
83
83
|
end: start + i + 1,
|
|
84
84
|
type: "invalidRegexFlag",
|
|
85
|
-
} as
|
|
85
|
+
} as TErrorTokens)
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -93,7 +93,7 @@ export class ShortcutContextParser<T extends
|
|
|
93
93
|
let finalOperator: any = operator
|
|
94
94
|
// another way to allow special unquoted value types is something like this:
|
|
95
95
|
if (typeof value === "string" && !isQuoted) {
|
|
96
|
-
const asNum = parseInt(value,
|
|
96
|
+
const asNum = parseInt(value, 10)
|
|
97
97
|
if (!isNaN(asNum)) finalValue = asNum
|
|
98
98
|
if (["true", "false"].includes(value)) {
|
|
99
99
|
finalValue = value === "true"
|
|
@@ -132,7 +132,7 @@ export class ShortcutContextParser<T extends
|
|
|
132
132
|
for (const key of Object.keys(context)) {
|
|
133
133
|
if (typeof context[key] === "boolean") {
|
|
134
134
|
this.validKeys.push(prev ? `${prev}.${key}` : key)
|
|
135
|
-
if (context[key]
|
|
135
|
+
if (context[key]) {
|
|
136
136
|
this.regexablekeys.push(prev ? `${prev}.${key}` : key)
|
|
137
137
|
}
|
|
138
138
|
} else {
|
package/src/examples/index.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { indent } from "@alanscodelog/utils/indent.js"
|
|
|
3
3
|
import { pretty } from "@alanscodelog/utils/pretty.js"
|
|
4
4
|
import type { Keys } from "@alanscodelog/utils/types"
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import packageJson from "../../package.json" assert { type: "json" }
|
|
7
|
+
const { version, repository } = packageJson
|
|
8
8
|
import type { ERROR_CODES, ErrorInfo } from "../types/errors.js"
|
|
9
9
|
|
|
10
10
|
/** @internal */
|
|
@@ -9,7 +9,7 @@ import { ERROR_CODES } from "../types/errors.js"
|
|
|
9
9
|
import type { FullParserOptions, ParserOptions } from "../types/parser.js"
|
|
10
10
|
|
|
11
11
|
/** @internal */
|
|
12
|
-
export function checkParserOpts<T
|
|
12
|
+
export function checkParserOpts<T>(opts: FullParserOptions<T>, evaluatorChecks: boolean = false, validatorChecks: boolean = false): void {
|
|
13
13
|
if (!evaluatorChecks) {
|
|
14
14
|
const keywordsList = [...opts.keywords.and, ...opts.keywords.or, ...opts.keywords.not].map(keyword => keyword.value)
|
|
15
15
|
const symNots = opts.keywords.not.filter(op => op.isSymbol).map(op => op.value)
|
|
@@ -48,8 +48,8 @@ export function checkParserOpts<T extends {}>(opts: FullParserOptions<T>, evalua
|
|
|
48
48
|
`prefixableStrings cannot contain blank entries`,
|
|
49
49
|
)
|
|
50
50
|
}
|
|
51
|
-
for (const key of ["and", "or", "not"]) {
|
|
52
|
-
const invalid = opts.keywords[key
|
|
51
|
+
for (const key of ["and", "or", "not"] as const) {
|
|
52
|
+
const invalid = opts.keywords[key]
|
|
53
53
|
?.find(_ => isBlank(_.value))
|
|
54
54
|
?.value
|
|
55
55
|
if (invalid !== undefined) {
|
|
@@ -5,10 +5,10 @@ import { defaultValueComparer } from "../defaults/defaultValueComparer.js"
|
|
|
5
5
|
import type { FullParserOptions, ParserOptions } from "../types/parser.js"
|
|
6
6
|
|
|
7
7
|
/** @internal */
|
|
8
|
-
export function parseParserOptions<T
|
|
8
|
+
export function parseParserOptions<T>(
|
|
9
9
|
options: ParserOptions<T>,
|
|
10
10
|
): FullParserOptions<T> {
|
|
11
|
-
const opts: ParserOptions = {
|
|
11
|
+
const opts: ParserOptions<T> = {
|
|
12
12
|
prefixApplier: defaultPrefixApplier,
|
|
13
13
|
keyParser: defaultKeyParser,
|
|
14
14
|
valueComparer: defaultValueComparer,
|
package/src/types/parser.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { ArrayNode, ConditionNode, NormalizedCondition, Position, TOKEN_TYP
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
// #partially-synced
|
|
7
|
-
export type FullParserOptions<T
|
|
7
|
+
export type FullParserOptions<T = any> = MakeRequired<
|
|
8
8
|
ParserOptions<T>,
|
|
9
9
|
// makes required all except:
|
|
10
10
|
Exclude<keyof ParserOptions<T>,
|
|
@@ -18,7 +18,7 @@ export type FullParserOptions<T extends {} = {}> = MakeRequired<
|
|
|
18
18
|
// overrides
|
|
19
19
|
keywords: DeepRequired<KeywordOptions>
|
|
20
20
|
}
|
|
21
|
-
export type ParserOptions<T
|
|
21
|
+
export type ParserOptions<T = any> = {
|
|
22
22
|
/**
|
|
23
23
|
* Allows any conditions (i.e. a variable or negated variable) to precede groups and append themselves to all variables inside them. Regular use of groups for changing precedence (e.g. `(a || b) && c` ) or negating expressions `!(a || b)` is still supported even if `prefixableGroups` is false.
|
|
24
24
|
*
|
|
@@ -116,7 +116,7 @@ export type ParserOptions<T extends {} = {}> = {
|
|
|
116
116
|
/**
|
|
117
117
|
* Enables regex strings as values. The value is treated as if it was quoted by forward slashes. Any repetition of lowercase characters (even if there are multiple) attached to the end are assumed to be flags and added as a single token to the value's `quotes.mode` property.
|
|
118
118
|
*
|
|
119
|
-
* Can be passed a custom function to determine when to use the regex value or not (it is converted to a regular value). The function is passed the property, the operator, and whether it's an expanded operator. If
|
|
119
|
+
* Can be passed a custom function to determine when to use the regex value or not (it is converted to a regular value). The function is passed the property, the operator, and whether it's an expanded operator. If there is an error token for the property or operator, an empty string is passed.
|
|
120
120
|
*
|
|
121
121
|
* ```ts
|
|
122
122
|
* // allow anything (`prop=/val/`, `prop:op:/val`, `prop=(/val/)`, `prop:op(/val/)`) but the value alone (`/regex/`)
|
|
@@ -272,7 +272,7 @@ export type ParserOptions<T extends {} = {}> = {
|
|
|
272
272
|
* ```ts
|
|
273
273
|
* type Operators = "contains"
|
|
274
274
|
* function valueComparer(condition: Omit<Condition, "negate">, contextValue: any, context: any): boolean {
|
|
275
|
-
* switch (operator as Operators) {
|
|
275
|
+
* switch (condition.operator as Operators) {
|
|
276
276
|
* case "contains": return (contextValue as string[]).includes(condition.value as string)
|
|
277
277
|
* // ...
|
|
278
278
|
* }
|
|
@@ -307,13 +307,13 @@ export type ParserOptions<T extends {} = {}> = {
|
|
|
307
307
|
* if (prefix) {
|
|
308
308
|
* const val = value as string // it's always a string if prefixed
|
|
309
309
|
* switch (prefix as RawPrefixes) {
|
|
310
|
-
* case "num": finalValue = parseInt(val,
|
|
310
|
+
* case "num": finalValue = parseInt(val, 10); break
|
|
311
311
|
* // ...
|
|
312
312
|
* }
|
|
313
313
|
* }
|
|
314
314
|
* // another way to allow special unquoted value types is something like this:
|
|
315
315
|
* if (typeof value === "string" && !isQuoted) {
|
|
316
|
-
* const asNum = parseInt(value,
|
|
316
|
+
* const asNum = parseInt(value, 10)
|
|
317
317
|
* if (!isNaN(asNum)) finalValue = asNum
|
|
318
318
|
* if (["true","false"].includes(value)) {
|
|
319
319
|
* finalValue = value === "true" ? true : false
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"shortcutContextParser.d.ts","sourceRoot":"","sources":["../../src/examples/shortcutContextParser.ts"],"names":[],"mappings":"AACA;;;;;;;;GAQG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAI/C,qBAAa,qBAAqB,CAAC,CAAC,SACnC,QAAQ,GAAG;IAAE,IAAI,EAAE,CAAC,YAAY,GAAG,gBAAgB,GAAG,kBAAkB,GAAG,oBAAoB,CAAC,CAAA;CAAE,GAClG,QAAQ,GAAG;IAAE,IAAI,EAAE,CAAC,YAAY,GAAG,gBAAgB,GAAG,kBAAkB,GAAG,oBAAoB,CAAC,CAAA;CAAE,CACjG,SAAQ,MAAM,CAAC,CAAC,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAK;IAExB,aAAa,EAAE,MAAM,EAAE,CAAK;gBAG3B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACjC,eAAe,GAAE,MAAM,EAAoB;IAmG5C,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IAK9C,OAAO,CAAC,uBAAuB;CAa/B"}
|
package/dist/global.d.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
package/dist/package.js
DELETED
package/src/global.d.ts
DELETED
package/src/package.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import pkg from "../package.json"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* This library imports it's own package.json for inserting the version number into errors. Doing this is not a problem normally (because babel is used to transpile the typescript instead of tsc), but when tsc is used to output the types, the output is nested (dist-types/src/*) because of the json import being outside the src dir. There is a way around this but it's complicated. The easier way is to just cheat and do the importing in this js file. Typescript consumes it none the wiser and babel transpiles it to cjs/es accordingly.
|
|
5
|
-
*
|
|
6
|
-
* The values could be inlined, but there are no battle tested actively maintained babel plugins for this, and problems would be hard to debug (given when developing, the package.json never contains the real version because the library is semantically released.)
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export const version = pkg.version
|
|
11
|
-
export const repository = pkg.repository
|