wizard-ql 2.1.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,87 +1,5 @@
1
1
  // src/spec.ts
2
- var BRACKETS = [
3
- ["[", "]"],
4
- ["{", "}"]
5
- ];
6
- var PARENS = [
7
- ["(", ")"]
8
- ];
9
- var NEGATORS = ["!"];
10
- var ARRAY_DELIMITERS = [","];
11
- var OPERATION_ALIAS_DICTIONARY = {
12
- AND: "AND",
13
- "&&": "AND",
14
- "&": "AND",
15
- "^": "AND",
16
- OR: "OR",
17
- "||": "OR",
18
- "|": "OR",
19
- V: "OR",
20
- GEQ: "GEQ",
21
- ">=": "GEQ",
22
- "=>": "GEQ",
23
- LEQ: "LEQ",
24
- "<=": "LEQ",
25
- "=<": "LEQ",
26
- NOTEQUALS: "NOTEQUAL",
27
- NOTEQUAL: "NOTEQUAL",
28
- NEQ: "NOTEQUAL",
29
- ISNT: "NOTEQUAL",
30
- "!==": "NOTEQUAL",
31
- "!=": "NOTEQUAL",
32
- EQUALS: "EQUAL",
33
- EQUAL: "EQUAL",
34
- EQ: "EQUAL",
35
- IS: "EQUAL",
36
- "==": "EQUAL",
37
- "=": "EQUAL",
38
- LESS: "LESS",
39
- "<": "LESS",
40
- GREATER: "GREATER",
41
- ">": "GREATER",
42
- MORE: "GREATER",
43
- NOTIN: "NOTIN",
44
- "!:": "NOTIN",
45
- IN: "IN",
46
- ":": "IN",
47
- NOTMATCHES: "NOTMATCH",
48
- NOTMATCH: "NOTMATCH",
49
- "!~": "NOTMATCH",
50
- MATCHES: "MATCH",
51
- MATCH: "MATCH",
52
- "~": "MATCH"
53
- };
54
- var ALIASES = Object.keys(OPERATION_ALIAS_DICTIONARY);
55
- var OPERATION_PURPOSE_DICTIONARY = {
56
- AND: "junction",
57
- OR: "junction",
58
- EQUAL: "comparison",
59
- NOTEQUAL: "comparison",
60
- LESS: "comparison",
61
- GREATER: "comparison",
62
- GEQ: "comparison",
63
- LEQ: "comparison",
64
- IN: "comparison",
65
- NOTIN: "comparison",
66
- MATCH: "comparison",
67
- NOTMATCH: "comparison"
68
- };
69
- var COMPARISON_TYPE_DICTIONARY = {
70
- EQUAL: "primitive",
71
- NOTEQUAL: "primitive",
72
- GEQ: "numeric",
73
- GREATER: "numeric",
74
- LEQ: "numeric",
75
- LESS: "numeric",
76
- IN: "array",
77
- NOTIN: "array",
78
- MATCH: "string",
79
- NOTMATCH: "string"
80
- };
81
2
  var TYPE_PRIORITY = ["boolean", "date", "number", "string"];
82
- // src/regex.ts
83
- var ESCAPE_REGEX = "(?<=(?<!\\\\)(?:\\\\\\\\)*)";
84
-
85
3
  // src/errors.ts
86
4
  function constructHeader(startToken, startIndex, endToken, endIndex) {
87
5
  let header = "Token #";
@@ -146,622 +64,963 @@ class ConstraintError extends Error {
146
64
  }
147
65
  }
148
66
 
149
- // src/parse.ts
150
- var TOKEN_REGEX = new RegExp("(?<=(?<!\\\\)(?:\\\\\\\\)*)(?<quote>\\x27|\\x22|\\x60)(?<quotecontent>.*?)(?<=(?<!\\\\)(?:\\\\\\\\)*)\\k<quote>|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x41ND(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x26\\x26|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x26|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\^|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4fR(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\|\\||(?<=(?<!\\\\)(?:\\\\\\\\)*)\\||(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x56(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x47EQ(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x3e\\x3d|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x3d\\x3e|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4cEQ(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x3c\\x3d|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x3d\\x3c|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4eOTEQUALS(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4eOTEQUAL(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4eEQ(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x49SNT(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x21\\x3d\\x3d|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x21\\x3d|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x45QUALS(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x45QUAL(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x45Q(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x49S(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x3d\\x3d|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x3d|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4cESS(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x3c|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x47REATER(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x3e|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4dORE(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4eOTIN(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x21\\x3a|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x49N(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x3a|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4eOTMATCHES(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4eOTMATCH(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x21\\x7e|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4dATCHES(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4dATCH(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x7e|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\(|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\[|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\]|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\{|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\}|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x21|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x2c", "g");
151
- var QUOTE_REGEX = new RegExp("(?<=(?<!\\\\)(?:\\\\\\\\)*)(?<quote>\\x27|\\x22|\\x60)(?<quotecontent>.*?)(?<=(?<!\\\\)(?:\\\\\\\\)*)\\k<quote>");
152
- var QUOTE_EDGE_REGEX = new RegExp(`^${"(?<=(?<!\\\\)(?:\\\\\\\\)*)(?<quote>\\x27|\\x22|\\x60)(?<quotecontent>.*?)(?<=(?<!\\\\)(?:\\\\\\\\)*)\\k<quote>"}$`);
153
- function pushSanitized(array, item, index) {
154
- let trimmed = item.trimEnd();
155
- const pretrimLength = trimmed.length;
156
- trimmed = trimmed.trimStart();
157
- const lengthDiff = trimmed.length - pretrimLength;
158
- if (trimmed) {
159
- const token = { content: trimmed, index: index - lengthDiff };
160
- array.push(token);
161
- return token;
162
- }
67
+ // src/regex.ts
68
+ var ESCAPE_REGEX = "(?<=(?<!\\\\)(?:\\\\\\\\)*)";
69
+ var NO_ESCAPE_ERROR = new Error("Your JS runtime version does not include RegExp.escape(). Please update or use a polyfill");
70
+ function createQuoteRegexString(quotes) {
71
+ if (!RegExp.escape)
72
+ throw NO_ESCAPE_ERROR;
73
+ return `${ESCAPE_REGEX}(?<quote>${quotes.map((q) => RegExp.escape(q)).join("|")})(?<quotecontent>.*?)${ESCAPE_REGEX}\\k<quote>`;
163
74
  }
164
- function _tokenize(expression, pattern) {
165
- const tokens = [];
166
- const matches = expression.toUpperCase().matchAll(pattern);
167
- let lastMatchEnd = null;
168
- for (const match of matches) {
169
- pushSanitized(tokens, expression.slice(lastMatchEnd ?? 0, match.index), lastMatchEnd === null ? 0 : lastMatchEnd);
170
- if (BRACKETS.some(([o]) => match[0] === o)) {
171
- const startToken = {
172
- content: match[0],
173
- index: match.index
174
- };
175
- tokens.push(startToken);
176
- let subopenings = 0;
177
- let endToken;
178
- for (const submatch of matches) {
179
- if (match[0] === submatch[0] || match[0] === submatch[0])
180
- ++subopenings;
181
- if (BRACKETS.some(([o, c]) => match[0] === o && submatch[0] === c)) {
182
- if (subopenings)
183
- --subopenings;
184
- else {
185
- endToken = {
186
- content: submatch[0],
187
- index: submatch.index
188
- };
189
- break;
190
- }
191
- }
192
- }
193
- const subtokens = endToken ? _tokenize(expression.slice(startToken.index + startToken.content.length, endToken.index), new RegExp(`${"(?<=(?<!\\\\)(?:\\\\\\\\)*)(?<quote>\\x27|\\x22|\\x60)(?<quotecontent>.*?)(?<=(?<!\\\\)(?:\\\\\\\\)*)\\k<quote>"}|${ESCAPE_REGEX},`, "g")) : _tokenize(expression.slice(startToken.index + startToken.content.length), TOKEN_REGEX);
194
- for (const subtoken of subtokens)
195
- subtoken.index += match.index + 1;
196
- tokens.push(...subtokens);
197
- if (endToken) {
198
- tokens.push(endToken);
199
- lastMatchEnd = endToken.index + endToken.content.length;
200
- } else {
201
- lastMatchEnd = expression.length;
202
- break;
203
- }
204
- continue;
205
- } else {
206
- pushSanitized(tokens, match.groups?.quotecontent !== undefined ? expression.slice(match.index, match.index + match[0].length) : match[0], match.index);
207
- }
208
- lastMatchEnd = match.index + match[0].length;
75
+ function createArrayDelimitRegexString(quotes, delimiters) {
76
+ if (!RegExp.escape)
77
+ throw NO_ESCAPE_ERROR;
78
+ return `${createQuoteRegexString(quotes)}|${ESCAPE_REGEX}(?:${delimiters.map((d) => RegExp.escape(d)).join("|")})`;
79
+ }
80
+ function isAlpha(text) {
81
+ for (let c = 0;c < text.length; ++c) {
82
+ const char = text.charCodeAt(c);
83
+ const isUpper = char >= 65 && char <= 90;
84
+ const isLower = char >= 97 && char <= 122;
85
+ if (!isUpper && !isLower)
86
+ return false;
209
87
  }
210
- pushSanitized(tokens, expression.slice(lastMatchEnd ?? 0), lastMatchEnd ?? 0);
211
- return tokens;
88
+ return true;
212
89
  }
213
- function tokenize(expression) {
214
- return _tokenize(expression, TOKEN_REGEX);
90
+ function createTokenRegexString(keywords, quotes) {
91
+ if (!RegExp.escape)
92
+ throw NO_ESCAPE_ERROR;
93
+ const QUOTE_REGEX_STR = createQuoteRegexString(quotes);
94
+ const tokens = keywords.sort((a, b) => b.length - a.length).map((alias) => {
95
+ const escaped = RegExp.escape(alias);
96
+ return isAlpha(alias) ? `(?<=${ESCAPE_REGEX}\\s|^)${escaped}(?=${ESCAPE_REGEX}\\s|$)` : `${ESCAPE_REGEX}${escaped}`;
97
+ });
98
+ const full = `${QUOTE_REGEX_STR}|${tokens.join("|")}`;
99
+ return full;
215
100
  }
216
- function processToken(token) {
217
- const unquoted = token.match(QUOTE_EDGE_REGEX)?.groups?.quotecontent;
218
- const escaped = unquoted ?? token;
219
- const unescaped = escaped.replaceAll(new RegExp(`(?<!${ESCAPE_REGEX}\\\\)\\\\`, "g"), "");
220
- return {
221
- raw: token,
222
- unquoted,
223
- escaped,
224
- unescaped
101
+
102
+ // src/parser.ts
103
+ class WizardParser {
104
+ static DEFAULT_OPERATORS = {
105
+ AND: {
106
+ negationName: "OR",
107
+ type: "productjunction",
108
+ aliases: ["&&", "&", "^", "∧"],
109
+ negationAliases: ["||", "|", "V", "∨"]
110
+ },
111
+ EQUAL: {
112
+ negationName: "NOTEQUAL",
113
+ type: "primitive",
114
+ aliases: ["EQUALS", "EQ", "IS", "=", "=="],
115
+ negationAliases: ["NOTEQUALS", "NEQ", "ISNT", "!=", "!==", "≠"]
116
+ },
117
+ LESS: {
118
+ negationName: "GEQ",
119
+ type: "numeric",
120
+ aliases: ["<"],
121
+ negationAliases: [">=", "=>", "≥"],
122
+ exclusionary: true
123
+ },
124
+ GREATER: {
125
+ negationName: "LEQ",
126
+ type: "numeric",
127
+ aliases: [">", "MORE", "MORETHAN"],
128
+ negationAliases: ["<=", "=<", "≤"],
129
+ exclusionary: true
130
+ },
131
+ IN: {
132
+ negationName: "NOTIN",
133
+ type: "array",
134
+ aliases: [":", "∈"],
135
+ negationAliases: ["!:", "∉"]
136
+ },
137
+ MATCH: {
138
+ negationName: "NOTMATCH",
139
+ type: "string",
140
+ aliases: ["MATCHES", "~", "≅"],
141
+ negationAliases: ["NOTMATCHES", "!~", "≇"]
142
+ }
225
143
  };
226
- }
227
- function getClosingIndex(tokens, start, opening, closing) {
228
- let openingCount = 1;
229
- for (let index = start + 1;index < tokens.length; ++index) {
230
- switch (tokens[index].content) {
231
- case opening:
232
- ++openingCount;
233
- break;
234
- case closing:
235
- --openingCount;
236
- break;
144
+ static DEFAULT_DIALECTS = {
145
+ programmatic: {
146
+ AND: "&",
147
+ OR: "|",
148
+ EQUAL: "=",
149
+ NOTEQUAL: "!=",
150
+ GEQ: ">=",
151
+ GREATER: ">",
152
+ LEQ: "<=",
153
+ LESS: "<",
154
+ IN: ":",
155
+ NOTIN: "!:",
156
+ MATCH: "~",
157
+ NOTMATCH: "!~"
158
+ },
159
+ linguistic: {
160
+ AND: "AND",
161
+ OR: "OR",
162
+ EQUAL: "EQUALS",
163
+ NOTEQUAL: "NOTEQUALS",
164
+ GEQ: "GEQ",
165
+ GREATER: "GREATER",
166
+ LEQ: "LEQ",
167
+ LESS: "LESS",
168
+ IN: "IN",
169
+ NOTIN: "NOTIN",
170
+ MATCH: "MATCHES",
171
+ NOTMATCH: "NOTMATCHES"
172
+ },
173
+ formal: {
174
+ AND: "∧",
175
+ OR: "∨",
176
+ EQUAL: "=",
177
+ NOTEQUAL: "≠",
178
+ GEQ: "≥",
179
+ GREATER: ">",
180
+ LEQ: "≤",
181
+ LESS: "<",
182
+ IN: "∈",
183
+ NOTIN: "∉",
184
+ MATCH: "≅",
185
+ NOTMATCH: "≇"
237
186
  }
238
- if (openingCount === 0)
239
- return index;
240
- }
241
- return -1;
242
- }
243
- function complementExpression(expression) {
244
- switch (expression.type) {
245
- case "group":
246
- switch (expression.operation) {
247
- case "AND":
248
- expression.operation = "OR";
249
- break;
250
- case "OR":
251
- expression.operation = "AND";
252
- break;
187
+ };
188
+ static QUOTES = ["'", '"', "`"];
189
+ static NEGATORS = ["!"];
190
+ static ARRAY_DELIMITERS = [","];
191
+ static ARRAY_BRACKETS = [
192
+ ["[", "]"],
193
+ ["{", "}"]
194
+ ];
195
+ static GROUP_BRACKETS = [
196
+ ["(", ")"]
197
+ ];
198
+ OPERATOR_DICTIONARY;
199
+ CONFIG;
200
+ TOKEN_REGEX;
201
+ QUOTE_REGEX;
202
+ QUOTE_EDGE_REGEX;
203
+ ARRAY_DELIMITER_REGEX;
204
+ FIELD_LIST;
205
+ constructor(config = {}) {
206
+ this.CONFIG = config;
207
+ if (!config.operators) {
208
+ config.operators = WizardParser.DEFAULT_OPERATORS;
209
+ if (config.implicitCondition === undefined) {
210
+ config.implicitCondition = {
211
+ operator: "EQUAL",
212
+ value: "true",
213
+ asType: "boolean"
214
+ };
253
215
  }
254
- expression.constituents.forEach(complementExpression);
255
- break;
256
- case "condition":
257
- switch (expression.operation) {
258
- case "EQUAL":
259
- expression.operation = "NOTEQUAL";
260
- break;
261
- case "NOTEQUAL":
262
- expression.operation = "EQUAL";
263
- break;
264
- case "GEQ":
265
- expression.operation = "LESS";
266
- break;
267
- case "LESS":
268
- expression.operation = "GEQ";
269
- break;
270
- case "LEQ":
271
- expression.operation = "GREATER";
272
- break;
273
- case "GREATER":
274
- expression.operation = "LEQ";
275
- break;
276
- case "IN":
277
- expression.operation = "NOTIN";
278
- break;
279
- case "NOTIN":
280
- expression.operation = "IN";
281
- break;
282
- case "MATCH":
283
- expression.operation = "NOTMATCH";
284
- break;
285
- case "NOTMATCH":
286
- expression.operation = "MATCH";
287
- break;
216
+ if (!config.dialects)
217
+ config.dialects = WizardParser.DEFAULT_DIALECTS;
218
+ }
219
+ this.FIELD_LIST = new Map;
220
+ if (config.caseInsensitive) {
221
+ if (config.types) {
222
+ for (const key in config.types)
223
+ this.FIELD_LIST.set(key.toLowerCase(), key);
288
224
  }
289
- break;
290
- }
291
- }
292
- function coerceType(processedToken, fieldTypes, operatorTypes, dateInterpreter) {
293
- const { raw, unescaped } = processedToken;
294
- let operatorHits = 0;
295
- let fieldHits = 0;
296
- for (const type of TYPE_PRIORITY) {
297
- const operatorHit = operatorTypes.includes(type);
298
- const fieldHit = fieldTypes.includes(type);
299
- if (operatorHit)
300
- ++operatorHits;
301
- if (fieldHit)
302
- ++fieldHits;
303
- if (!operatorHit || !fieldHit)
304
- continue;
305
- switch (type) {
306
- case "boolean":
307
- if (raw === "true")
308
- return true;
309
- else if (raw === "false")
310
- return false;
311
- break;
312
- case "number": {
313
- const num = Number(raw);
314
- if (isNaN(num))
315
- break;
316
- return num;
225
+ if (config.restricted) {
226
+ for (const key in config.restricted)
227
+ this.FIELD_LIST.set(key.toLowerCase(), key);
317
228
  }
318
- case "string":
319
- return unescaped;
320
- case "date": {
321
- const num = Number(raw);
322
- const date = dateInterpreter(isNaN(num) ? unescaped : num);
323
- if (isNaN(+date))
324
- break;
325
- return date;
229
+ }
230
+ this.OPERATOR_DICTIONARY = {};
231
+ for (const operationName in config.operators) {
232
+ const operation = config.operators[operationName];
233
+ const opDef = {
234
+ name: operationName,
235
+ type: operation.type,
236
+ negation: operation.negationName,
237
+ exclusionary: operation.exclusionary ?? false
238
+ };
239
+ const negOpDef = {
240
+ name: operation.negationName,
241
+ type: operation.type === "sumjunction" ? "productjunction" : operation.type === "productjunction" ? "sumjunction" : operation.type,
242
+ negation: operationName,
243
+ exclusionary: !operation.exclusionary
244
+ };
245
+ if (operationName in this.OPERATOR_DICTIONARY)
246
+ throw Error(`Two operator definitions have been supplied with the same name "${operationName}"`);
247
+ this.OPERATOR_DICTIONARY[opDef.name] = opDef;
248
+ if (operation.negationName in this.OPERATOR_DICTIONARY)
249
+ throw Error(`An operator definition shares a negation name with another operator's definition "${operation.negationName}"`);
250
+ this.OPERATOR_DICTIONARY[negOpDef.name] = negOpDef;
251
+ if (operation.aliases) {
252
+ for (const alias of operation.aliases) {
253
+ if (alias in this.OPERATOR_DICTIONARY)
254
+ throw Error(`An operator definition has an alias that refers to another operator "${alias}"`);
255
+ this.OPERATOR_DICTIONARY[alias] = opDef;
256
+ }
257
+ }
258
+ if (operation.negationAliases) {
259
+ for (const alias of operation.negationAliases) {
260
+ if (alias in this.OPERATOR_DICTIONARY)
261
+ throw Error(`An operator definition has a negation alias that refers to another operator "${alias}"`);
262
+ this.OPERATOR_DICTIONARY[alias] = negOpDef;
263
+ }
326
264
  }
327
265
  }
328
- if (operatorTypes.includes(type))
329
- --operatorHits;
330
- if (fieldTypes.includes(type))
331
- --fieldHits;
266
+ this.TOKEN_REGEX = new RegExp(createTokenRegexString(Object.keys(this.OPERATOR_DICTIONARY).concat(WizardParser.GROUP_BRACKETS.flat()).concat(WizardParser.ARRAY_BRACKETS.flat()).concat(WizardParser.NEGATORS).concat(WizardParser.ARRAY_DELIMITERS), WizardParser.QUOTES), "g");
267
+ const quoteRegexStr = createQuoteRegexString(WizardParser.QUOTES);
268
+ this.QUOTE_REGEX = new RegExp(quoteRegexStr);
269
+ this.QUOTE_EDGE_REGEX = new RegExp(`^${quoteRegexStr}$`);
270
+ this.ARRAY_DELIMITER_REGEX = new RegExp(createArrayDelimitRegexString(WizardParser.QUOTES, WizardParser.ARRAY_DELIMITERS), "g");
271
+ Object.freeze(config);
332
272
  }
333
- if (!operatorHits)
334
- throw new Error("operator");
335
- if (!fieldHits)
336
- throw new Error("field");
337
- throw new Error("operator");
338
- }
339
- function validateCondition(condition, constraints, valueIsImplicit, ctx) {
340
- let validated = false;
341
- condition.field = processToken(condition.field).unescaped;
342
- const field = constraints?.caseInsensitive ? [...Object.keys(constraints.types ?? {}), ...Object.keys(constraints.restricted ?? {})].find((k) => k.toLowerCase() === condition.field.toLowerCase()) ?? condition.field : condition.field;
343
- const restriction = constraints?.restricted?.[field];
344
- const type = constraints?.types?.[field];
345
- if (constraints?.disallowUnvalidated && restriction === undefined && type === undefined)
346
- throw new ConstraintError(`Unknown field "${condition.field}"`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
347
- const operationType = COMPARISON_TYPE_DICTIONARY[condition.operation];
348
- const types = type && (Array.isArray(type) ? type : [type]);
349
- const processedValues = (Array.isArray(condition.value) ? condition.value : [condition.value]).map((v) => processToken(v));
350
- if (restriction === true)
351
- throw new ConstraintError(`Field "${condition.field}" is restricted`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
352
- else if (Array.isArray(restriction)) {
353
- const [philosophy, checks] = restriction;
354
- if (philosophy === "deny") {
355
- for (const check of checks) {
356
- if (check instanceof RegExp) {
357
- if (processedValues.some(({ unescaped }) => check.test(unescaped))) {
358
- throw new ConstraintError(`Value for field "${condition.field}" violates prohibitive pattern constraint "${check.toString()}". Prohibited values/patterns: Allowed values/patterns: ${checks.join(", ")}`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
359
- }
360
- } else {
361
- if (processedValues.some(({ unescaped }) => unescaped === check)) {
362
- throw new ConstraintError(`Forbidden value "${check}" for field "${condition.field}". Prohibited values/patterns: ${checks.join(", ")}`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
273
+ static pushSanitized(array, item, index) {
274
+ let trimmed = item.trimEnd();
275
+ const pretrimLength = trimmed.length;
276
+ trimmed = trimmed.trimStart();
277
+ const lengthDiff = trimmed.length - pretrimLength;
278
+ if (trimmed) {
279
+ const token = { content: trimmed, index: index - lengthDiff };
280
+ array.push(token);
281
+ return token;
282
+ }
283
+ }
284
+ interpretDate(v) {
285
+ if (this.CONFIG.dateInterpreter)
286
+ return this.CONFIG.dateInterpreter(v);
287
+ else
288
+ return new Date(v);
289
+ }
290
+ _tokenize(expression, pattern) {
291
+ const tokens = [];
292
+ const matches = expression.toUpperCase().matchAll(pattern);
293
+ let lastMatchEnd = null;
294
+ for (const match of matches) {
295
+ WizardParser.pushSanitized(tokens, expression.slice(lastMatchEnd ?? 0, match.index), lastMatchEnd === null ? 0 : lastMatchEnd);
296
+ if (WizardParser.ARRAY_BRACKETS.some(([o]) => match[0] === o)) {
297
+ const startToken = {
298
+ content: match[0],
299
+ index: match.index
300
+ };
301
+ tokens.push(startToken);
302
+ let subopenings = 0;
303
+ let endToken;
304
+ for (const submatch of matches) {
305
+ if (match[0] === submatch[0] || match[0] === submatch[0])
306
+ ++subopenings;
307
+ if (WizardParser.ARRAY_BRACKETS.some(([o, c]) => match[0] === o && submatch[0] === c)) {
308
+ if (subopenings)
309
+ --subopenings;
310
+ else {
311
+ endToken = {
312
+ content: submatch[0],
313
+ index: submatch.index
314
+ };
315
+ break;
316
+ }
363
317
  }
364
318
  }
365
- }
366
- } else {
367
- for (const { unescaped } of processedValues) {
368
- if (!checks.some((c) => c instanceof RegExp ? c.test(unescaped) : c === unescaped)) {
369
- throw new ConstraintError(`Value for field "${condition.field}" does not meet any allowed value/pattern. Allowed values/patterns: ${checks.join(", ")}`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
319
+ const subtokens = endToken ? this._tokenize(expression.slice(startToken.index + startToken.content.length, endToken.index), this.ARRAY_DELIMITER_REGEX) : this._tokenize(expression.slice(startToken.index + startToken.content.length), pattern);
320
+ for (const subtoken of subtokens)
321
+ subtoken.index += match.index + 1;
322
+ tokens.push(...subtokens);
323
+ if (endToken) {
324
+ tokens.push(endToken);
325
+ lastMatchEnd = endToken.index + endToken.content.length;
326
+ } else {
327
+ lastMatchEnd = expression.length;
328
+ break;
370
329
  }
330
+ continue;
331
+ } else {
332
+ WizardParser.pushSanitized(tokens, match.groups?.quotecontent !== undefined ? expression.slice(match.index, match.index + match[0].length) : match[0], match.index);
371
333
  }
334
+ lastMatchEnd = match.index + match[0].length;
372
335
  }
373
- validated = true;
336
+ WizardParser.pushSanitized(tokens, expression.slice(lastMatchEnd ?? 0), lastMatchEnd ?? 0);
337
+ return tokens;
374
338
  }
375
- if (operationType === "array" && !Array.isArray(condition.value))
376
- throw new ConstraintError(`Value "${condition.value.toString()}" is not permitted for "${condition.operation}" which expects an array`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
377
- else if (operationType !== "array" && Array.isArray(condition.value))
378
- throw new ConstraintError(`Value "${condition.value.toString()}" is not permitted for "${condition.operation}" which expects a non-array value`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
379
- const dateInterpreter = constraints?.dateInterpreter ?? ((v) => new Date(v));
380
- for (let i = 0;i < processedValues.length; ++i) {
381
- const v = processedValues[i];
382
- let opTypes;
383
- if (valueIsImplicit)
384
- opTypes = ["boolean"];
385
- else {
386
- switch (operationType) {
387
- case "primitive":
388
- case "array":
389
- opTypes = ["boolean", "number", "string"];
390
- if (types?.includes("date"))
391
- opTypes.push("date");
392
- break;
393
- case "numeric":
394
- opTypes = ["number"];
395
- if (types?.includes("date"))
396
- opTypes.push("date");
339
+ tokenize(expression) {
340
+ return this._tokenize(expression, this.TOKEN_REGEX);
341
+ }
342
+ static getClosingIndex(tokens, start, opening, closing) {
343
+ let openingCount = 1;
344
+ for (let index = start + 1;index < tokens.length; ++index) {
345
+ switch (tokens[index].content) {
346
+ case opening:
347
+ ++openingCount;
397
348
  break;
398
- case "string":
399
- opTypes = ["string"];
349
+ case closing:
350
+ --openingCount;
400
351
  break;
401
352
  }
353
+ if (openingCount === 0)
354
+ return index;
402
355
  }
403
- try {
404
- const value = coerceType(v, types ?? opTypes, opTypes, dateInterpreter);
405
- if (Array.isArray(condition.value))
406
- condition.value[i] = value;
407
- else
408
- condition.value = value;
409
- } catch (err) {
410
- switch (err.message) {
411
- case "operator":
412
- throw new ConstraintError(`Value "${condition.value.toString()}" not allowed for operation "${condition.operation}" which only allows for "${operationType}" type`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
413
- case "field":
414
- throw new ConstraintError(`Value "${condition.value.toString()}" includes a type not permitted for field "${condition.field}". Allowed types: ${(types ?? opTypes).join(", ")}`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
415
- }
416
- }
417
- }
418
- if (types)
419
- validated = true;
420
- const edit = condition;
421
- edit.field = field;
422
- edit.validated = validated;
423
- return edit;
424
- }
425
- function _parse(tokens, _offset, constraints) {
426
- let field;
427
- let comparisonOperation;
428
- let value;
429
- let inConjunction = false;
430
- let groupOperation;
431
- let expectingExpression = true;
432
- const expressions = [];
433
- function getExpressionGroup(ctx) {
434
- if (inConjunction) {
435
- const prior = expressions.at(-1);
436
- if (!prior)
437
- throw new ParseError("Unexpected: Expression list empty when parser is meant to append to an AND group", ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
438
- if (prior.type !== "group" || prior.operation !== "AND")
439
- throw new ParseError("Unexpected: Last expression is not an AND group yet parser thinks it's appending to one", ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
440
- return prior.constituents;
441
- }
442
- return expressions;
356
+ return -1;
443
357
  }
444
- function resolveCondition(ctx) {
445
- const baseCtx = {
446
- tokens,
447
- startToken: field?.token ?? comparisonOperation?.token ?? value?.token,
448
- startIndex: field?.index ?? comparisonOperation?.index ?? value?.index,
449
- endToken: value?.token ?? comparisonOperation?.token ?? field?.token,
450
- endIndex: value?.index ?? comparisonOperation?.index ?? field?.index
358
+ processToken(token) {
359
+ const unquoted = token.match(this.QUOTE_EDGE_REGEX)?.groups?.quotecontent;
360
+ const escaped = unquoted ?? token;
361
+ const unescaped = escaped.replaceAll(new RegExp(`(?<!${ESCAPE_REGEX}\\\\)\\\\`, "g"), "");
362
+ return {
363
+ raw: token,
364
+ unquoted,
365
+ escaped,
366
+ unescaped
451
367
  };
452
- if (!ctx)
453
- ctx = baseCtx;
454
- const group = getExpressionGroup(ctx);
455
- if (field && comparisonOperation && value) {
456
- if (!expectingExpression)
457
- throw new ParseError("Unexpected expression resolution before junctive operator", ctx.tokens, ctx.startToken, ctx.startIndex, ctx.endToken, ctx.endIndex);
458
- group.push(validateCondition({
459
- type: "condition",
460
- field: field.content,
461
- operation: comparisonOperation.content,
462
- value: value.content
463
- }, constraints, value.implicit, baseCtx));
464
- inConjunction = false;
465
- expectingExpression = false;
466
- } else if (field && !comparisonOperation && !value) {
467
- if (!expectingExpression)
468
- throw new ParseError("Unexpected expression resolution before junctive operator", ctx.tokens, ctx.startToken, ctx.startIndex, ctx.endToken, ctx.endIndex);
469
- group.push(validateCondition({
470
- type: "condition",
471
- field: field.content,
472
- operation: "EQUAL",
473
- value: "true"
474
- }, constraints, true, baseCtx));
475
- inConjunction = false;
476
- expectingExpression = false;
477
- } else if (field || comparisonOperation || value !== undefined)
478
- throw new ParseError("Failed to resolve condition; missing operand or operator", ctx.tokens, ctx.startToken, ctx.startIndex, ctx.endToken, ctx.endIndex);
479
- field = undefined;
480
- comparisonOperation = undefined;
481
- value = undefined;
482
368
  }
483
- for (let t = 0;t < tokens.length; ++t) {
484
- const token = tokens[t];
485
- if (PARENS.some(([, c]) => token.content === c))
486
- throw new ParseError("Unexpected closing parenthesis", tokens, token, _offset + t);
487
- if (BRACKETS.some(([, c]) => token.content === c))
488
- throw new ParseError("Unexpected closing bracket/brace", tokens, token, _offset + t);
489
- const paren = PARENS.find(([o]) => token.content === o);
490
- if (paren) {
491
- if (field || comparisonOperation || value)
492
- throw new ParseError("Tried to open a group during an operation", tokens, token, _offset + t);
493
- const closingIndex = getClosingIndex(tokens, t, paren[0], paren[1]);
494
- if (closingIndex === -1)
495
- throw new ParseError("Missing closing parenthesis for group", tokens, token, _offset + t);
496
- ++t;
497
- const subExpression = _parse(tokens.slice(t, closingIndex), _offset + t, constraints);
498
- if (subExpression) {
499
- if (subExpression.type === "group" && subExpression.operation === groupOperation)
500
- expressions.push(...subExpression.constituents);
501
- else {
502
- const group = getExpressionGroup({ tokens, startToken: token, startIndex: _offset + t });
503
- group.push(subExpression);
504
- inConjunction = false;
369
+ complementExpression(expression) {
370
+ const inverse = this.OPERATOR_DICTIONARY[expression.operation].negation;
371
+ if (!inverse)
372
+ throw new Error(`Could not find inverse operation given operation name "${expression.operation}"`);
373
+ expression.operation = inverse;
374
+ if (expression.type === "group")
375
+ expression.constituents.forEach((e) => this.complementExpression(e));
376
+ }
377
+ coerceType(processedToken, fieldTypes, operatorTypes) {
378
+ const { raw, unescaped } = processedToken;
379
+ let operatorHits = 0;
380
+ let fieldHits = 0;
381
+ for (const type of TYPE_PRIORITY) {
382
+ const operatorHit = operatorTypes.includes(type);
383
+ const fieldHit = fieldTypes.includes(type);
384
+ if (operatorHit)
385
+ ++operatorHits;
386
+ if (fieldHit)
387
+ ++fieldHits;
388
+ if (!operatorHit || !fieldHit)
389
+ continue;
390
+ switch (type) {
391
+ case "boolean":
392
+ if (raw === "true")
393
+ return true;
394
+ else if (raw === "false")
395
+ return false;
396
+ break;
397
+ case "number": {
398
+ const num = Number(raw);
399
+ if (isNaN(num))
400
+ break;
401
+ return num;
402
+ }
403
+ case "string":
404
+ return unescaped;
405
+ case "date": {
406
+ const num = Number(raw);
407
+ const date = this.interpretDate(isNaN(num) ? unescaped : num);
408
+ if (isNaN(+date))
409
+ break;
410
+ return date;
505
411
  }
506
412
  }
507
- t = closingIndex;
508
- continue;
413
+ if (operatorTypes.includes(type))
414
+ --operatorHits;
415
+ if (fieldTypes.includes(type))
416
+ --fieldHits;
509
417
  }
510
- const op = OPERATION_ALIAS_DICTIONARY[token.content];
511
- if (op && OPERATION_PURPOSE_DICTIONARY[op] === "junction") {
512
- resolveCondition({
513
- tokens,
514
- startToken: field?.token ?? token,
515
- startIndex: field?.index ?? _offset + t,
516
- endToken: token,
517
- endIndex: _offset + t
518
- });
519
- const prior = expressions.at(-1);
520
- if (!prior)
521
- throw new ParseError("Unexpected junction operator with no preceding expression", tokens, token, _offset + t);
522
- expectingExpression = true;
523
- if (groupOperation && groupOperation !== op) {
524
- if (expressions.length < 2)
525
- throw new ParseError("Unexpected junction operator with no preceding expression", tokens, token, _offset + t);
526
- switch (groupOperation) {
527
- case "AND": {
528
- const futureSubgroup = _parse(tokens.slice(t + 1), _offset + t, constraints);
529
- if (futureSubgroup === null)
530
- throw new ParseError("Dangling junction operator", tokens, token, _offset + t);
531
- return {
532
- type: "group",
533
- operation: "OR",
534
- constituents: [
535
- {
536
- type: "group",
537
- operation: "AND",
538
- constituents: expressions
539
- },
540
- futureSubgroup
541
- ]
542
- };
418
+ if (!operatorHits)
419
+ throw new Error("operator");
420
+ if (!fieldHits)
421
+ throw new Error("field");
422
+ throw new Error("operator");
423
+ }
424
+ validateCondition(condition, valueIsImplicit, ctx) {
425
+ let validated = false;
426
+ condition.field = this.processToken(condition.field).unescaped;
427
+ const field = this.CONFIG.caseInsensitive ? this.FIELD_LIST.get(condition.field.toLowerCase()) ?? condition.field : condition.field;
428
+ const restriction = this.CONFIG.restricted?.[field];
429
+ const fieldType = this.CONFIG.types?.[field];
430
+ if (this.CONFIG.disallowUnvalidated && restriction === undefined && fieldType === undefined)
431
+ throw new ConstraintError(`Unknown field "${condition.field}"`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
432
+ const fieldTypes = fieldType && (Array.isArray(fieldType) ? fieldType : [fieldType]);
433
+ const operationType = this.OPERATOR_DICTIONARY[condition.operation].type;
434
+ const processedValues = (Array.isArray(condition.value) ? condition.value : [condition.value]).map((v) => this.processToken(v));
435
+ if (restriction === true)
436
+ throw new ConstraintError(`Field "${condition.field}" is restricted`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
437
+ else if (Array.isArray(restriction)) {
438
+ const [philosophy, checks] = restriction;
439
+ if (philosophy === "deny") {
440
+ for (const check of checks) {
441
+ if (check instanceof RegExp) {
442
+ if (processedValues.some(({ unescaped }) => check.test(unescaped))) {
443
+ throw new ConstraintError(`Value for field "${condition.field}" violates prohibitive pattern constraint "${check.toString()}". Prohibited values/patterns: Allowed values/patterns: ${checks.join(", ")}`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
444
+ }
445
+ } else {
446
+ if (processedValues.some(({ unescaped }) => unescaped === check)) {
447
+ throw new ConstraintError(`Forbidden value "${check}" for field "${condition.field}". Prohibited values/patterns: ${checks.join(", ")}`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
448
+ }
543
449
  }
544
- case "OR":
545
- inConjunction = true;
546
- if (prior.type === "group" && prior.operation === "AND")
547
- continue;
548
- expressions.splice(-1, 1);
549
- expressions.push({
550
- type: "group",
551
- operation: "AND",
552
- constituents: [
553
- prior
554
- ]
555
- });
556
- continue;
450
+ }
451
+ } else {
452
+ for (const { unescaped } of processedValues) {
453
+ if (!checks.some((c) => c instanceof RegExp ? c.test(unescaped) : c === unescaped)) {
454
+ throw new ConstraintError(`Value for field "${condition.field}" does not meet any allowed value/pattern. Allowed values/patterns: ${checks.join(", ")}`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
455
+ }
456
+ }
457
+ }
458
+ validated = true;
459
+ }
460
+ if (operationType === "array" && !Array.isArray(condition.value))
461
+ throw new ConstraintError(`Value "${condition.value}" is not permitted for "${condition.operation}" which expects an array`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
462
+ else if (operationType !== "array" && Array.isArray(condition.value))
463
+ throw new ConstraintError(`Value "${condition.value.toString()}" is not permitted for "${condition.operation}" which expects a non-array value`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
464
+ for (let i = 0;i < processedValues.length; ++i) {
465
+ const v = processedValues[i];
466
+ let opTypes;
467
+ if (valueIsImplicit) {
468
+ const implicitTypes = this.CONFIG.implicitCondition.asType;
469
+ opTypes = Array.isArray(implicitTypes) ? implicitTypes : [implicitTypes];
470
+ } else {
471
+ switch (operationType) {
472
+ case "primitive":
473
+ case "array":
474
+ opTypes = ["boolean", "number", "string"];
475
+ if (fieldTypes?.includes("date"))
476
+ opTypes.push("date");
477
+ break;
478
+ case "number":
479
+ opTypes = ["number"];
480
+ break;
481
+ case "date":
482
+ opTypes = ["date"];
483
+ break;
484
+ case "boolean":
485
+ opTypes = ["boolean"];
486
+ break;
487
+ case "numeric":
488
+ opTypes = ["number"];
489
+ if (fieldTypes?.includes("date"))
490
+ opTypes.push("date");
491
+ break;
492
+ case "string":
493
+ opTypes = ["string"];
494
+ break;
557
495
  }
558
496
  }
559
- groupOperation = op;
560
- if (expressions.length === 1 && expressions[0]?.type === "group" && expressions[0].operation === groupOperation) {
561
- const exp = expressions[0];
562
- expressions.splice(0, 1);
563
- expressions.push(...exp.constituents);
497
+ try {
498
+ const value = this.coerceType(v, fieldTypes ?? opTypes, opTypes);
499
+ if (Array.isArray(condition.value))
500
+ condition.value[i] = value;
501
+ else
502
+ condition.value = value;
503
+ } catch (err) {
504
+ switch (err.message) {
505
+ case "operator":
506
+ throw new ConstraintError(`Value "${condition.value.toString()}" not allowed for operation "${condition.operation}" which only allows for "${operationType}" type`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
507
+ case "field":
508
+ throw new ConstraintError(`Value "${condition.value.toString()}" includes a type not permitted for field "${condition.field}". Allowed types: ${(fieldTypes ?? opTypes).join(", ")}`, ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
509
+ }
564
510
  }
565
- continue;
566
511
  }
567
- if (NEGATORS.includes(token.content)) {
568
- const nextToken = tokens[t + 1];
569
- const nextParen = PARENS.find(([o]) => nextToken?.content === o);
570
- if (nextParen) {
571
- resolveCondition({
572
- tokens,
573
- startToken: field?.token ?? token,
574
- startIndex: field?.index ?? _offset + t,
575
- endToken: value?.token ?? comparisonOperation?.token ?? field?.token,
576
- endIndex: value?.index ?? comparisonOperation?.index ?? field?.index
577
- });
578
- ++t;
579
- const closingIndex = getClosingIndex(tokens, t, nextParen[0], nextParen[1]);
512
+ if (fieldTypes)
513
+ validated = true;
514
+ const edit = condition;
515
+ edit.field = field;
516
+ edit.validated = validated;
517
+ return edit;
518
+ }
519
+ _parse(tokens, _offset) {
520
+ let field;
521
+ let comparisonOperation;
522
+ let value;
523
+ let inConjunction = false;
524
+ let activeGroupOperation;
525
+ let expectingExpression = true;
526
+ const expressions = [];
527
+ const getExpressionGroup = (ctx) => {
528
+ if (inConjunction) {
529
+ const prior = expressions.at(-1);
530
+ if (!prior)
531
+ throw new ParseError("Unexpected: Expression list empty when parser is meant to append to an AND group", ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
532
+ if (prior.type !== "group" || prior.operation !== "AND")
533
+ throw new ParseError("Unexpected: Last expression is not an AND group yet parser thinks it's appending to one", ctx?.tokens, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
534
+ return prior.constituents;
535
+ }
536
+ return expressions;
537
+ };
538
+ const resolveCondition = (ctx) => {
539
+ const baseCtx = {
540
+ tokens,
541
+ startToken: field?.token ?? comparisonOperation?.token ?? value?.token,
542
+ startIndex: field?.index ?? comparisonOperation?.index ?? value?.index,
543
+ endToken: value?.token ?? comparisonOperation?.token ?? field?.token,
544
+ endIndex: value?.index ?? comparisonOperation?.index ?? field?.index
545
+ };
546
+ if (!ctx)
547
+ ctx = baseCtx;
548
+ const group = getExpressionGroup(ctx);
549
+ if (field && comparisonOperation && value) {
550
+ if (!expectingExpression)
551
+ throw new ParseError("Unexpected expression resolution before junctive operator", ctx.tokens, ctx.startToken, ctx.startIndex, ctx.endToken, ctx.endIndex);
552
+ group.push(this.validateCondition({
553
+ type: "condition",
554
+ field: field.content,
555
+ operation: comparisonOperation.content,
556
+ value: value.content
557
+ }, value.implicit, baseCtx));
558
+ inConjunction = false;
559
+ expectingExpression = false;
560
+ } else if (field && !comparisonOperation && !value) {
561
+ if (!expectingExpression)
562
+ throw new ParseError("Unexpected expression resolution before junctive operator", ctx.tokens, ctx.startToken, ctx.startIndex, ctx.endToken, ctx.endIndex);
563
+ if (this.CONFIG.implicitCondition) {
564
+ group.push(this.validateCondition({
565
+ type: "condition",
566
+ field: field.content,
567
+ operation: this.CONFIG.implicitCondition.operator,
568
+ value: this.CONFIG.implicitCondition.value
569
+ }, true, baseCtx));
570
+ inConjunction = false;
571
+ expectingExpression = false;
572
+ } else
573
+ throw new ParseError("Failed to resolve condition; missing operand or operator (No implicit condition is configured)", ctx.tokens, ctx.startToken, ctx.startIndex, ctx.endToken, ctx.endIndex);
574
+ } else if (field || comparisonOperation || value !== undefined)
575
+ throw new ParseError("Failed to resolve condition; missing operand or operator", ctx.tokens, ctx.startToken, ctx.startIndex, ctx.endToken, ctx.endIndex);
576
+ field = undefined;
577
+ comparisonOperation = undefined;
578
+ value = undefined;
579
+ };
580
+ for (let t = 0;t < tokens.length; ++t) {
581
+ const token = tokens[t];
582
+ if (WizardParser.GROUP_BRACKETS.some(([, c]) => token.content === c))
583
+ throw new ParseError("Unexpected closing parenthesis", tokens, token, _offset + t);
584
+ if (WizardParser.ARRAY_BRACKETS.some(([, c]) => token.content === c))
585
+ throw new ParseError("Unexpected closing bracket/brace", tokens, token, _offset + t);
586
+ const paren = WizardParser.GROUP_BRACKETS.find(([o]) => token.content === o);
587
+ if (paren) {
588
+ if (field || comparisonOperation || value)
589
+ throw new ParseError("Tried to open a group during an operation", tokens, token, _offset + t);
590
+ const closingIndex = WizardParser.getClosingIndex(tokens, t, paren[0], paren[1]);
580
591
  if (closingIndex === -1)
581
592
  throw new ParseError("Missing closing parenthesis for group", tokens, token, _offset + t);
582
593
  ++t;
583
- const futureSubExpression = _parse(tokens.slice(t, closingIndex), _offset + t, constraints);
584
- if (futureSubExpression) {
585
- complementExpression(futureSubExpression);
586
- if (futureSubExpression.type === "group" && futureSubExpression.operation === groupOperation)
587
- expressions.push(...futureSubExpression.constituents);
594
+ const subExpression = this._parse(tokens.slice(t, closingIndex), _offset + t);
595
+ if (subExpression) {
596
+ if (subExpression.type === "group" && subExpression.operation === activeGroupOperation)
597
+ expressions.push(...subExpression.constituents);
588
598
  else {
589
- const group = getExpressionGroup({
590
- tokens,
591
- startToken: token,
592
- startIndex: _offset + t,
593
- endToken: tokens[closingIndex],
594
- endIndex: closingIndex
595
- });
596
- group.push(futureSubExpression);
599
+ const group = getExpressionGroup({ tokens, startToken: token, startIndex: _offset + t });
600
+ group.push(subExpression);
597
601
  inConjunction = false;
598
602
  }
599
603
  }
600
604
  t = closingIndex;
601
605
  continue;
602
606
  }
603
- }
604
- if (!comparisonOperation || op && OPERATION_PURPOSE_DICTIONARY[op] === "comparison") {
605
- if (op && OPERATION_PURPOSE_DICTIONARY[op] === "comparison") {
606
- if (comparisonOperation || !field)
607
- throw new ParseError("Unexpected comparison operator", tokens, field?.token ?? token, field?.index ?? _offset + t, token, _offset + t);
608
- comparisonOperation = {
609
- content: op,
610
- token,
611
- index: _offset + t
612
- };
613
- continue;
614
- } else if (field)
615
- throw new ParseError("Expected a comparison operator", tokens, field.token, field.index, token, _offset + t);
616
- }
617
- if (!field) {
618
- if (NEGATORS.includes(token.content)) {
619
- const nextToken = tokens[t + 1];
620
- if (!nextToken)
621
- throw new ParseError('Unexpected "!"', tokens, token, _offset + t);
607
+ const op = this.OPERATOR_DICTIONARY[token.content];
608
+ if (op?.type === "sumjunction" || op?.type === "productjunction") {
622
609
  resolveCondition({
623
610
  tokens,
624
- startToken: token,
625
- startIndex: _offset + t,
626
- endToken: nextToken,
627
- endIndex: _offset + t + 1
611
+ startToken: field?.token ?? token,
612
+ startIndex: field?.index ?? _offset + t,
613
+ endToken: token,
614
+ endIndex: _offset + t
628
615
  });
629
- ++t;
630
- field = {
631
- content: nextToken.content,
632
- token,
633
- index: _offset + t
634
- };
635
- comparisonOperation = {
636
- content: "EQUAL"
637
- };
638
- value = {
639
- content: "false",
640
- implicit: true
641
- };
616
+ const prior = expressions.at(-1);
617
+ if (!prior)
618
+ throw new ParseError("Unexpected junction operator with no preceding expression", tokens, token, _offset + t);
619
+ expectingExpression = true;
620
+ if (activeGroupOperation && activeGroupOperation !== op.name) {
621
+ if (expressions.length < 2)
622
+ throw new ParseError("Unexpected junction operator with no preceding expression", tokens, token, _offset + t);
623
+ const activeGroupOperationType = this.OPERATOR_DICTIONARY[activeGroupOperation].type;
624
+ if (activeGroupOperationType === op.type)
625
+ throw new ParseError("Mixed two junction operators of the same precedence level. Unclear how to separate without grouping", tokens, token, _offset + t);
626
+ switch (op.type) {
627
+ case "productjunction": {
628
+ inConjunction = true;
629
+ if (prior.type === "group" && this.OPERATOR_DICTIONARY[prior.operation].type === "productjunction")
630
+ continue;
631
+ expressions.splice(-1, 1);
632
+ expressions.push({
633
+ type: "group",
634
+ operation: op.name,
635
+ constituents: [
636
+ prior
637
+ ]
638
+ });
639
+ continue;
640
+ }
641
+ case "sumjunction": {
642
+ const futureSubgroup = this._parse(tokens.slice(t + 1), _offset + t);
643
+ if (futureSubgroup === null)
644
+ throw new ParseError("Dangling junction operator", tokens, token, _offset + t);
645
+ return {
646
+ type: "group",
647
+ operation: op.name,
648
+ constituents: [
649
+ {
650
+ type: "group",
651
+ operation: activeGroupOperation,
652
+ constituents: expressions
653
+ },
654
+ futureSubgroup
655
+ ]
656
+ };
657
+ }
658
+ }
659
+ }
660
+ activeGroupOperation = op.name;
661
+ if (expressions.length === 1 && expressions[0]?.type === "group" && expressions[0].operation === activeGroupOperation) {
662
+ const exp = expressions[0];
663
+ expressions.splice(0, 1);
664
+ expressions.push(...exp.constituents);
665
+ }
666
+ continue;
667
+ }
668
+ if (WizardParser.NEGATORS.includes(token.content)) {
669
+ const nextToken = tokens[t + 1];
670
+ const nextParen = WizardParser.GROUP_BRACKETS.find(([o]) => nextToken?.content === o);
671
+ if (nextParen) {
672
+ resolveCondition({
673
+ tokens,
674
+ startToken: field?.token ?? token,
675
+ startIndex: field?.index ?? _offset + t,
676
+ endToken: value?.token ?? comparisonOperation?.token ?? field?.token,
677
+ endIndex: value?.index ?? comparisonOperation?.index ?? field?.index
678
+ });
679
+ ++t;
680
+ const closingIndex = WizardParser.getClosingIndex(tokens, t, nextParen[0], nextParen[1]);
681
+ if (closingIndex === -1)
682
+ throw new ParseError("Missing closing parenthesis for group", tokens, token, _offset + t);
683
+ ++t;
684
+ const futureSubExpression = this._parse(tokens.slice(t, closingIndex), _offset + t);
685
+ if (futureSubExpression) {
686
+ this.complementExpression(futureSubExpression);
687
+ if (futureSubExpression.type === "group" && futureSubExpression.operation === activeGroupOperation)
688
+ expressions.push(...futureSubExpression.constituents);
689
+ else {
690
+ const group = getExpressionGroup({
691
+ tokens,
692
+ startToken: token,
693
+ startIndex: _offset + t,
694
+ endToken: tokens[closingIndex],
695
+ endIndex: closingIndex
696
+ });
697
+ group.push(futureSubExpression);
698
+ inConjunction = false;
699
+ }
700
+ }
701
+ t = closingIndex;
702
+ continue;
703
+ }
704
+ }
705
+ if (!comparisonOperation || op) {
706
+ if (op) {
707
+ if (comparisonOperation || !field)
708
+ throw new ParseError("Unexpected comparison operator", tokens, field?.token ?? token, field?.index ?? _offset + t, token, _offset + t);
709
+ comparisonOperation = {
710
+ content: op.name,
711
+ token,
712
+ index: _offset + t
713
+ };
714
+ continue;
715
+ } else if (field)
716
+ throw new ParseError("Expected a comparison operator", tokens, field.token, field.index, token, _offset + t);
717
+ }
718
+ if (!field) {
719
+ if (WizardParser.NEGATORS.includes(token.content)) {
720
+ if (!this.CONFIG.implicitCondition)
721
+ throw new ParseError("Could not attempt a negative implicit condition as one is not defined in the config", tokens, token, _offset + t);
722
+ const nextToken = tokens[t + 1];
723
+ if (!nextToken)
724
+ throw new ParseError('Unexpected "!"', tokens, token, _offset + t);
725
+ resolveCondition({
726
+ tokens,
727
+ startToken: token,
728
+ startIndex: _offset + t,
729
+ endToken: nextToken,
730
+ endIndex: _offset + t + 1
731
+ });
732
+ ++t;
733
+ field = {
734
+ content: nextToken.content,
735
+ token,
736
+ index: _offset + t
737
+ };
738
+ comparisonOperation = {
739
+ content: this.OPERATOR_DICTIONARY[this.CONFIG.implicitCondition.operator].negation
740
+ };
741
+ value = {
742
+ content: this.CONFIG.implicitCondition.value,
743
+ implicit: true
744
+ };
745
+ resolveCondition({
746
+ tokens,
747
+ startToken: field.token,
748
+ startIndex: field.index,
749
+ endToken: nextToken,
750
+ endIndex: _offset + t
751
+ });
752
+ } else {
753
+ field = {
754
+ content: token.content,
755
+ token,
756
+ index: _offset + t
757
+ };
758
+ }
759
+ continue;
760
+ }
761
+ if (!value) {
762
+ const bracket = WizardParser.ARRAY_BRACKETS.find(([o]) => token.content === o);
763
+ if (bracket) {
764
+ const closingIndex = WizardParser.getClosingIndex(tokens, t, bracket[0], bracket[1]);
765
+ if (closingIndex === -1)
766
+ throw new ParseError("Missing closing bracket/brace for array value", tokens, token, _offset + t);
767
+ ++t;
768
+ const arr = [];
769
+ value = {
770
+ content: arr,
771
+ token: tokens[closingIndex],
772
+ index: closingIndex
773
+ };
774
+ const arrayContents = tokens.slice(t, closingIndex);
775
+ let workingEntry = "";
776
+ let firstEntryToken;
777
+ let firstEntryTokenIndex;
778
+ let lastEntryToken;
779
+ let lastEntryTokenIndex;
780
+ const resolveEntry = (subtoken, subindex) => {
781
+ if (workingEntry) {
782
+ const subquotes = workingEntry.match(this.QUOTE_REGEX);
783
+ if (subquotes && (subquotes.index !== 0 || subquotes[0].length !== workingEntry.length))
784
+ throw new ParseError("Quotes must surround entire values in arrays", tokens, firstEntryToken ?? subtoken, firstEntryTokenIndex ?? subindex, lastEntryToken ?? subtoken, lastEntryTokenIndex ?? subindex);
785
+ arr.push(workingEntry);
786
+ workingEntry = "";
787
+ firstEntryToken = undefined;
788
+ firstEntryTokenIndex = undefined;
789
+ lastEntryToken = undefined;
790
+ lastEntryTokenIndex = undefined;
791
+ }
792
+ };
793
+ for (let ct = 0;ct < arrayContents.length; ++ct) {
794
+ const contentToken = arrayContents[ct];
795
+ if (WizardParser.ARRAY_DELIMITERS.includes(contentToken.content)) {
796
+ if (!workingEntry)
797
+ throw new ParseError("Unexpected blank entry in array", tokens, contentToken, _offset + t + ct);
798
+ resolveEntry(contentToken, _offset + t + ct);
799
+ } else {
800
+ lastEntryToken = contentToken;
801
+ lastEntryTokenIndex = _offset + t + ct;
802
+ if (!firstEntryToken)
803
+ firstEntryToken = lastEntryToken;
804
+ if (!firstEntryTokenIndex)
805
+ firstEntryTokenIndex = lastEntryTokenIndex;
806
+ workingEntry += contentToken.content;
807
+ }
808
+ }
809
+ resolveEntry(arrayContents.at(-1), _offset + t + arrayContents.length - 1);
810
+ if (!arr.length) {
811
+ throw new ParseError("Empty array provided as value", tokens, token, _offset + t - 1, tokens[closingIndex], _offset + closingIndex);
812
+ }
813
+ t = closingIndex;
814
+ } else {
815
+ value = {
816
+ content: token.content,
817
+ token,
818
+ index: _offset + t
819
+ };
820
+ }
642
821
  resolveCondition({
643
822
  tokens,
644
823
  startToken: field.token,
645
824
  startIndex: field.index,
646
- endToken: nextToken,
647
- endIndex: _offset + t
825
+ endToken: value.token ?? comparisonOperation?.token ?? field.token,
826
+ endIndex: value.index ?? comparisonOperation?.index ?? field.index
648
827
  });
649
- } else {
650
- field = {
651
- content: token.content,
652
- token,
653
- index: _offset + t
654
- };
655
828
  }
656
- continue;
657
829
  }
658
- if (!value) {
659
- const bracket = BRACKETS.find(([o]) => token.content === o);
660
- if (bracket) {
661
- let resolveEntry = function(subtoken, subindex) {
662
- if (workingEntry) {
663
- const subquotes = workingEntry.match(QUOTE_REGEX);
664
- if (subquotes && (subquotes.index !== 0 || subquotes[0].length !== workingEntry.length))
665
- throw new ParseError("Quotes must surround entire values in arrays", tokens, firstEntryToken ?? subtoken, firstEntryTokenIndex ?? subindex, lastEntryToken ?? subtoken, lastEntryTokenIndex ?? subindex);
666
- arr.push(workingEntry);
667
- workingEntry = "";
668
- firstEntryToken = undefined;
669
- firstEntryTokenIndex = undefined;
670
- lastEntryToken = undefined;
671
- lastEntryTokenIndex = undefined;
672
- }
673
- };
674
- const closingIndex = getClosingIndex(tokens, t, bracket[0], bracket[1]);
675
- if (closingIndex === -1)
676
- throw new ParseError("Missing closing bracket/brace for array value", tokens, token, _offset + t);
677
- ++t;
678
- const arr = [];
679
- value = {
680
- content: arr,
681
- token: tokens[closingIndex],
682
- index: closingIndex
683
- };
684
- const arrayContents = tokens.slice(t, closingIndex);
685
- let workingEntry = "";
686
- let firstEntryToken;
687
- let firstEntryTokenIndex;
688
- let lastEntryToken;
689
- let lastEntryTokenIndex;
690
- for (let ct = 0;ct < arrayContents.length; ++ct) {
691
- const contentToken = arrayContents[ct];
692
- if (ARRAY_DELIMITERS.includes(contentToken.content)) {
693
- if (!workingEntry)
694
- throw new ParseError("Unexpected blank entry in array", tokens, contentToken, _offset + t + ct);
695
- resolveEntry(contentToken, _offset + t + ct);
696
- } else {
697
- lastEntryToken = contentToken;
698
- lastEntryTokenIndex = _offset + t + ct;
699
- if (!firstEntryToken)
700
- firstEntryToken = lastEntryToken;
701
- if (!firstEntryTokenIndex)
702
- firstEntryTokenIndex = lastEntryTokenIndex;
703
- workingEntry += contentToken.content;
830
+ resolveCondition({
831
+ tokens,
832
+ startToken: field?.token,
833
+ startIndex: field?.index,
834
+ endToken: value?.token ?? comparisonOperation?.token ?? field?.token,
835
+ endIndex: value?.index ?? comparisonOperation?.index ?? field?.index
836
+ });
837
+ if (inConjunction)
838
+ throw new ParseError("Dangling junction operator", tokens, tokens.at(-1), _offset + tokens.length - 1);
839
+ if (activeGroupOperation) {
840
+ if (expressions.length === 1)
841
+ throw new ParseError("Dangling junction operator", tokens, tokens.at(-1), _offset + tokens.length - 1);
842
+ return {
843
+ type: "group",
844
+ operation: activeGroupOperation,
845
+ constituents: expressions
846
+ };
847
+ } else if (expressions.length > 1)
848
+ throw new ParseError("Group possesses multiple conditions without disjunctive operators", tokens, tokens[0], _offset);
849
+ else
850
+ return expressions[0] ?? null;
851
+ }
852
+ parse(expression) {
853
+ let tokens;
854
+ if (Array.isArray(expression)) {
855
+ tokens = [];
856
+ let type;
857
+ for (let t = 0;t < expression.length; ++t) {
858
+ const token = expression[t];
859
+ if (!type)
860
+ type = typeof token;
861
+ if (typeof token !== type)
862
+ console.warn("WizardQL: parse was called with a mixed array of string tokens and token objects");
863
+ tokens.push(typeof token === "string" ? { content: token, index: type === "string" ? t : -1 } : token);
864
+ }
865
+ } else
866
+ tokens = this.tokenize(expression);
867
+ return this._parse(tokens, 0);
868
+ }
869
+ addQuotesIfNecessary(value) {
870
+ if (value instanceof Date) {
871
+ return this.CONFIG.dateSerializer ? this.CONFIG.dateSerializer(value) : value.toISOString();
872
+ }
873
+ if (typeof value !== "string")
874
+ return value.toString();
875
+ if (value === "true")
876
+ return '"true"';
877
+ if (value === "false")
878
+ return '"false"';
879
+ if (!isNaN(Number(value)))
880
+ return `"${value}"`;
881
+ const escaped = value.replaceAll("\\", "\\\\");
882
+ if (new RegExp(this.TOKEN_REGEX, "gi").test(escaped))
883
+ return `"${escaped.replaceAll('"', "\\\"")}"`;
884
+ return escaped;
885
+ }
886
+ stringify(expression, opts) {
887
+ const {
888
+ dialect,
889
+ alwaysParenthesize = false,
890
+ compact = false,
891
+ condenseImplicit = false
892
+ } = typeof opts === "string" ? { dialect: opts } : opts;
893
+ if (!this.CONFIG.dialects)
894
+ throw new Error("No dialect dictionaries are defined in the config");
895
+ if (!(dialect in this.CONFIG.dialects))
896
+ throw new Error(`Dialect '${dialect}' is not defined in the config`);
897
+ let string = "";
898
+ switch (expression.type) {
899
+ case "group":
900
+ for (const constituent of expression.constituents) {
901
+ if (string.length) {
902
+ const transformed = this.CONFIG.dialects[dialect][expression.operation];
903
+ const alpha = isAlpha(transformed);
904
+ if (!compact || alpha)
905
+ string += " ";
906
+ string += transformed;
907
+ if (!compact || alpha)
908
+ string += " ";
704
909
  }
910
+ if (alwaysParenthesize && constituent.type === "group" || expression.operation === "AND" && constituent.operation === "OR")
911
+ string += WizardParser.GROUP_BRACKETS[0][0];
912
+ string += this.stringify(constituent, opts);
913
+ if (alwaysParenthesize && constituent.type === "group" || expression.operation === "AND" && constituent.operation === "OR")
914
+ string += WizardParser.GROUP_BRACKETS[0][1];
705
915
  }
706
- resolveEntry(arrayContents.at(-1), _offset + t + arrayContents.length - 1);
707
- if (!arr.length) {
708
- throw new ParseError("Empty array provided as value", tokens, token, _offset + t - 1, tokens[closingIndex], _offset + closingIndex);
916
+ break;
917
+ case "condition": {
918
+ const resolveNonImplicit = () => {
919
+ const transformed = this.CONFIG.dialects[dialect][expression.operation];
920
+ const alpha = isAlpha(transformed);
921
+ string += expression.field;
922
+ if (!compact || alpha)
923
+ string += " ";
924
+ string += transformed;
925
+ if (!compact || alpha)
926
+ string += " ";
927
+ if (Array.isArray(expression.value)) {
928
+ const join = expression.value.map((v) => this.addQuotesIfNecessary(v)).join(compact ? WizardParser.ARRAY_DELIMITERS[0] : WizardParser.ARRAY_DELIMITERS[0] + " ");
929
+ string += `${WizardParser.ARRAY_BRACKETS[0][0]}${join}${WizardParser.ARRAY_BRACKETS[0][1]}`;
930
+ } else
931
+ string += this.addQuotesIfNecessary(expression.value);
932
+ };
933
+ if (condenseImplicit && this.CONFIG.implicitCondition) {
934
+ const implicitOperator = this.OPERATOR_DICTIONARY[this.CONFIG.implicitCondition.operator];
935
+ let isNegation = expression.operation === implicitOperator.negation;
936
+ if (expression.operation === implicitOperator.name || isNegation) {
937
+ const fieldType = this.CONFIG.types?.[expression.field];
938
+ const fieldTypes = fieldType && (Array.isArray(fieldType) ? fieldType : [fieldType]);
939
+ const opTypes = Array.isArray(this.CONFIG.implicitCondition.asType) ? this.CONFIG.implicitCondition.asType : [this.CONFIG.implicitCondition.asType];
940
+ const processed = this.processToken(this.CONFIG.implicitCondition.value);
941
+ const coerced = this.coerceType(processed, fieldTypes ?? opTypes, opTypes);
942
+ const bothBoolean = typeof coerced === "boolean" && typeof expression.value === "boolean";
943
+ if (coerced.valueOf() === expression.value || bothBoolean) {
944
+ if (bothBoolean && coerced !== expression.value)
945
+ isNegation = !isNegation;
946
+ string += `${isNegation ? WizardParser.NEGATORS[0] : ""}${expression.field}`;
947
+ } else
948
+ resolveNonImplicit();
949
+ } else
950
+ resolveNonImplicit();
951
+ } else
952
+ resolveNonImplicit();
953
+ break;
954
+ }
955
+ }
956
+ return string;
957
+ }
958
+ static getOrPutArrayInMap(map, key) {
959
+ const existingEntry = map.get(key);
960
+ if (existingEntry)
961
+ return existingEntry;
962
+ else {
963
+ const arr = [];
964
+ map.set(key, arr);
965
+ return arr;
966
+ }
967
+ }
968
+ summarize(expressions) {
969
+ const array = Array.isArray(expressions) ? expressions : [expressions];
970
+ const summary = new Map;
971
+ for (const expression of array) {
972
+ if (expression.type === "group") {
973
+ const constituents = this.summarize(expression.constituents);
974
+ for (const [field, values] of constituents.entries()) {
975
+ const collection = WizardParser.getOrPutArrayInMap(summary, field);
976
+ collection.push(...values);
709
977
  }
710
- t = closingIndex;
711
978
  } else {
712
- value = {
713
- content: token.content,
714
- token,
715
- index: _offset + t
716
- };
979
+ const collection = WizardParser.getOrPutArrayInMap(summary, expression.field);
980
+ collection.push({
981
+ operation: expression.operation,
982
+ value: expression.value,
983
+ exclusionary: this.OPERATOR_DICTIONARY[expression.operation].exclusionary
984
+ });
717
985
  }
718
- resolveCondition({
719
- tokens,
720
- startToken: field.token,
721
- startIndex: field.index,
722
- endToken: value.token ?? comparisonOperation?.token ?? field.token,
723
- endIndex: value.index ?? comparisonOperation?.index ?? field.index
724
- });
725
986
  }
987
+ return summary;
726
988
  }
727
- resolveCondition({
728
- tokens,
729
- startToken: field?.token,
730
- startIndex: field?.index,
731
- endToken: value?.token ?? comparisonOperation?.token ?? field?.token,
732
- endIndex: value?.index ?? comparisonOperation?.index ?? field?.index
733
- });
734
- if (inConjunction)
735
- throw new ParseError("Dangling junction operator", tokens, tokens.at(-1), _offset + tokens.length - 1);
736
- if (groupOperation) {
737
- if (expressions.length === 1)
738
- throw new ParseError("Dangling junction operator", tokens, tokens.at(-1), _offset + tokens.length - 1);
739
- return {
740
- type: "group",
741
- operation: groupOperation,
742
- constituents: expressions
743
- };
744
- } else if (expressions.length > 1)
745
- throw new ParseError("Group possesses multiple conditions without disjunctive operators", tokens, tokens[0], _offset);
746
- else
747
- return expressions[0] ?? null;
748
- }
749
- function parse(expression, constraints) {
750
- let tokens;
751
- if (Array.isArray(expression)) {
752
- tokens = [];
753
- let type;
754
- for (let t = 0;t < expression.length; ++t) {
755
- const token = expression[t];
756
- if (!type)
757
- type = typeof token;
758
- if (typeof token !== type)
759
- console.warn("WizardQL: parse was called with a mixed array of string tokens and token objects");
760
- tokens.push(typeof token === "string" ? { content: token, index: type === "string" ? t : -1 } : token);
989
+ getPartType(segment, activeArrayOpeningBracket) {
990
+ if (segment.match(this.QUOTE_EDGE_REGEX))
991
+ return "quoted";
992
+ if (!isNaN(Number(segment)))
993
+ return "number";
994
+ for (const [opening, closing] of WizardParser.ARRAY_BRACKETS) {
995
+ if (!activeArrayOpeningBracket && segment === opening)
996
+ return "openingarraybracket";
997
+ else if (activeArrayOpeningBracket === opening && segment === closing)
998
+ return "closingarraybracket";
761
999
  }
762
- } else
763
- tokens = tokenize(expression);
764
- return _parse(tokens, 0, constraints);
1000
+ if (!activeArrayOpeningBracket) {
1001
+ for (const [opening, closing] of WizardParser.GROUP_BRACKETS) {
1002
+ if (segment === opening)
1003
+ return "openinggroupbracket";
1004
+ else if (segment === closing)
1005
+ return "closinggroupbracket";
1006
+ }
1007
+ }
1008
+ if (activeArrayOpeningBracket && WizardParser.ARRAY_DELIMITERS.includes(segment))
1009
+ return "arraydelimiter";
1010
+ if (!activeArrayOpeningBracket && WizardParser.NEGATORS.includes(segment))
1011
+ return "negator";
1012
+ if (!activeArrayOpeningBracket && segment in this.OPERATOR_DICTIONARY) {
1013
+ const type = this.OPERATOR_DICTIONARY[segment].type;
1014
+ if (type === "sumjunction" || type === "productjunction")
1015
+ return "junctionoperator";
1016
+ else
1017
+ return "conditionoperator";
1018
+ }
1019
+ return "literal";
1020
+ }
1021
+ resolveOperatorAlias(alias) {
1022
+ return this.OPERATOR_DICTIONARY[alias]?.name;
1023
+ }
765
1024
  }
766
1025
  // src/execute.ts
767
1026
  function executeAsKnex(query, expression) {
@@ -809,138 +1068,6 @@ function executeAsKnex(query, expression) {
809
1068
  }
810
1069
  }
811
1070
  }
812
- // src/summarize.ts
813
- function getOrPutArrayInMap(map, key) {
814
- const existingEntry = map.get(key);
815
- if (existingEntry)
816
- return existingEntry;
817
- else {
818
- const arr = [];
819
- map.set(key, arr);
820
- return arr;
821
- }
822
- }
823
- function summarize(expressions) {
824
- const array = Array.isArray(expressions) ? expressions : [expressions];
825
- const summary = new Map;
826
- for (const expression of array) {
827
- if (expression.type === "group") {
828
- const constituents = summarize(expression.constituents);
829
- for (const [field, values] of constituents.entries()) {
830
- const collection = getOrPutArrayInMap(summary, field);
831
- collection.push(...values);
832
- }
833
- } else {
834
- const collection = getOrPutArrayInMap(summary, expression.field);
835
- collection.push({
836
- operation: expression.operation,
837
- value: expression.value,
838
- exclusionary: ["NOTEQUAL", "LESS", "GREATER", "NOTIN"].includes(expression.operation)
839
- });
840
- }
841
- }
842
- return summary;
843
- }
844
- // src/stringify.ts
845
- var DEFAULT_BRACKET = BRACKETS[0];
846
- var DEFAULT_PAREN = PARENS[0];
847
- var DEFAULT_NEGATOR = NEGATORS[0];
848
- var DEFAULT_DELIMITER = ARRAY_DELIMITERS[0];
849
- var formats = {
850
- programmatic: {
851
- AND: "&",
852
- OR: "|",
853
- EQUAL: "=",
854
- NOTEQUAL: "!=",
855
- GEQ: ">=",
856
- GREATER: ">",
857
- LEQ: "<=",
858
- LESS: "<",
859
- IN: ":",
860
- NOTIN: "!:",
861
- MATCH: "~",
862
- NOTMATCH: "!~"
863
- },
864
- linguistic: {
865
- AND: "AND",
866
- OR: "OR",
867
- EQUAL: "EQUALS",
868
- NOTEQUAL: "NOTEQUALS",
869
- GEQ: "GEQ",
870
- GREATER: "GREATER",
871
- LEQ: "LEQ",
872
- LESS: "LESS",
873
- IN: "IN",
874
- NOTIN: "NOTIN",
875
- MATCH: "MATCHES",
876
- NOTMATCH: "NOTMATCHES"
877
- },
878
- formal: {
879
- AND: "^",
880
- OR: "V"
881
- }
882
- };
883
- function addQuotesIfNecessary(value) {
884
- if (typeof value !== "string")
885
- return value.toString();
886
- if (value === "true")
887
- return '"true"';
888
- if (value === "false")
889
- return '"false"';
890
- if (!isNaN(Number(value)))
891
- return `"${value}"`;
892
- const escaped = value.replaceAll("\\", "\\\\");
893
- if (new RegExp(TOKEN_REGEX, "gi").test(escaped))
894
- return `"${escaped.replaceAll('"', "\\\"")}"`;
895
- return escaped;
896
- }
897
- function stringify(expression, opts = {}) {
898
- const {
899
- junctionNotation = "programmatic",
900
- comparisonNotation = "programmatic",
901
- alwaysParenthesize = false,
902
- compact = false,
903
- condenseBooleans = false
904
- } = opts;
905
- let string = "";
906
- switch (expression.type) {
907
- case "group":
908
- for (const constituent of expression.constituents) {
909
- if (string.length) {
910
- if (!compact || junctionNotation === "linguistic")
911
- string += " ";
912
- string += formats[junctionNotation][expression.operation];
913
- if (!compact || junctionNotation === "linguistic")
914
- string += " ";
915
- }
916
- if (alwaysParenthesize && constituent.type === "group" || expression.operation === "AND" && constituent.operation === "OR")
917
- string += DEFAULT_PAREN[0];
918
- string += stringify(constituent, opts);
919
- if (alwaysParenthesize && constituent.type === "group" || expression.operation === "AND" && constituent.operation === "OR")
920
- string += DEFAULT_PAREN[1];
921
- }
922
- break;
923
- case "condition":
924
- if (condenseBooleans && ["EQUAL", "NOTEQUAL"].includes(expression.operation) && typeof expression.value === "boolean") {
925
- const negative = expression.operation === "EQUAL" && !expression.value || expression.operation === "NOTEQUAL" && expression.value;
926
- string += `${negative ? DEFAULT_NEGATOR : ""}${expression.field}`;
927
- } else {
928
- string += expression.field;
929
- if (!compact || comparisonNotation === "linguistic")
930
- string += " ";
931
- string += formats[comparisonNotation][expression.operation];
932
- if (!compact || comparisonNotation === "linguistic")
933
- string += " ";
934
- if (Array.isArray(expression.value)) {
935
- const join = expression.value.map(addQuotesIfNecessary).join(compact ? DEFAULT_DELIMITER : DEFAULT_DELIMITER + " ");
936
- string += `${DEFAULT_BRACKET[0]}${join}${DEFAULT_BRACKET[1]}`;
937
- } else
938
- string += addQuotesIfNecessary(expression.value);
939
- }
940
- break;
941
- }
942
- return string;
943
- }
944
1071
  // src/dominput.ts
945
1072
  function getCursorIndex(element) {
946
1073
  const selection = window.getSelection();
@@ -973,7 +1100,8 @@ function setCursor(element, index) {
973
1100
  accumulated += length;
974
1101
  }
975
1102
  }
976
- function createDOMInput({ input, constraints, onUpdate, parseOnInitialize }) {
1103
+ function createDOMInput({ input, config, onUpdate, parseOnInitialize }) {
1104
+ const parser = new WizardParser(config);
977
1105
  const history = [];
978
1106
  let historyIndex = -1;
979
1107
  let savedCursor;
@@ -982,13 +1110,13 @@ function createDOMInput({ input, constraints, onUpdate, parseOnInitialize }) {
982
1110
  const text = input.textContent.replaceAll(`
983
1111
  `, "");
984
1112
  const endPadding = input.textContent?.match(/\s*$/)?.[0].length ?? 0;
985
- const newTokens = tokenize(text);
1113
+ const newTokens = parser.tokenize(text);
986
1114
  const lastToken = newTokens.at(-1);
987
- if (lastToken && lastToken.content in OPERATION_ALIAS_DICTIONARY && focused && (!endPadding && lastToken.content.match(/^[A-Za-z]+?$/)))
1115
+ if (lastToken && parser.resolveOperatorAlias(lastToken.content) && focused && (!endPadding && lastToken.content.match(/^[A-Za-z]+?$/)))
988
1116
  return;
989
1117
  const absoluteIndex = focused ? getCursorIndex(input) : 0;
990
1118
  observer.disconnect();
991
- let inArray;
1119
+ let activeArrayOpeningBracket;
992
1120
  let offset = 0;
993
1121
  for (let t = 0;t < newTokens.length; ++t) {
994
1122
  const token = newTokens[t];
@@ -1009,24 +1137,37 @@ function createDOMInput({ input, constraints, onUpdate, parseOnInitialize }) {
1009
1137
  const element = document.createElement("span");
1010
1138
  element.textContent = token.content;
1011
1139
  element.toggleAttribute("data-node", true);
1012
- if (token.content.match(QUOTE_EDGE_REGEX))
1013
- element.toggleAttribute("data-quoted", true);
1014
- if (!isNaN(Number(token.content)))
1015
- element.toggleAttribute("data-number", true);
1016
- if (PARENS.concat(BRACKETS).some((entry) => entry.includes(token.content))) {
1017
- if (inArray && BRACKETS.some(([o, c]) => inArray === o && token.content === c))
1018
- inArray = undefined;
1019
- if (!inArray)
1140
+ const partType = parser.getPartType(token.content, activeArrayOpeningBracket);
1141
+ token.partType = partType;
1142
+ switch (partType) {
1143
+ case "quoted":
1144
+ element.toggleAttribute("data-quoted", true);
1145
+ break;
1146
+ case "number":
1147
+ element.toggleAttribute("data-number", true);
1148
+ break;
1149
+ case "openingarraybracket":
1150
+ activeArrayOpeningBracket = token.content;
1151
+ case "openinggroupbracket":
1020
1152
  element.setAttribute("data-bracket", token.content);
1021
- if (!inArray && BRACKETS.some(([o]) => o === token.content))
1022
- inArray = token.content;
1023
- }
1024
- if (inArray && ARRAY_DELIMITERS.includes(token.content))
1025
- element.toggleAttribute("data-delimiter", true);
1026
- if (!inArray && NEGATORS.includes(token.content))
1027
- element.toggleAttribute("data-negation", true);
1028
- if (!inArray && token.content in OPERATION_ALIAS_DICTIONARY) {
1029
- element.setAttribute("data-operation", OPERATION_PURPOSE_DICTIONARY[OPERATION_ALIAS_DICTIONARY[token.content]]);
1153
+ break;
1154
+ case "closingarraybracket":
1155
+ activeArrayOpeningBracket = undefined;
1156
+ case "closinggroupbracket":
1157
+ element.setAttribute("data-bracket", token.content);
1158
+ break;
1159
+ case "arraydelimiter":
1160
+ element.toggleAttribute("data-delimiter", true);
1161
+ break;
1162
+ case "negator":
1163
+ element.toggleAttribute("data-negator", true);
1164
+ break;
1165
+ case "junctionoperator":
1166
+ element.setAttribute("data-operator", "junction");
1167
+ break;
1168
+ case "conditionoperator":
1169
+ element.setAttribute("data-operator", "condition");
1170
+ break;
1030
1171
  }
1031
1172
  const existing = input.childNodes.item(t + offset);
1032
1173
  if (existing)
@@ -1059,7 +1200,7 @@ function createDOMInput({ input, constraints, onUpdate, parseOnInitialize }) {
1059
1200
  }
1060
1201
  let result;
1061
1202
  try {
1062
- result = parse(newTokens, constraints);
1203
+ result = parser.parse(newTokens);
1063
1204
  input.removeAttribute("data-error-message");
1064
1205
  input.removeAttribute("data-error-start");
1065
1206
  input.removeAttribute("data-error-end");
@@ -1118,16 +1259,10 @@ function createDOMInput({ input, constraints, onUpdate, parseOnInitialize }) {
1118
1259
  };
1119
1260
  }
1120
1261
  export {
1121
- tokenize,
1122
- summarize,
1123
- stringify,
1124
- parse,
1125
1262
  executeAsKnex,
1126
1263
  createDOMInput,
1127
- QUOTE_EDGE_REGEX,
1264
+ WizardParser,
1265
+ TYPE_PRIORITY,
1128
1266
  ParseError,
1129
- OPERATION_PURPOSE_DICTIONARY,
1130
- OPERATION_ALIAS_DICTIONARY,
1131
- ConstraintError,
1132
- COMPARISON_TYPE_DICTIONARY
1267
+ ConstraintError
1133
1268
  };