@gblikas/querykit 0.1.0 → 0.3.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/.cursor/BUGBOT.md +65 -2
- package/README.md +163 -1
- package/dist/parser/index.d.ts +1 -0
- package/dist/parser/index.js +1 -0
- package/dist/parser/input-parser.d.ts +215 -0
- package/dist/parser/input-parser.js +493 -0
- package/dist/parser/parser.d.ts +148 -1
- package/dist/parser/parser.js +880 -6
- package/dist/parser/types.d.ts +432 -0
- package/examples/qk-next/app/page.tsx +6 -1
- package/package.json +1 -1
- package/src/parser/divergence.test.ts +357 -0
- package/src/parser/index.ts +2 -1
- package/src/parser/input-parser.test.ts +770 -0
- package/src/parser/input-parser.ts +697 -0
- package/src/parser/parse-with-context-suggestions.test.ts +360 -0
- package/src/parser/parse-with-context-validation.test.ts +447 -0
- package/src/parser/parse-with-context.test.ts +325 -0
- package/src/parser/parser.test.ts +209 -1
- package/src/parser/parser.ts +1106 -25
- package/src/parser/token-consistency.test.ts +341 -0
- package/src/parser/types.ts +545 -23
- package/examples/qk-next/pnpm-lock.yaml +0 -5623
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Input Parser for QueryKit
|
|
4
|
+
*
|
|
5
|
+
* This module provides utilities for parsing partial/in-progress query input
|
|
6
|
+
* from search bars, enabling features like:
|
|
7
|
+
* - Key-value highlighting
|
|
8
|
+
* - Autocomplete suggestions
|
|
9
|
+
* - Real-time validation feedback
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.parseQueryInput = parseQueryInput;
|
|
13
|
+
exports.getTermAtPosition = getTermAtPosition;
|
|
14
|
+
exports.isInputComplete = isInputComplete;
|
|
15
|
+
exports.extractKeyValue = extractKeyValue;
|
|
16
|
+
exports.parseQueryTokens = parseQueryTokens;
|
|
17
|
+
/**
|
|
18
|
+
* Regular expression patterns for parsing
|
|
19
|
+
*/
|
|
20
|
+
const PATTERNS = {
|
|
21
|
+
// Matches logical operators (AND, OR, NOT) with word boundaries
|
|
22
|
+
LOGICAL_OPERATOR: /\b(AND|OR|NOT)\b/gi,
|
|
23
|
+
// Matches comparison operators: :, :>, :>=, :<, :<=, :!=, :=
|
|
24
|
+
COMPARISON_OPERATOR: /^(:>=|:<=|:!=|:>|:<|:=|:)/,
|
|
25
|
+
// Matches a quoted string (single or double quotes)
|
|
26
|
+
QUOTED_STRING: /^(["'])(?:\\.|[^\\])*?\1/,
|
|
27
|
+
// Matches word characters and some special chars (for keys/values)
|
|
28
|
+
WORD_CHARS: /^[a-zA-Z0-9_.-]+/,
|
|
29
|
+
// Matches whitespace
|
|
30
|
+
WHITESPACE: /^\s+/,
|
|
31
|
+
// Matches parentheses
|
|
32
|
+
PAREN_OPEN: /^\(/,
|
|
33
|
+
PAREN_CLOSE: /^\)/,
|
|
34
|
+
// Matches negation prefix
|
|
35
|
+
NEGATION: /^-/
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Parse a single term (key:value, key:>value, or just value)
|
|
39
|
+
*/
|
|
40
|
+
function parseTerm(input, startPosition) {
|
|
41
|
+
if (!input || input.length === 0) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
let key = null;
|
|
45
|
+
let operator = null;
|
|
46
|
+
let value = null;
|
|
47
|
+
let remaining = input;
|
|
48
|
+
let currentPos = 0;
|
|
49
|
+
// Handle negation prefix (e.g., -status:active)
|
|
50
|
+
let hasNegation = false;
|
|
51
|
+
const negationMatch = remaining.match(PATTERNS.NEGATION);
|
|
52
|
+
if (negationMatch) {
|
|
53
|
+
hasNegation = true;
|
|
54
|
+
remaining = remaining.substring(1);
|
|
55
|
+
currentPos += 1;
|
|
56
|
+
}
|
|
57
|
+
// Try to match a key (word before operator)
|
|
58
|
+
const keyMatch = remaining.match(PATTERNS.WORD_CHARS);
|
|
59
|
+
if (keyMatch) {
|
|
60
|
+
const potentialKey = keyMatch[0];
|
|
61
|
+
const afterKey = remaining.substring(potentialKey.length);
|
|
62
|
+
// Check if followed by an operator
|
|
63
|
+
const operatorMatch = afterKey.match(PATTERNS.COMPARISON_OPERATOR);
|
|
64
|
+
if (operatorMatch) {
|
|
65
|
+
// This is a key:value pattern
|
|
66
|
+
key = (hasNegation ? '-' : '') + potentialKey;
|
|
67
|
+
operator = operatorMatch[0];
|
|
68
|
+
currentPos += potentialKey.length + operator.length;
|
|
69
|
+
remaining = afterKey.substring(operator.length);
|
|
70
|
+
// Try to match the value
|
|
71
|
+
// First check for quoted string
|
|
72
|
+
const quotedMatch = remaining.match(PATTERNS.QUOTED_STRING);
|
|
73
|
+
if (quotedMatch) {
|
|
74
|
+
value = quotedMatch[0];
|
|
75
|
+
currentPos += value.length;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// Match unquoted value (until whitespace or logical operator)
|
|
79
|
+
const valueMatch = remaining.match(/^[^\s()]+/);
|
|
80
|
+
if (valueMatch) {
|
|
81
|
+
value = valueMatch[0];
|
|
82
|
+
currentPos += value.length;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Operator present but no value yet
|
|
86
|
+
value = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// No operator - this is a bare value (or incomplete key)
|
|
92
|
+
// Treat the whole thing as a potential key that could become key:value
|
|
93
|
+
// or as a bare value for full-text search
|
|
94
|
+
key = null;
|
|
95
|
+
operator = null;
|
|
96
|
+
value = (hasNegation ? '-' : '') + potentialKey;
|
|
97
|
+
currentPos += potentialKey.length;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// Check for quoted string as bare value
|
|
102
|
+
const quotedMatch = remaining.match(PATTERNS.QUOTED_STRING);
|
|
103
|
+
if (quotedMatch) {
|
|
104
|
+
key = null;
|
|
105
|
+
operator = null;
|
|
106
|
+
value = (hasNegation ? '-' : '') + quotedMatch[0];
|
|
107
|
+
currentPos += quotedMatch[0].length;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// No recognizable token
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
key,
|
|
116
|
+
operator,
|
|
117
|
+
value,
|
|
118
|
+
startPosition,
|
|
119
|
+
endPosition: startPosition + currentPos,
|
|
120
|
+
raw: input.substring(0, currentPos)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Tokenize the input string into terms and logical operators
|
|
125
|
+
*/
|
|
126
|
+
function tokenize(input) {
|
|
127
|
+
const terms = [];
|
|
128
|
+
const logicalOperators = [];
|
|
129
|
+
let remaining = input;
|
|
130
|
+
let position = 0;
|
|
131
|
+
while (remaining.length > 0) {
|
|
132
|
+
// Skip whitespace
|
|
133
|
+
const wsMatch = remaining.match(PATTERNS.WHITESPACE);
|
|
134
|
+
if (wsMatch) {
|
|
135
|
+
position += wsMatch[0].length;
|
|
136
|
+
remaining = remaining.substring(wsMatch[0].length);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
// Skip parentheses (they're structural, not terms)
|
|
140
|
+
if (remaining.match(PATTERNS.PAREN_OPEN)) {
|
|
141
|
+
position += 1;
|
|
142
|
+
remaining = remaining.substring(1);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (remaining.match(PATTERNS.PAREN_CLOSE)) {
|
|
146
|
+
position += 1;
|
|
147
|
+
remaining = remaining.substring(1);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
// Check for logical operators
|
|
151
|
+
const logicalMatch = remaining.match(/^(AND|OR|NOT)\b/i);
|
|
152
|
+
if (logicalMatch) {
|
|
153
|
+
logicalOperators.push({
|
|
154
|
+
operator: logicalMatch[0].toUpperCase(),
|
|
155
|
+
position
|
|
156
|
+
});
|
|
157
|
+
position += logicalMatch[0].length;
|
|
158
|
+
remaining = remaining.substring(logicalMatch[0].length);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
// Try to parse a term
|
|
162
|
+
const term = parseTerm(remaining, position);
|
|
163
|
+
if (term) {
|
|
164
|
+
terms.push(term);
|
|
165
|
+
position = term.endPosition;
|
|
166
|
+
remaining = input.substring(position);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
// Skip unknown character
|
|
170
|
+
position += 1;
|
|
171
|
+
remaining = remaining.substring(1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { terms, logicalOperators };
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Determine the cursor context based on position within a term
|
|
178
|
+
*/
|
|
179
|
+
function determineCursorContext(term, cursorPosition) {
|
|
180
|
+
const relativePos = cursorPosition - term.startPosition;
|
|
181
|
+
if (term.key !== null && term.operator !== null) {
|
|
182
|
+
// Key and operator are present
|
|
183
|
+
const keyLength = term.key.length;
|
|
184
|
+
const operatorLength = term.operator.length;
|
|
185
|
+
const keyPlusOperatorLength = keyLength + operatorLength;
|
|
186
|
+
if (relativePos < keyLength) {
|
|
187
|
+
return 'key';
|
|
188
|
+
}
|
|
189
|
+
else if (relativePos < keyPlusOperatorLength) {
|
|
190
|
+
return 'operator';
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// Cursor is at or after the operator - this is the value position
|
|
194
|
+
// Even if value is null (user hasn't typed anything yet),
|
|
195
|
+
// they're positioned to type a value
|
|
196
|
+
return 'value';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else if (term.key !== null) {
|
|
200
|
+
// Only key present (incomplete term)
|
|
201
|
+
return 'key';
|
|
202
|
+
}
|
|
203
|
+
else if (term.value !== null) {
|
|
204
|
+
// Only value present (bare value)
|
|
205
|
+
return 'value';
|
|
206
|
+
}
|
|
207
|
+
return 'empty';
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Find the term that contains the cursor position
|
|
211
|
+
*/
|
|
212
|
+
function findActiveTermAndContext(terms, cursorPosition, inputLength) {
|
|
213
|
+
if (cursorPosition === null) {
|
|
214
|
+
// If no cursor position provided, use the last term
|
|
215
|
+
if (terms.length > 0) {
|
|
216
|
+
const lastTerm = terms[terms.length - 1];
|
|
217
|
+
return {
|
|
218
|
+
activeTerm: lastTerm,
|
|
219
|
+
cursorContext: determineCursorContext(lastTerm, lastTerm.endPosition)
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return { activeTerm: null, cursorContext: 'empty' };
|
|
223
|
+
}
|
|
224
|
+
// Find term containing cursor
|
|
225
|
+
for (const term of terms) {
|
|
226
|
+
if (cursorPosition >= term.startPosition &&
|
|
227
|
+
cursorPosition <= term.endPosition) {
|
|
228
|
+
return {
|
|
229
|
+
activeTerm: term,
|
|
230
|
+
cursorContext: determineCursorContext(term, cursorPosition)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Cursor is between terms or at the end
|
|
235
|
+
if (cursorPosition >= inputLength && terms.length > 0) {
|
|
236
|
+
// Cursor at the end - check if right after a term
|
|
237
|
+
const lastTerm = terms[terms.length - 1];
|
|
238
|
+
if (cursorPosition === lastTerm.endPosition) {
|
|
239
|
+
return {
|
|
240
|
+
activeTerm: lastTerm,
|
|
241
|
+
cursorContext: determineCursorContext(lastTerm, cursorPosition)
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return { activeTerm: null, cursorContext: 'between' };
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Parse query input to extract structured information about the current search state.
|
|
249
|
+
*
|
|
250
|
+
* This function is designed for real-time parsing of user input in a search bar,
|
|
251
|
+
* allowing developers to:
|
|
252
|
+
* - Highlight keys and values differently
|
|
253
|
+
* - Provide autocomplete suggestions based on context
|
|
254
|
+
* - Validate input as the user types
|
|
255
|
+
*
|
|
256
|
+
* @param input The current search input string
|
|
257
|
+
* @param cursorPosition Optional cursor position to determine the active term
|
|
258
|
+
* @param options Optional parsing options
|
|
259
|
+
* @returns Structured information about the query input
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* ```typescript
|
|
263
|
+
* // User is typing "status:d" (intending to type "status:done")
|
|
264
|
+
* const result = parseQueryInput('status:d');
|
|
265
|
+
* // result.terms[0] = { key: 'status', operator: ':', value: 'd', ... }
|
|
266
|
+
* // result.activeTerm = { key: 'status', operator: ':', value: 'd', ... }
|
|
267
|
+
* // result.cursorContext = 'value'
|
|
268
|
+
*
|
|
269
|
+
* // User is typing "priority:>2 status:"
|
|
270
|
+
* const result = parseQueryInput('priority:>2 status:', 19);
|
|
271
|
+
* // result.terms[0] = { key: 'priority', operator: ':>', value: '2', ... }
|
|
272
|
+
* // result.terms[1] = { key: 'status', operator: ':', value: null, ... }
|
|
273
|
+
* // result.activeTerm = result.terms[1] (cursor is at position 19)
|
|
274
|
+
* // result.cursorContext = 'value' (waiting for value input)
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
function parseQueryInput(input, cursorPosition, options) {
|
|
278
|
+
// Handle empty input
|
|
279
|
+
if (!input || input.trim().length === 0) {
|
|
280
|
+
return {
|
|
281
|
+
terms: [],
|
|
282
|
+
activeTerm: null,
|
|
283
|
+
cursorContext: 'empty',
|
|
284
|
+
input,
|
|
285
|
+
cursorPosition: cursorPosition ?? null,
|
|
286
|
+
logicalOperators: []
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// Tokenize the input
|
|
290
|
+
const { terms, logicalOperators } = tokenize(input);
|
|
291
|
+
// Apply case-insensitivity to keys if requested
|
|
292
|
+
if (options?.caseInsensitiveKeys) {
|
|
293
|
+
for (const term of terms) {
|
|
294
|
+
if (term.key !== null) {
|
|
295
|
+
term.key = term.key.toLowerCase();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Find active term and cursor context
|
|
300
|
+
const { activeTerm, cursorContext } = findActiveTermAndContext(terms, cursorPosition ?? null, input.length);
|
|
301
|
+
return {
|
|
302
|
+
terms,
|
|
303
|
+
activeTerm,
|
|
304
|
+
cursorContext,
|
|
305
|
+
input,
|
|
306
|
+
cursorPosition: cursorPosition ?? null,
|
|
307
|
+
logicalOperators
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Get the term at a specific cursor position.
|
|
312
|
+
* Convenience function for quick lookups.
|
|
313
|
+
*
|
|
314
|
+
* @param input The query input string
|
|
315
|
+
* @param cursorPosition The cursor position
|
|
316
|
+
* @returns The term at the cursor position, or null if none
|
|
317
|
+
*/
|
|
318
|
+
function getTermAtPosition(input, cursorPosition) {
|
|
319
|
+
const result = parseQueryInput(input, cursorPosition);
|
|
320
|
+
return result.activeTerm;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Check if the input appears to be a complete, valid query expression.
|
|
324
|
+
* This is a lightweight check - it doesn't guarantee the query will parse successfully.
|
|
325
|
+
*
|
|
326
|
+
* @param input The query input string
|
|
327
|
+
* @returns true if the input appears complete, false if it looks incomplete
|
|
328
|
+
*/
|
|
329
|
+
function isInputComplete(input) {
|
|
330
|
+
if (!input || input.trim().length === 0) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
const result = parseQueryInput(input);
|
|
334
|
+
// Check if any term is incomplete
|
|
335
|
+
for (const term of result.terms) {
|
|
336
|
+
// A key:value term is incomplete if it has an operator but no value
|
|
337
|
+
if (term.key !== null && term.operator !== null && term.value === null) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Check if the input ends with a logical operator
|
|
342
|
+
const trimmed = input.trim();
|
|
343
|
+
if (/\b(AND|OR|NOT)\s*$/i.test(trimmed)) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
// Check if there's an unclosed quote
|
|
347
|
+
const singleQuotes = (input.match(/'/g) || []).length;
|
|
348
|
+
const doubleQuotes = (input.match(/"/g) || []).length;
|
|
349
|
+
if (singleQuotes % 2 !== 0 || doubleQuotes % 2 !== 0) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
// Check for unclosed parentheses
|
|
353
|
+
const openParens = (input.match(/\(/g) || []).length;
|
|
354
|
+
const closeParens = (input.match(/\)/g) || []).length;
|
|
355
|
+
if (openParens !== closeParens) {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Extract just the key and value from a simple input.
|
|
362
|
+
* Convenience function for the most common use case.
|
|
363
|
+
*
|
|
364
|
+
* @param input The query input string (e.g., "status:done")
|
|
365
|
+
* @returns Object with key and value, or null if not a key:value pattern
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* ```typescript
|
|
369
|
+
* extractKeyValue('status:done');
|
|
370
|
+
* // { key: 'status', value: 'done' }
|
|
371
|
+
*
|
|
372
|
+
* extractKeyValue('status:');
|
|
373
|
+
* // { key: 'status', value: null }
|
|
374
|
+
*
|
|
375
|
+
* extractKeyValue('hello');
|
|
376
|
+
* // null (no key:value pattern)
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
function extractKeyValue(input) {
|
|
380
|
+
const result = parseQueryInput(input.trim());
|
|
381
|
+
if (result.terms.length === 0) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
const term = result.terms[0];
|
|
385
|
+
if (term.key === null) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
key: term.key,
|
|
390
|
+
value: term.value
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Parse query input into an interleaved sequence of terms and operators.
|
|
395
|
+
*
|
|
396
|
+
* This provides a flat, ordered representation ideal for:
|
|
397
|
+
* - Rendering query tokens as UI chips/tags
|
|
398
|
+
* - Building visual query builders
|
|
399
|
+
* - Syntax highlighting with proper ordering
|
|
400
|
+
*
|
|
401
|
+
* @param input The query input string
|
|
402
|
+
* @param cursorPosition Optional cursor position to identify active token
|
|
403
|
+
* @returns Ordered sequence of term and operator tokens
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* ```typescript
|
|
407
|
+
* const result = parseQueryTokens('status:done AND priority:high');
|
|
408
|
+
* // result.tokens = [
|
|
409
|
+
* // { type: 'term', key: 'status', value: 'done', ... },
|
|
410
|
+
* // { type: 'operator', operator: 'AND', ... },
|
|
411
|
+
* // { type: 'term', key: 'priority', value: 'high', ... }
|
|
412
|
+
* // ]
|
|
413
|
+
*
|
|
414
|
+
* // For incomplete input like 'status:d'
|
|
415
|
+
* const result = parseQueryTokens('status:d');
|
|
416
|
+
* // result.tokens = [
|
|
417
|
+
* // { type: 'term', key: 'status', value: 'd', ... }
|
|
418
|
+
* // ]
|
|
419
|
+
* ```
|
|
420
|
+
*/
|
|
421
|
+
function parseQueryTokens(input, cursorPosition) {
|
|
422
|
+
if (!input || input.trim().length === 0) {
|
|
423
|
+
return {
|
|
424
|
+
tokens: [],
|
|
425
|
+
input,
|
|
426
|
+
activeToken: null,
|
|
427
|
+
activeTokenIndex: -1
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const context = parseQueryInput(input, cursorPosition);
|
|
431
|
+
// Build a combined list of all tokens with their positions
|
|
432
|
+
const allTokens = [];
|
|
433
|
+
// Add terms
|
|
434
|
+
for (const term of context.terms) {
|
|
435
|
+
allTokens.push({
|
|
436
|
+
position: term.startPosition,
|
|
437
|
+
token: {
|
|
438
|
+
type: 'term',
|
|
439
|
+
key: term.key,
|
|
440
|
+
operator: term.operator,
|
|
441
|
+
value: term.value,
|
|
442
|
+
startPosition: term.startPosition,
|
|
443
|
+
endPosition: term.endPosition,
|
|
444
|
+
raw: term.raw
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
// Add operators
|
|
449
|
+
for (const op of context.logicalOperators) {
|
|
450
|
+
const opLength = op.operator.length;
|
|
451
|
+
allTokens.push({
|
|
452
|
+
position: op.position,
|
|
453
|
+
token: {
|
|
454
|
+
type: 'operator',
|
|
455
|
+
operator: op.operator,
|
|
456
|
+
startPosition: op.position,
|
|
457
|
+
endPosition: op.position + opLength,
|
|
458
|
+
raw: op.operator
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
// Sort by position to get the interleaved order
|
|
463
|
+
allTokens.sort((a, b) => a.position - b.position);
|
|
464
|
+
const tokens = allTokens.map(t => t.token);
|
|
465
|
+
// Find active token based on cursor position
|
|
466
|
+
let activeToken = null;
|
|
467
|
+
let activeTokenIndex = -1;
|
|
468
|
+
if (cursorPosition !== undefined) {
|
|
469
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
470
|
+
const token = tokens[i];
|
|
471
|
+
if (cursorPosition >= token.startPosition &&
|
|
472
|
+
cursorPosition <= token.endPosition) {
|
|
473
|
+
activeToken = token;
|
|
474
|
+
activeTokenIndex = i;
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// If cursor is at the end and right after the last token
|
|
479
|
+
if (activeToken === null && tokens.length > 0) {
|
|
480
|
+
const lastToken = tokens[tokens.length - 1];
|
|
481
|
+
if (cursorPosition === lastToken.endPosition) {
|
|
482
|
+
activeToken = lastToken;
|
|
483
|
+
activeTokenIndex = tokens.length - 1;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
tokens,
|
|
489
|
+
input,
|
|
490
|
+
activeToken,
|
|
491
|
+
activeTokenIndex
|
|
492
|
+
};
|
|
493
|
+
}
|
package/dist/parser/parser.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IParserOptions, IQueryParser, QueryExpression } from './types';
|
|
1
|
+
import { IParserOptions, IParseWithContextOptions, IQueryParser, IQueryParseResult, QueryExpression } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Error thrown when query parsing fails
|
|
4
4
|
*/
|
|
@@ -15,6 +15,35 @@ export declare class QueryParser implements IQueryParser {
|
|
|
15
15
|
* Parse a query string into a QueryKit AST
|
|
16
16
|
*/
|
|
17
17
|
parse(query: string): QueryExpression;
|
|
18
|
+
/**
|
|
19
|
+
* Pre-process a query string to convert non-standard syntax to Liqe-compatible syntax.
|
|
20
|
+
* Supports:
|
|
21
|
+
* - `field:[val1, val2, val3]` → `(field:val1 OR field:val2 OR field:val3)`
|
|
22
|
+
*
|
|
23
|
+
* This keeps the syntax consistent with the `key:value` pattern used throughout QueryKit:
|
|
24
|
+
* - `priority:>2` (comparison)
|
|
25
|
+
* - `status:active` (equality)
|
|
26
|
+
* - `status:[todo, doing, done]` (IN / multiple values)
|
|
27
|
+
*/
|
|
28
|
+
private preprocessQuery;
|
|
29
|
+
/**
|
|
30
|
+
* Convert a field and comma-separated values to an OR expression string
|
|
31
|
+
*/
|
|
32
|
+
private convertToOrExpression;
|
|
33
|
+
/**
|
|
34
|
+
* Parse a comma-separated string into values, respecting quoted strings.
|
|
35
|
+
* Commas inside quoted strings are preserved as part of the value.
|
|
36
|
+
*
|
|
37
|
+
* Examples:
|
|
38
|
+
* - `a, b, c` → ['a', 'b', 'c']
|
|
39
|
+
* - `"John, Jr.", Jane` → ['"John, Jr."', 'Jane']
|
|
40
|
+
* - `'hello, world', test` → ["'hello, world'", 'test']
|
|
41
|
+
*/
|
|
42
|
+
private parseCommaSeparatedValues;
|
|
43
|
+
/**
|
|
44
|
+
* Format a field:value pair, quoting the value if necessary
|
|
45
|
+
*/
|
|
46
|
+
private formatFieldValue;
|
|
18
47
|
/**
|
|
19
48
|
* Validate a query string
|
|
20
49
|
*/
|
|
@@ -35,6 +64,11 @@ export declare class QueryParser implements IQueryParser {
|
|
|
35
64
|
* Create a comparison expression
|
|
36
65
|
*/
|
|
37
66
|
private createComparisonExpression;
|
|
67
|
+
/**
|
|
68
|
+
* Convert a Liqe RangeExpression to a QueryKit logical AND expression
|
|
69
|
+
* E.g., `field:[2 TO 5]` becomes `(field >= 2 AND field <= 5)`
|
|
70
|
+
*/
|
|
71
|
+
private convertRangeExpression;
|
|
38
72
|
/**
|
|
39
73
|
* Convert a Liqe operator to a QueryKit operator
|
|
40
74
|
*/
|
|
@@ -48,4 +82,117 @@ export declare class QueryParser implements IQueryParser {
|
|
|
48
82
|
* Normalize a field name based on parser options
|
|
49
83
|
*/
|
|
50
84
|
private normalizeFieldName;
|
|
85
|
+
/**
|
|
86
|
+
* Parse a query string with full context information.
|
|
87
|
+
*
|
|
88
|
+
* Unlike `parse()`, this method never throws. Instead, it returns a result object
|
|
89
|
+
* that indicates success or failure along with rich contextual information useful
|
|
90
|
+
* for building search UIs.
|
|
91
|
+
*
|
|
92
|
+
* @param query The query string to parse
|
|
93
|
+
* @param options Optional configuration (cursor position, etc.)
|
|
94
|
+
* @returns Rich parse result with tokens, AST/error, and structural analysis
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* const result = parser.parseWithContext('status:done AND priority:high');
|
|
99
|
+
*
|
|
100
|
+
* if (result.success) {
|
|
101
|
+
* // Use result.ast for query execution
|
|
102
|
+
* console.log('Valid query:', result.ast);
|
|
103
|
+
* } else {
|
|
104
|
+
* // Show error to user
|
|
105
|
+
* console.log('Error:', result.error?.message);
|
|
106
|
+
* }
|
|
107
|
+
*
|
|
108
|
+
* // Always available for UI rendering
|
|
109
|
+
* console.log('Tokens:', result.tokens);
|
|
110
|
+
* console.log('Structure:', result.structure);
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
parseWithContext(query: string, options?: IParseWithContextOptions): IQueryParseResult;
|
|
114
|
+
/**
|
|
115
|
+
* Convert tokens from input parser format to IQueryToken format
|
|
116
|
+
*/
|
|
117
|
+
private convertTokens;
|
|
118
|
+
/**
|
|
119
|
+
* Convert a single token from input parser format
|
|
120
|
+
*/
|
|
121
|
+
private convertSingleToken;
|
|
122
|
+
/**
|
|
123
|
+
* Analyze the structure of a query
|
|
124
|
+
*/
|
|
125
|
+
private analyzeStructure;
|
|
126
|
+
/**
|
|
127
|
+
* Calculate the maximum nesting depth of parentheses
|
|
128
|
+
*/
|
|
129
|
+
private calculateDepth;
|
|
130
|
+
/**
|
|
131
|
+
* Determine query complexity classification
|
|
132
|
+
*/
|
|
133
|
+
private determineComplexity;
|
|
134
|
+
/**
|
|
135
|
+
* Try to extract error position from error message
|
|
136
|
+
*/
|
|
137
|
+
private extractErrorPosition;
|
|
138
|
+
/**
|
|
139
|
+
* Try to extract the problematic text from the query based on error
|
|
140
|
+
*/
|
|
141
|
+
private extractProblematicText;
|
|
142
|
+
/**
|
|
143
|
+
* Validate fields against the provided schema
|
|
144
|
+
*/
|
|
145
|
+
private validateFields;
|
|
146
|
+
/**
|
|
147
|
+
* Find a similar field name (for typo suggestions)
|
|
148
|
+
*/
|
|
149
|
+
private findSimilarField;
|
|
150
|
+
/**
|
|
151
|
+
* Calculate Levenshtein distance between two strings
|
|
152
|
+
*/
|
|
153
|
+
private levenshteinDistance;
|
|
154
|
+
/**
|
|
155
|
+
* Perform security pre-check against the provided options
|
|
156
|
+
*/
|
|
157
|
+
private performSecurityCheck;
|
|
158
|
+
/**
|
|
159
|
+
* Generate autocomplete suggestions based on cursor position
|
|
160
|
+
*/
|
|
161
|
+
private generateAutocompleteSuggestions;
|
|
162
|
+
/**
|
|
163
|
+
* Suggest for empty/start context
|
|
164
|
+
*/
|
|
165
|
+
private suggestForEmptyContext;
|
|
166
|
+
/**
|
|
167
|
+
* Suggest between tokens (after a complete term)
|
|
168
|
+
*/
|
|
169
|
+
private suggestBetweenTokens;
|
|
170
|
+
/**
|
|
171
|
+
* Suggest field names
|
|
172
|
+
*/
|
|
173
|
+
private suggestFields;
|
|
174
|
+
/**
|
|
175
|
+
* Get field suggestions based on partial input
|
|
176
|
+
*/
|
|
177
|
+
private getFieldSuggestions;
|
|
178
|
+
/**
|
|
179
|
+
* Suggest operators
|
|
180
|
+
*/
|
|
181
|
+
private suggestOperators;
|
|
182
|
+
/**
|
|
183
|
+
* Get operator suggestions based on field type
|
|
184
|
+
*/
|
|
185
|
+
private getOperatorSuggestions;
|
|
186
|
+
/**
|
|
187
|
+
* Suggest values
|
|
188
|
+
*/
|
|
189
|
+
private suggestValues;
|
|
190
|
+
/**
|
|
191
|
+
* Get value suggestions based on schema
|
|
192
|
+
*/
|
|
193
|
+
private getValueSuggestions;
|
|
194
|
+
/**
|
|
195
|
+
* Generate error recovery suggestions
|
|
196
|
+
*/
|
|
197
|
+
private generateErrorRecovery;
|
|
51
198
|
}
|