wizard-ql 2.1.0 → 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/LICENSE +1 -1
- package/README.md +148 -50
- package/dist/dominput.d.ts +6 -6
- package/dist/errors.d.ts +4 -4
- package/dist/execute.d.ts +3 -2
- package/dist/index.d.ts +2 -4
- package/dist/index.js +935 -802
- package/dist/parser.d.ts +384 -0
- package/dist/regex.d.ts +23 -4
- package/dist/spec.d.ts +76 -161
- package/package.json +14 -6
- package/dist/parse.d.ts +0 -55
- package/dist/stringify.d.ts +0 -42
- package/dist/summarize.d.ts +0 -15
package/dist/index.js
CHANGED
|
@@ -1,89 +1,5 @@
|
|
|
1
1
|
// src/spec.ts
|
|
2
|
-
var QUOTES = ["'", '"', "`"];
|
|
3
|
-
var BRACKETS = [
|
|
4
|
-
["[", "]"],
|
|
5
|
-
["{", "}"]
|
|
6
|
-
];
|
|
7
|
-
var PARENS = [
|
|
8
|
-
["(", ")"]
|
|
9
|
-
];
|
|
10
|
-
var NEGATORS = ["!"];
|
|
11
|
-
var ARRAY_DELIMITERS = [","];
|
|
12
|
-
var OPERATION_ALIAS_DICTIONARY = {
|
|
13
|
-
AND: "AND",
|
|
14
|
-
"&&": "AND",
|
|
15
|
-
"&": "AND",
|
|
16
|
-
"^": "AND",
|
|
17
|
-
OR: "OR",
|
|
18
|
-
"||": "OR",
|
|
19
|
-
"|": "OR",
|
|
20
|
-
V: "OR",
|
|
21
|
-
GEQ: "GEQ",
|
|
22
|
-
">=": "GEQ",
|
|
23
|
-
"=>": "GEQ",
|
|
24
|
-
LEQ: "LEQ",
|
|
25
|
-
"<=": "LEQ",
|
|
26
|
-
"=<": "LEQ",
|
|
27
|
-
NOTEQUALS: "NOTEQUAL",
|
|
28
|
-
NOTEQUAL: "NOTEQUAL",
|
|
29
|
-
NEQ: "NOTEQUAL",
|
|
30
|
-
ISNT: "NOTEQUAL",
|
|
31
|
-
"!==": "NOTEQUAL",
|
|
32
|
-
"!=": "NOTEQUAL",
|
|
33
|
-
EQUALS: "EQUAL",
|
|
34
|
-
EQUAL: "EQUAL",
|
|
35
|
-
EQ: "EQUAL",
|
|
36
|
-
IS: "EQUAL",
|
|
37
|
-
"==": "EQUAL",
|
|
38
|
-
"=": "EQUAL",
|
|
39
|
-
LESS: "LESS",
|
|
40
|
-
"<": "LESS",
|
|
41
|
-
GREATER: "GREATER",
|
|
42
|
-
">": "GREATER",
|
|
43
|
-
MORE: "GREATER",
|
|
44
|
-
NOTIN: "NOTIN",
|
|
45
|
-
"!:": "NOTIN",
|
|
46
|
-
IN: "IN",
|
|
47
|
-
":": "IN",
|
|
48
|
-
NOTMATCHES: "NOTMATCH",
|
|
49
|
-
NOTMATCH: "NOTMATCH",
|
|
50
|
-
"!~": "NOTMATCH",
|
|
51
|
-
MATCHES: "MATCH",
|
|
52
|
-
MATCH: "MATCH",
|
|
53
|
-
"~": "MATCH"
|
|
54
|
-
};
|
|
55
|
-
var ALIASES = Object.keys(OPERATION_ALIAS_DICTIONARY);
|
|
56
|
-
var OPERATION_PURPOSE_DICTIONARY = {
|
|
57
|
-
AND: "junction",
|
|
58
|
-
OR: "junction",
|
|
59
|
-
EQUAL: "comparison",
|
|
60
|
-
NOTEQUAL: "comparison",
|
|
61
|
-
LESS: "comparison",
|
|
62
|
-
GREATER: "comparison",
|
|
63
|
-
GEQ: "comparison",
|
|
64
|
-
LEQ: "comparison",
|
|
65
|
-
IN: "comparison",
|
|
66
|
-
NOTIN: "comparison",
|
|
67
|
-
MATCH: "comparison",
|
|
68
|
-
NOTMATCH: "comparison"
|
|
69
|
-
};
|
|
70
|
-
var COMPARISON_TYPE_DICTIONARY = {
|
|
71
|
-
EQUAL: "primitive",
|
|
72
|
-
NOTEQUAL: "primitive",
|
|
73
|
-
GEQ: "numeric",
|
|
74
|
-
GREATER: "numeric",
|
|
75
|
-
LEQ: "numeric",
|
|
76
|
-
LESS: "numeric",
|
|
77
|
-
IN: "array",
|
|
78
|
-
NOTIN: "array",
|
|
79
|
-
MATCH: "string",
|
|
80
|
-
NOTMATCH: "string"
|
|
81
|
-
};
|
|
82
2
|
var TYPE_PRIORITY = ["boolean", "date", "number", "string"];
|
|
83
|
-
// src/regex.ts
|
|
84
|
-
var ESCAPE_REGEX = "(?<=(?<!\\\\)(?:\\\\\\\\)*)";
|
|
85
|
-
var QUOTE_REGEX_STR = `${ESCAPE_REGEX}(?<quote>${QUOTES.map((q) => RegExp.escape(q)).join("|")})(?<quotecontent>.*?)${ESCAPE_REGEX}\\k<quote>`;
|
|
86
|
-
|
|
87
3
|
// src/errors.ts
|
|
88
4
|
function constructHeader(startToken, startIndex, endToken, endIndex) {
|
|
89
5
|
let header = "Token #";
|
|
@@ -148,622 +64,963 @@ class ConstraintError extends Error {
|
|
|
148
64
|
}
|
|
149
65
|
}
|
|
150
66
|
|
|
151
|
-
// src/
|
|
152
|
-
var
|
|
153
|
-
var
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
trimmed = trimmed.trimStart();
|
|
159
|
-
const lengthDiff = trimmed.length - pretrimLength;
|
|
160
|
-
if (trimmed) {
|
|
161
|
-
const token = { content: trimmed, index: index - lengthDiff };
|
|
162
|
-
array.push(token);
|
|
163
|
-
return token;
|
|
164
|
-
}
|
|
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>`;
|
|
165
74
|
}
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
let subopenings = 0;
|
|
179
|
-
let endToken;
|
|
180
|
-
for (const submatch of matches) {
|
|
181
|
-
if (match[0] === submatch[0] || match[0] === submatch[0])
|
|
182
|
-
++subopenings;
|
|
183
|
-
if (BRACKETS.some(([o, c]) => match[0] === o && submatch[0] === c)) {
|
|
184
|
-
if (subopenings)
|
|
185
|
-
--subopenings;
|
|
186
|
-
else {
|
|
187
|
-
endToken = {
|
|
188
|
-
content: submatch[0],
|
|
189
|
-
index: submatch.index
|
|
190
|
-
};
|
|
191
|
-
break;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
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);
|
|
196
|
-
for (const subtoken of subtokens)
|
|
197
|
-
subtoken.index += match.index + 1;
|
|
198
|
-
tokens.push(...subtokens);
|
|
199
|
-
if (endToken) {
|
|
200
|
-
tokens.push(endToken);
|
|
201
|
-
lastMatchEnd = endToken.index + endToken.content.length;
|
|
202
|
-
} else {
|
|
203
|
-
lastMatchEnd = expression.length;
|
|
204
|
-
break;
|
|
205
|
-
}
|
|
206
|
-
continue;
|
|
207
|
-
} else {
|
|
208
|
-
pushSanitized(tokens, match.groups?.quotecontent !== undefined ? expression.slice(match.index, match.index + match[0].length) : match[0], match.index);
|
|
209
|
-
}
|
|
210
|
-
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;
|
|
211
87
|
}
|
|
212
|
-
|
|
213
|
-
return tokens;
|
|
88
|
+
return true;
|
|
214
89
|
}
|
|
215
|
-
function
|
|
216
|
-
|
|
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;
|
|
217
100
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
+
}
|
|
227
143
|
};
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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: "≇"
|
|
239
186
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
+
};
|
|
255
215
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
expression.operation = "EQUAL";
|
|
265
|
-
break;
|
|
266
|
-
case "GEQ":
|
|
267
|
-
expression.operation = "LESS";
|
|
268
|
-
break;
|
|
269
|
-
case "LESS":
|
|
270
|
-
expression.operation = "GEQ";
|
|
271
|
-
break;
|
|
272
|
-
case "LEQ":
|
|
273
|
-
expression.operation = "GREATER";
|
|
274
|
-
break;
|
|
275
|
-
case "GREATER":
|
|
276
|
-
expression.operation = "LEQ";
|
|
277
|
-
break;
|
|
278
|
-
case "IN":
|
|
279
|
-
expression.operation = "NOTIN";
|
|
280
|
-
break;
|
|
281
|
-
case "NOTIN":
|
|
282
|
-
expression.operation = "IN";
|
|
283
|
-
break;
|
|
284
|
-
case "MATCH":
|
|
285
|
-
expression.operation = "NOTMATCH";
|
|
286
|
-
break;
|
|
287
|
-
case "NOTMATCH":
|
|
288
|
-
expression.operation = "MATCH";
|
|
289
|
-
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);
|
|
290
224
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
function coerceType(processedToken, fieldTypes, operatorTypes, dateInterpreter) {
|
|
295
|
-
const { raw, unescaped } = processedToken;
|
|
296
|
-
let operatorHits = 0;
|
|
297
|
-
let fieldHits = 0;
|
|
298
|
-
for (const type of TYPE_PRIORITY) {
|
|
299
|
-
const operatorHit = operatorTypes.includes(type);
|
|
300
|
-
const fieldHit = fieldTypes.includes(type);
|
|
301
|
-
if (operatorHit)
|
|
302
|
-
++operatorHits;
|
|
303
|
-
if (fieldHit)
|
|
304
|
-
++fieldHits;
|
|
305
|
-
if (!operatorHit || !fieldHit)
|
|
306
|
-
continue;
|
|
307
|
-
switch (type) {
|
|
308
|
-
case "boolean":
|
|
309
|
-
if (raw === "true")
|
|
310
|
-
return true;
|
|
311
|
-
else if (raw === "false")
|
|
312
|
-
return false;
|
|
313
|
-
break;
|
|
314
|
-
case "number": {
|
|
315
|
-
const num = Number(raw);
|
|
316
|
-
if (isNaN(num))
|
|
317
|
-
break;
|
|
318
|
-
return num;
|
|
225
|
+
if (config.restricted) {
|
|
226
|
+
for (const key in config.restricted)
|
|
227
|
+
this.FIELD_LIST.set(key.toLowerCase(), key);
|
|
319
228
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
+
}
|
|
328
264
|
}
|
|
329
265
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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);
|
|
334
272
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
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
|
+
}
|
|
365
317
|
}
|
|
366
318
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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;
|
|
372
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);
|
|
373
333
|
}
|
|
334
|
+
lastMatchEnd = match.index + match[0].length;
|
|
374
335
|
}
|
|
375
|
-
|
|
336
|
+
WizardParser.pushSanitized(tokens, expression.slice(lastMatchEnd ?? 0), lastMatchEnd ?? 0);
|
|
337
|
+
return tokens;
|
|
376
338
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
opTypes = ["boolean"];
|
|
387
|
-
else {
|
|
388
|
-
switch (operationType) {
|
|
389
|
-
case "primitive":
|
|
390
|
-
case "array":
|
|
391
|
-
opTypes = ["boolean", "number", "string"];
|
|
392
|
-
if (types?.includes("date"))
|
|
393
|
-
opTypes.push("date");
|
|
394
|
-
break;
|
|
395
|
-
case "numeric":
|
|
396
|
-
opTypes = ["number"];
|
|
397
|
-
if (types?.includes("date"))
|
|
398
|
-
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;
|
|
399
348
|
break;
|
|
400
|
-
case
|
|
401
|
-
|
|
349
|
+
case closing:
|
|
350
|
+
--openingCount;
|
|
402
351
|
break;
|
|
403
352
|
}
|
|
353
|
+
if (openingCount === 0)
|
|
354
|
+
return index;
|
|
404
355
|
}
|
|
405
|
-
|
|
406
|
-
const value = coerceType(v, types ?? opTypes, opTypes, dateInterpreter);
|
|
407
|
-
if (Array.isArray(condition.value))
|
|
408
|
-
condition.value[i] = value;
|
|
409
|
-
else
|
|
410
|
-
condition.value = value;
|
|
411
|
-
} catch (err) {
|
|
412
|
-
switch (err.message) {
|
|
413
|
-
case "operator":
|
|
414
|
-
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);
|
|
415
|
-
case "field":
|
|
416
|
-
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);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
if (types)
|
|
421
|
-
validated = true;
|
|
422
|
-
const edit = condition;
|
|
423
|
-
edit.field = field;
|
|
424
|
-
edit.validated = validated;
|
|
425
|
-
return edit;
|
|
426
|
-
}
|
|
427
|
-
function _parse(tokens, _offset, constraints) {
|
|
428
|
-
let field;
|
|
429
|
-
let comparisonOperation;
|
|
430
|
-
let value;
|
|
431
|
-
let inConjunction = false;
|
|
432
|
-
let groupOperation;
|
|
433
|
-
let expectingExpression = true;
|
|
434
|
-
const expressions = [];
|
|
435
|
-
function getExpressionGroup(ctx) {
|
|
436
|
-
if (inConjunction) {
|
|
437
|
-
const prior = expressions.at(-1);
|
|
438
|
-
if (!prior)
|
|
439
|
-
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);
|
|
440
|
-
if (prior.type !== "group" || prior.operation !== "AND")
|
|
441
|
-
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);
|
|
442
|
-
return prior.constituents;
|
|
443
|
-
}
|
|
444
|
-
return expressions;
|
|
356
|
+
return -1;
|
|
445
357
|
}
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
|
453
367
|
};
|
|
454
|
-
if (!ctx)
|
|
455
|
-
ctx = baseCtx;
|
|
456
|
-
const group = getExpressionGroup(ctx);
|
|
457
|
-
if (field && comparisonOperation && value) {
|
|
458
|
-
if (!expectingExpression)
|
|
459
|
-
throw new ParseError("Unexpected expression resolution before junctive operator", ctx.tokens, ctx.startToken, ctx.startIndex, ctx.endToken, ctx.endIndex);
|
|
460
|
-
group.push(validateCondition({
|
|
461
|
-
type: "condition",
|
|
462
|
-
field: field.content,
|
|
463
|
-
operation: comparisonOperation.content,
|
|
464
|
-
value: value.content
|
|
465
|
-
}, constraints, value.implicit, baseCtx));
|
|
466
|
-
inConjunction = false;
|
|
467
|
-
expectingExpression = false;
|
|
468
|
-
} else if (field && !comparisonOperation && !value) {
|
|
469
|
-
if (!expectingExpression)
|
|
470
|
-
throw new ParseError("Unexpected expression resolution before junctive operator", ctx.tokens, ctx.startToken, ctx.startIndex, ctx.endToken, ctx.endIndex);
|
|
471
|
-
group.push(validateCondition({
|
|
472
|
-
type: "condition",
|
|
473
|
-
field: field.content,
|
|
474
|
-
operation: "EQUAL",
|
|
475
|
-
value: "true"
|
|
476
|
-
}, constraints, true, baseCtx));
|
|
477
|
-
inConjunction = false;
|
|
478
|
-
expectingExpression = false;
|
|
479
|
-
} else if (field || comparisonOperation || value !== undefined)
|
|
480
|
-
throw new ParseError("Failed to resolve condition; missing operand or operator", ctx.tokens, ctx.startToken, ctx.startIndex, ctx.endToken, ctx.endIndex);
|
|
481
|
-
field = undefined;
|
|
482
|
-
comparisonOperation = undefined;
|
|
483
|
-
value = undefined;
|
|
484
368
|
}
|
|
485
|
-
|
|
486
|
-
const
|
|
487
|
-
if (
|
|
488
|
-
throw new
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const
|
|
500
|
-
if (
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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;
|
|
507
411
|
}
|
|
508
412
|
}
|
|
509
|
-
|
|
510
|
-
|
|
413
|
+
if (operatorTypes.includes(type))
|
|
414
|
+
--operatorHits;
|
|
415
|
+
if (fieldTypes.includes(type))
|
|
416
|
+
--fieldHits;
|
|
511
417
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
]
|
|
544
|
-
};
|
|
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
|
+
}
|
|
545
449
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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;
|
|
559
495
|
}
|
|
560
496
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
+
}
|
|
566
510
|
}
|
|
567
|
-
continue;
|
|
568
511
|
}
|
|
569
|
-
if (
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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]);
|
|
582
591
|
if (closingIndex === -1)
|
|
583
592
|
throw new ParseError("Missing closing parenthesis for group", tokens, token, _offset + t);
|
|
584
593
|
++t;
|
|
585
|
-
const
|
|
586
|
-
if (
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
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);
|
|
590
598
|
else {
|
|
591
|
-
const group = getExpressionGroup({
|
|
592
|
-
|
|
593
|
-
startToken: token,
|
|
594
|
-
startIndex: _offset + t,
|
|
595
|
-
endToken: tokens[closingIndex],
|
|
596
|
-
endIndex: closingIndex
|
|
597
|
-
});
|
|
598
|
-
group.push(futureSubExpression);
|
|
599
|
+
const group = getExpressionGroup({ tokens, startToken: token, startIndex: _offset + t });
|
|
600
|
+
group.push(subExpression);
|
|
599
601
|
inConjunction = false;
|
|
600
602
|
}
|
|
601
603
|
}
|
|
602
604
|
t = closingIndex;
|
|
603
605
|
continue;
|
|
604
606
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
if (op && OPERATION_PURPOSE_DICTIONARY[op] === "comparison") {
|
|
608
|
-
if (comparisonOperation || !field)
|
|
609
|
-
throw new ParseError("Unexpected comparison operator", tokens, field?.token ?? token, field?.index ?? _offset + t, token, _offset + t);
|
|
610
|
-
comparisonOperation = {
|
|
611
|
-
content: op,
|
|
612
|
-
token,
|
|
613
|
-
index: _offset + t
|
|
614
|
-
};
|
|
615
|
-
continue;
|
|
616
|
-
} else if (field)
|
|
617
|
-
throw new ParseError("Expected a comparison operator", tokens, field.token, field.index, token, _offset + t);
|
|
618
|
-
}
|
|
619
|
-
if (!field) {
|
|
620
|
-
if (NEGATORS.includes(token.content)) {
|
|
621
|
-
const nextToken = tokens[t + 1];
|
|
622
|
-
if (!nextToken)
|
|
623
|
-
throw new ParseError('Unexpected "!"', tokens, token, _offset + t);
|
|
607
|
+
const op = this.OPERATOR_DICTIONARY[token.content];
|
|
608
|
+
if (op?.type === "sumjunction" || op?.type === "productjunction") {
|
|
624
609
|
resolveCondition({
|
|
625
610
|
tokens,
|
|
626
|
-
startToken: token,
|
|
627
|
-
startIndex: _offset + t,
|
|
628
|
-
endToken:
|
|
629
|
-
endIndex: _offset + t
|
|
611
|
+
startToken: field?.token ?? token,
|
|
612
|
+
startIndex: field?.index ?? _offset + t,
|
|
613
|
+
endToken: token,
|
|
614
|
+
endIndex: _offset + t
|
|
630
615
|
});
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
+
}
|
|
644
821
|
resolveCondition({
|
|
645
822
|
tokens,
|
|
646
823
|
startToken: field.token,
|
|
647
824
|
startIndex: field.index,
|
|
648
|
-
endToken:
|
|
649
|
-
endIndex:
|
|
825
|
+
endToken: value.token ?? comparisonOperation?.token ?? field.token,
|
|
826
|
+
endIndex: value.index ?? comparisonOperation?.index ?? field.index
|
|
650
827
|
});
|
|
651
|
-
} else {
|
|
652
|
-
field = {
|
|
653
|
-
content: token.content,
|
|
654
|
-
token,
|
|
655
|
-
index: _offset + t
|
|
656
|
-
};
|
|
657
828
|
}
|
|
658
|
-
continue;
|
|
659
829
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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 += " ";
|
|
706
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];
|
|
707
915
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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);
|
|
711
977
|
}
|
|
712
|
-
t = closingIndex;
|
|
713
978
|
} else {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
+
});
|
|
719
985
|
}
|
|
720
|
-
resolveCondition({
|
|
721
|
-
tokens,
|
|
722
|
-
startToken: field.token,
|
|
723
|
-
startIndex: field.index,
|
|
724
|
-
endToken: value.token ?? comparisonOperation?.token ?? field.token,
|
|
725
|
-
endIndex: value.index ?? comparisonOperation?.index ?? field.index
|
|
726
|
-
});
|
|
727
986
|
}
|
|
987
|
+
return summary;
|
|
728
988
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
if (expressions.length === 1)
|
|
740
|
-
throw new ParseError("Dangling junction operator", tokens, tokens.at(-1), _offset + tokens.length - 1);
|
|
741
|
-
return {
|
|
742
|
-
type: "group",
|
|
743
|
-
operation: groupOperation,
|
|
744
|
-
constituents: expressions
|
|
745
|
-
};
|
|
746
|
-
} else if (expressions.length > 1)
|
|
747
|
-
throw new ParseError("Group possesses multiple conditions without disjunctive operators", tokens, tokens[0], _offset);
|
|
748
|
-
else
|
|
749
|
-
return expressions[0] ?? null;
|
|
750
|
-
}
|
|
751
|
-
function parse(expression, constraints) {
|
|
752
|
-
let tokens;
|
|
753
|
-
if (Array.isArray(expression)) {
|
|
754
|
-
tokens = [];
|
|
755
|
-
let type;
|
|
756
|
-
for (let t = 0;t < expression.length; ++t) {
|
|
757
|
-
const token = expression[t];
|
|
758
|
-
if (!type)
|
|
759
|
-
type = typeof token;
|
|
760
|
-
if (typeof token !== type)
|
|
761
|
-
console.warn("WizardQL: parse was called with a mixed array of string tokens and token objects");
|
|
762
|
-
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";
|
|
763
999
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
+
}
|
|
767
1024
|
}
|
|
768
1025
|
// src/execute.ts
|
|
769
1026
|
function executeAsKnex(query, expression) {
|
|
@@ -811,138 +1068,6 @@ function executeAsKnex(query, expression) {
|
|
|
811
1068
|
}
|
|
812
1069
|
}
|
|
813
1070
|
}
|
|
814
|
-
// src/summarize.ts
|
|
815
|
-
function getOrPutArrayInMap(map, key) {
|
|
816
|
-
const existingEntry = map.get(key);
|
|
817
|
-
if (existingEntry)
|
|
818
|
-
return existingEntry;
|
|
819
|
-
else {
|
|
820
|
-
const arr = [];
|
|
821
|
-
map.set(key, arr);
|
|
822
|
-
return arr;
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
function summarize(expressions) {
|
|
826
|
-
const array = Array.isArray(expressions) ? expressions : [expressions];
|
|
827
|
-
const summary = new Map;
|
|
828
|
-
for (const expression of array) {
|
|
829
|
-
if (expression.type === "group") {
|
|
830
|
-
const constituents = summarize(expression.constituents);
|
|
831
|
-
for (const [field, values] of constituents.entries()) {
|
|
832
|
-
const collection = getOrPutArrayInMap(summary, field);
|
|
833
|
-
collection.push(...values);
|
|
834
|
-
}
|
|
835
|
-
} else {
|
|
836
|
-
const collection = getOrPutArrayInMap(summary, expression.field);
|
|
837
|
-
collection.push({
|
|
838
|
-
operation: expression.operation,
|
|
839
|
-
value: expression.value,
|
|
840
|
-
exclusionary: ["NOTEQUAL", "LESS", "GREATER", "NOTIN"].includes(expression.operation)
|
|
841
|
-
});
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
return summary;
|
|
845
|
-
}
|
|
846
|
-
// src/stringify.ts
|
|
847
|
-
var DEFAULT_BRACKET = BRACKETS[0];
|
|
848
|
-
var DEFAULT_PAREN = PARENS[0];
|
|
849
|
-
var DEFAULT_NEGATOR = NEGATORS[0];
|
|
850
|
-
var DEFAULT_DELIMITER = ARRAY_DELIMITERS[0];
|
|
851
|
-
var formats = {
|
|
852
|
-
programmatic: {
|
|
853
|
-
AND: "&",
|
|
854
|
-
OR: "|",
|
|
855
|
-
EQUAL: "=",
|
|
856
|
-
NOTEQUAL: "!=",
|
|
857
|
-
GEQ: ">=",
|
|
858
|
-
GREATER: ">",
|
|
859
|
-
LEQ: "<=",
|
|
860
|
-
LESS: "<",
|
|
861
|
-
IN: ":",
|
|
862
|
-
NOTIN: "!:",
|
|
863
|
-
MATCH: "~",
|
|
864
|
-
NOTMATCH: "!~"
|
|
865
|
-
},
|
|
866
|
-
linguistic: {
|
|
867
|
-
AND: "AND",
|
|
868
|
-
OR: "OR",
|
|
869
|
-
EQUAL: "EQUALS",
|
|
870
|
-
NOTEQUAL: "NOTEQUALS",
|
|
871
|
-
GEQ: "GEQ",
|
|
872
|
-
GREATER: "GREATER",
|
|
873
|
-
LEQ: "LEQ",
|
|
874
|
-
LESS: "LESS",
|
|
875
|
-
IN: "IN",
|
|
876
|
-
NOTIN: "NOTIN",
|
|
877
|
-
MATCH: "MATCHES",
|
|
878
|
-
NOTMATCH: "NOTMATCHES"
|
|
879
|
-
},
|
|
880
|
-
formal: {
|
|
881
|
-
AND: "^",
|
|
882
|
-
OR: "V"
|
|
883
|
-
}
|
|
884
|
-
};
|
|
885
|
-
function addQuotesIfNecessary(value) {
|
|
886
|
-
if (typeof value !== "string")
|
|
887
|
-
return value.toString();
|
|
888
|
-
if (value === "true")
|
|
889
|
-
return '"true"';
|
|
890
|
-
if (value === "false")
|
|
891
|
-
return '"false"';
|
|
892
|
-
if (!isNaN(Number(value)))
|
|
893
|
-
return `"${value}"`;
|
|
894
|
-
const escaped = value.replaceAll("\\", "\\\\");
|
|
895
|
-
if (new RegExp(TOKEN_REGEX, "gi").test(escaped))
|
|
896
|
-
return `"${escaped.replaceAll('"', "\\\"")}"`;
|
|
897
|
-
return escaped;
|
|
898
|
-
}
|
|
899
|
-
function stringify(expression, opts = {}) {
|
|
900
|
-
const {
|
|
901
|
-
junctionNotation = "programmatic",
|
|
902
|
-
comparisonNotation = "programmatic",
|
|
903
|
-
alwaysParenthesize = false,
|
|
904
|
-
compact = false,
|
|
905
|
-
condenseBooleans = false
|
|
906
|
-
} = opts;
|
|
907
|
-
let string = "";
|
|
908
|
-
switch (expression.type) {
|
|
909
|
-
case "group":
|
|
910
|
-
for (const constituent of expression.constituents) {
|
|
911
|
-
if (string.length) {
|
|
912
|
-
if (!compact || junctionNotation === "linguistic")
|
|
913
|
-
string += " ";
|
|
914
|
-
string += formats[junctionNotation][expression.operation];
|
|
915
|
-
if (!compact || junctionNotation === "linguistic")
|
|
916
|
-
string += " ";
|
|
917
|
-
}
|
|
918
|
-
if (alwaysParenthesize && constituent.type === "group" || expression.operation === "AND" && constituent.operation === "OR")
|
|
919
|
-
string += DEFAULT_PAREN[0];
|
|
920
|
-
string += stringify(constituent, opts);
|
|
921
|
-
if (alwaysParenthesize && constituent.type === "group" || expression.operation === "AND" && constituent.operation === "OR")
|
|
922
|
-
string += DEFAULT_PAREN[1];
|
|
923
|
-
}
|
|
924
|
-
break;
|
|
925
|
-
case "condition":
|
|
926
|
-
if (condenseBooleans && ["EQUAL", "NOTEQUAL"].includes(expression.operation) && typeof expression.value === "boolean") {
|
|
927
|
-
const negative = expression.operation === "EQUAL" && !expression.value || expression.operation === "NOTEQUAL" && expression.value;
|
|
928
|
-
string += `${negative ? DEFAULT_NEGATOR : ""}${expression.field}`;
|
|
929
|
-
} else {
|
|
930
|
-
string += expression.field;
|
|
931
|
-
if (!compact || comparisonNotation === "linguistic")
|
|
932
|
-
string += " ";
|
|
933
|
-
string += formats[comparisonNotation][expression.operation];
|
|
934
|
-
if (!compact || comparisonNotation === "linguistic")
|
|
935
|
-
string += " ";
|
|
936
|
-
if (Array.isArray(expression.value)) {
|
|
937
|
-
const join = expression.value.map(addQuotesIfNecessary).join(compact ? DEFAULT_DELIMITER : DEFAULT_DELIMITER + " ");
|
|
938
|
-
string += `${DEFAULT_BRACKET[0]}${join}${DEFAULT_BRACKET[1]}`;
|
|
939
|
-
} else
|
|
940
|
-
string += addQuotesIfNecessary(expression.value);
|
|
941
|
-
}
|
|
942
|
-
break;
|
|
943
|
-
}
|
|
944
|
-
return string;
|
|
945
|
-
}
|
|
946
1071
|
// src/dominput.ts
|
|
947
1072
|
function getCursorIndex(element) {
|
|
948
1073
|
const selection = window.getSelection();
|
|
@@ -975,7 +1100,8 @@ function setCursor(element, index) {
|
|
|
975
1100
|
accumulated += length;
|
|
976
1101
|
}
|
|
977
1102
|
}
|
|
978
|
-
function createDOMInput({ input,
|
|
1103
|
+
function createDOMInput({ input, config, onUpdate, parseOnInitialize }) {
|
|
1104
|
+
const parser = new WizardParser(config);
|
|
979
1105
|
const history = [];
|
|
980
1106
|
let historyIndex = -1;
|
|
981
1107
|
let savedCursor;
|
|
@@ -984,13 +1110,13 @@ function createDOMInput({ input, constraints, onUpdate, parseOnInitialize }) {
|
|
|
984
1110
|
const text = input.textContent.replaceAll(`
|
|
985
1111
|
`, "");
|
|
986
1112
|
const endPadding = input.textContent?.match(/\s*$/)?.[0].length ?? 0;
|
|
987
|
-
const newTokens = tokenize(text);
|
|
1113
|
+
const newTokens = parser.tokenize(text);
|
|
988
1114
|
const lastToken = newTokens.at(-1);
|
|
989
|
-
if (lastToken && lastToken.content
|
|
1115
|
+
if (lastToken && parser.resolveOperatorAlias(lastToken.content) && focused && (!endPadding && lastToken.content.match(/^[A-Za-z]+?$/)))
|
|
990
1116
|
return;
|
|
991
1117
|
const absoluteIndex = focused ? getCursorIndex(input) : 0;
|
|
992
1118
|
observer.disconnect();
|
|
993
|
-
let
|
|
1119
|
+
let activeArrayOpeningBracket;
|
|
994
1120
|
let offset = 0;
|
|
995
1121
|
for (let t = 0;t < newTokens.length; ++t) {
|
|
996
1122
|
const token = newTokens[t];
|
|
@@ -1011,24 +1137,37 @@ function createDOMInput({ input, constraints, onUpdate, parseOnInitialize }) {
|
|
|
1011
1137
|
const element = document.createElement("span");
|
|
1012
1138
|
element.textContent = token.content;
|
|
1013
1139
|
element.toggleAttribute("data-node", true);
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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":
|
|
1022
1152
|
element.setAttribute("data-bracket", token.content);
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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;
|
|
1032
1171
|
}
|
|
1033
1172
|
const existing = input.childNodes.item(t + offset);
|
|
1034
1173
|
if (existing)
|
|
@@ -1061,7 +1200,7 @@ function createDOMInput({ input, constraints, onUpdate, parseOnInitialize }) {
|
|
|
1061
1200
|
}
|
|
1062
1201
|
let result;
|
|
1063
1202
|
try {
|
|
1064
|
-
result = parse(newTokens
|
|
1203
|
+
result = parser.parse(newTokens);
|
|
1065
1204
|
input.removeAttribute("data-error-message");
|
|
1066
1205
|
input.removeAttribute("data-error-start");
|
|
1067
1206
|
input.removeAttribute("data-error-end");
|
|
@@ -1120,16 +1259,10 @@ function createDOMInput({ input, constraints, onUpdate, parseOnInitialize }) {
|
|
|
1120
1259
|
};
|
|
1121
1260
|
}
|
|
1122
1261
|
export {
|
|
1123
|
-
tokenize,
|
|
1124
|
-
summarize,
|
|
1125
|
-
stringify,
|
|
1126
|
-
parse,
|
|
1127
1262
|
executeAsKnex,
|
|
1128
1263
|
createDOMInput,
|
|
1129
|
-
|
|
1264
|
+
WizardParser,
|
|
1265
|
+
TYPE_PRIORITY,
|
|
1130
1266
|
ParseError,
|
|
1131
|
-
|
|
1132
|
-
OPERATION_ALIAS_DICTIONARY,
|
|
1133
|
-
ConstraintError,
|
|
1134
|
-
COMPARISON_TYPE_DICTIONARY
|
|
1267
|
+
ConstraintError
|
|
1135
1268
|
};
|