@marianmeres/condition-parser 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/parser.d.ts +137 -8
- package/dist/parser.js +139 -12
- package/package.json +2 -2
package/dist/parser.d.ts
CHANGED
|
@@ -10,31 +10,160 @@ interface Meta {
|
|
|
10
10
|
values: any[];
|
|
11
11
|
expressions: ExpressionData[];
|
|
12
12
|
}
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Configuration options for the ConditionParser.
|
|
15
|
+
*/
|
|
14
16
|
export interface ConditionParserOptions {
|
|
17
|
+
/**
|
|
18
|
+
* The default operator to use when not explicitly specified in the expression.
|
|
19
|
+
* Defaults to "eq" (equals).
|
|
20
|
+
* @example "contains", "eq", "gt", etc.
|
|
21
|
+
*/
|
|
15
22
|
defaultOperator: string;
|
|
23
|
+
/**
|
|
24
|
+
* Enable debug logging to console. Useful for troubleshooting parser behavior.
|
|
25
|
+
* @default false
|
|
26
|
+
*/
|
|
16
27
|
debug: boolean;
|
|
17
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Transform function applied to each parsed expression before it's added to the output.
|
|
30
|
+
* Useful for normalizing keys/values or applying custom transformations.
|
|
31
|
+
* @param context - The parsed expression context
|
|
32
|
+
* @returns The transformed expression context
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* transform: (ctx) => ({
|
|
36
|
+
* ...ctx,
|
|
37
|
+
* key: ctx.key.toLowerCase(),
|
|
38
|
+
* value: ctx.value.toUpperCase()
|
|
39
|
+
* })
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
18
42
|
transform: (context: ExpressionContext) => ExpressionContext;
|
|
19
|
-
/**
|
|
20
|
-
* adding.
|
|
43
|
+
/**
|
|
44
|
+
* Hook function called before adding each expression to the output.
|
|
45
|
+
* If it returns a falsy value, the expression will be skipped.
|
|
46
|
+
* Useful for filtering or routing expressions to different destinations.
|
|
47
|
+
* @param context - The parsed expression context
|
|
48
|
+
* @returns The expression context to add, or null/undefined to skip
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* preAddHook: (ctx) => {
|
|
52
|
+
* if (ctx.key === 'special') return null; // skip this
|
|
53
|
+
* return ctx;
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
21
57
|
preAddHook: (context: ExpressionContext) => null | undefined | ExpressionContext;
|
|
22
58
|
}
|
|
23
59
|
/**
|
|
24
|
-
* Human
|
|
60
|
+
* Human-friendly conditions notation parser for search expressions.
|
|
61
|
+
*
|
|
62
|
+
* Parses expressions like `"key:value"` or `"key:operator:value"` and supports
|
|
63
|
+
* logical operators (`and`, `or`, `and not`, `or not`), parenthesized grouping,
|
|
64
|
+
* quoted strings with escaping, and trailing unparsable content.
|
|
65
|
+
*
|
|
66
|
+
* Designed to work seamlessly with @marianmeres/condition-builder.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* Basic usage:
|
|
70
|
+
* ```ts
|
|
71
|
+
* const result = ConditionParser.parse("foo:bar and baz:bat");
|
|
72
|
+
* // result.parsed contains the parsed conditions
|
|
73
|
+
* // result.unparsed contains any trailing unparsable text
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* Complex expressions with grouping:
|
|
78
|
+
* ```ts
|
|
79
|
+
* const result = ConditionParser.parse(
|
|
80
|
+
* '(folder:"my projects" or folder:inbox) foo bar'
|
|
81
|
+
* );
|
|
82
|
+
* ```
|
|
25
83
|
*
|
|
26
|
-
*
|
|
84
|
+
* @example
|
|
85
|
+
* With custom operator:
|
|
86
|
+
* ```ts
|
|
87
|
+
* const result = ConditionParser.parse("age:gt:18 and status:active");
|
|
88
|
+
* ```
|
|
27
89
|
*
|
|
28
|
-
* Internally uses series of layered parsers, each handling a specific part of the grammar,
|
|
90
|
+
* Internally uses a series of layered parsers, each handling a specific part of the grammar,
|
|
29
91
|
* with logical expressions at the top, basic expressions at the bottom, and parenthesized
|
|
30
92
|
* grouping connecting them recursively.
|
|
31
93
|
*/
|
|
32
94
|
export declare class ConditionParser {
|
|
33
95
|
#private;
|
|
96
|
+
/**
|
|
97
|
+
* Default operator used when none is specified in the expression.
|
|
98
|
+
* @default "eq"
|
|
99
|
+
*/
|
|
34
100
|
static DEFAULT_OPERATOR: string;
|
|
101
|
+
/**
|
|
102
|
+
* Global debug flag. When true, all parser instances will log debug information.
|
|
103
|
+
* @default false
|
|
104
|
+
*/
|
|
35
105
|
static DEBUG: boolean;
|
|
36
106
|
private constructor();
|
|
37
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Public helper for creating formatted error messages with position and context.
|
|
109
|
+
*
|
|
110
|
+
* Note: Prefixed with `__` to indicate this is a special-purpose public method
|
|
111
|
+
* not intended for general use. It's exposed primarily for testing and advanced
|
|
112
|
+
* use cases.
|
|
113
|
+
*
|
|
114
|
+
* @param input - The full input string being parsed
|
|
115
|
+
* @param pos - The position where the error occurred
|
|
116
|
+
* @param message - The error message
|
|
117
|
+
* @param contextRadius - Number of characters to show before/after error position (default: 20)
|
|
118
|
+
* @returns Error object with formatted message including position and context
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* const error = ConditionParser.__createError(
|
|
123
|
+
* "foo:bar and baz:bat",
|
|
124
|
+
* 12,
|
|
125
|
+
* "Unexpected character",
|
|
126
|
+
* 20
|
|
127
|
+
* );
|
|
128
|
+
* // Error message includes position and visual marker
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
static __createError(input: string, pos: number, message: string, contextRadius?: number): Error;
|
|
132
|
+
/**
|
|
133
|
+
* Parses a human-friendly search condition string into a structured format.
|
|
134
|
+
*
|
|
135
|
+
* @param input - The search expression string to parse
|
|
136
|
+
* @param options - Optional configuration for parsing behavior
|
|
137
|
+
* @returns An object containing:
|
|
138
|
+
* - `parsed`: Array of parsed condition expressions in ConditionDump format
|
|
139
|
+
* - `unparsed`: Any trailing text that couldn't be parsed (useful for free-text search)
|
|
140
|
+
* - `meta`: Metadata about the parsed expressions (unique keys, operators, values)
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* Basic parsing:
|
|
144
|
+
* ```ts
|
|
145
|
+
* const { parsed, unparsed } = ConditionParser.parse("foo:bar and baz:bat");
|
|
146
|
+
* ```
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* With options:
|
|
150
|
+
* ```ts
|
|
151
|
+
* const result = ConditionParser.parse("FOO:bar", {
|
|
152
|
+
* defaultOperator: "contains",
|
|
153
|
+
* transform: (ctx) => ({ ...ctx, key: ctx.key.toLowerCase() })
|
|
154
|
+
* });
|
|
155
|
+
* ```
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* Handling unparsed content:
|
|
159
|
+
* ```ts
|
|
160
|
+
* const { parsed, unparsed } = ConditionParser.parse(
|
|
161
|
+
* "category:books free text search"
|
|
162
|
+
* );
|
|
163
|
+
* // parsed: [{ expression: { key: "category", operator: "eq", value: "books" }, ... }]
|
|
164
|
+
* // unparsed: "free text search"
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
38
167
|
static parse(input: string, options?: Partial<ConditionParserOptions>): {
|
|
39
168
|
parsed: ConditionDump;
|
|
40
169
|
unparsed: string;
|
package/dist/parser.js
CHANGED
|
@@ -1,14 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Human
|
|
2
|
+
* Human-friendly conditions notation parser for search expressions.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Parses expressions like `"key:value"` or `"key:operator:value"` and supports
|
|
5
|
+
* logical operators (`and`, `or`, `and not`, `or not`), parenthesized grouping,
|
|
6
|
+
* quoted strings with escaping, and trailing unparsable content.
|
|
5
7
|
*
|
|
6
|
-
*
|
|
8
|
+
* Designed to work seamlessly with @marianmeres/condition-builder.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* Basic usage:
|
|
12
|
+
* ```ts
|
|
13
|
+
* const result = ConditionParser.parse("foo:bar and baz:bat");
|
|
14
|
+
* // result.parsed contains the parsed conditions
|
|
15
|
+
* // result.unparsed contains any trailing unparsable text
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* Complex expressions with grouping:
|
|
20
|
+
* ```ts
|
|
21
|
+
* const result = ConditionParser.parse(
|
|
22
|
+
* '(folder:"my projects" or folder:inbox) foo bar'
|
|
23
|
+
* );
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* With custom operator:
|
|
28
|
+
* ```ts
|
|
29
|
+
* const result = ConditionParser.parse("age:gt:18 and status:active");
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* Internally uses a series of layered parsers, each handling a specific part of the grammar,
|
|
7
33
|
* with logical expressions at the top, basic expressions at the bottom, and parenthesized
|
|
8
34
|
* grouping connecting them recursively.
|
|
9
35
|
*/
|
|
10
36
|
export class ConditionParser {
|
|
37
|
+
/**
|
|
38
|
+
* Default operator used when none is specified in the expression.
|
|
39
|
+
* @default "eq"
|
|
40
|
+
*/
|
|
11
41
|
static DEFAULT_OPERATOR = "eq";
|
|
42
|
+
/**
|
|
43
|
+
* Global debug flag. When true, all parser instances will log debug information.
|
|
44
|
+
* @default false
|
|
45
|
+
*/
|
|
12
46
|
static DEBUG = false;
|
|
13
47
|
#input;
|
|
14
48
|
#pos = 0;
|
|
@@ -48,6 +82,50 @@ export class ConditionParser {
|
|
|
48
82
|
console.debug("[ConditionParser]", ...args);
|
|
49
83
|
}
|
|
50
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Public helper for creating formatted error messages with position and context.
|
|
87
|
+
*
|
|
88
|
+
* Note: Prefixed with `__` to indicate this is a special-purpose public method
|
|
89
|
+
* not intended for general use. It's exposed primarily for testing and advanced
|
|
90
|
+
* use cases.
|
|
91
|
+
*
|
|
92
|
+
* @param input - The full input string being parsed
|
|
93
|
+
* @param pos - The position where the error occurred
|
|
94
|
+
* @param message - The error message
|
|
95
|
+
* @param contextRadius - Number of characters to show before/after error position (default: 20)
|
|
96
|
+
* @returns Error object with formatted message including position and context
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* const error = ConditionParser.__createError(
|
|
101
|
+
* "foo:bar and baz:bat",
|
|
102
|
+
* 12,
|
|
103
|
+
* "Unexpected character",
|
|
104
|
+
* 20
|
|
105
|
+
* );
|
|
106
|
+
* // Error message includes position and visual marker
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
static __createError(input, pos, message, contextRadius = 20) {
|
|
110
|
+
const start = Math.max(0, pos - contextRadius);
|
|
111
|
+
const end = Math.min(input.length, pos + contextRadius);
|
|
112
|
+
const snippet = input.slice(start, end);
|
|
113
|
+
const markerPos = pos - start;
|
|
114
|
+
const errorMsg = [
|
|
115
|
+
message,
|
|
116
|
+
`Position: ${pos}`,
|
|
117
|
+
`Context: "${snippet}"`,
|
|
118
|
+
` ${" ".repeat(markerPos)}^`,
|
|
119
|
+
].join("\n");
|
|
120
|
+
return new Error(errorMsg);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Creates an error message with position information and context.
|
|
124
|
+
* Uses the public static helper internally.
|
|
125
|
+
*/
|
|
126
|
+
#createError(message) {
|
|
127
|
+
return ConditionParser.__createError(this.#input, this.#pos, message);
|
|
128
|
+
}
|
|
51
129
|
/** Will look ahead (if positive) or behind (if negative) based on `offset` */
|
|
52
130
|
#peek(offset = 0) {
|
|
53
131
|
const at = this.#pos + offset;
|
|
@@ -78,7 +156,7 @@ export class ConditionParser {
|
|
|
78
156
|
this.#debug("parseParenthesizedValue:start");
|
|
79
157
|
// sanity
|
|
80
158
|
if (this.#peek() !== "(") {
|
|
81
|
-
throw
|
|
159
|
+
throw this.#createError("Not parenthesized string");
|
|
82
160
|
}
|
|
83
161
|
// Consume opening (
|
|
84
162
|
this.#consume();
|
|
@@ -98,7 +176,7 @@ export class ConditionParser {
|
|
|
98
176
|
result += char;
|
|
99
177
|
}
|
|
100
178
|
}
|
|
101
|
-
throw
|
|
179
|
+
throw this.#createError("Unterminated parenthesized string");
|
|
102
180
|
}
|
|
103
181
|
/** Will parse the currently ahead quoted block with escape support.
|
|
104
182
|
* Supports both single ' and double " quotes. */
|
|
@@ -106,7 +184,7 @@ export class ConditionParser {
|
|
|
106
184
|
this.#debug("parseQuotedString:start");
|
|
107
185
|
// sanity
|
|
108
186
|
if (!this.#isQuoteAhead()) {
|
|
109
|
-
throw
|
|
187
|
+
throw this.#createError("Not quoted string");
|
|
110
188
|
}
|
|
111
189
|
let result = "";
|
|
112
190
|
// Consume opening quote
|
|
@@ -125,7 +203,7 @@ export class ConditionParser {
|
|
|
125
203
|
result += char;
|
|
126
204
|
}
|
|
127
205
|
}
|
|
128
|
-
throw
|
|
206
|
+
throw this.#createError("Unterminated quoted string");
|
|
129
207
|
}
|
|
130
208
|
/** Will parse the currently ahead unquoted block until delimiter ":", "(", ")", or \s) */
|
|
131
209
|
#parseUnquotedString() {
|
|
@@ -158,19 +236,34 @@ export class ConditionParser {
|
|
|
158
236
|
this.#consumeWhitespace();
|
|
159
237
|
const remaining = this.#input.slice(this.#pos);
|
|
160
238
|
let result = null;
|
|
239
|
+
const _isNotAhead = (s) => /\s*not\s+/i.exec(s);
|
|
161
240
|
if (/^and /i.test(remaining)) {
|
|
162
241
|
this.#pos += 4;
|
|
163
242
|
result = "and";
|
|
243
|
+
// maybe followed by "not"?
|
|
244
|
+
const notIsAheadMatch = _isNotAhead(remaining);
|
|
245
|
+
if (notIsAheadMatch) {
|
|
246
|
+
// minus 1, because the initial test includes single trailing whitespace
|
|
247
|
+
this.#pos += notIsAheadMatch[0].length - 1;
|
|
248
|
+
result = "andNot";
|
|
249
|
+
}
|
|
164
250
|
}
|
|
165
251
|
else if (/^or /i.test(remaining)) {
|
|
166
252
|
this.#pos += 3;
|
|
167
253
|
result = "or";
|
|
254
|
+
// maybe followed by "not"?
|
|
255
|
+
const notIsAheadMatch = _isNotAhead(remaining);
|
|
256
|
+
if (notIsAheadMatch) {
|
|
257
|
+
// minus 1, because the initial test includes single trailing whitespace
|
|
258
|
+
this.#pos += notIsAheadMatch[0].length - 1;
|
|
259
|
+
result = "orNot";
|
|
260
|
+
}
|
|
168
261
|
}
|
|
169
262
|
else if (openingParenthesesLevel !== undefined) {
|
|
170
263
|
const preLevel = openingParenthesesLevel;
|
|
171
264
|
const postLevel = this.#countSameCharsAhead(")");
|
|
172
265
|
if (preLevel !== postLevel) {
|
|
173
|
-
throw
|
|
266
|
+
throw this.#createError(`Parentheses level mismatch (opening: ${preLevel}, closing: ${postLevel})`);
|
|
174
267
|
}
|
|
175
268
|
}
|
|
176
269
|
this.#debug("parseConditionOperator:result", result);
|
|
@@ -192,7 +285,7 @@ export class ConditionParser {
|
|
|
192
285
|
this.#consumeWhitespace();
|
|
193
286
|
if (this.#consume() !== ":") {
|
|
194
287
|
this.#pos = _startPos;
|
|
195
|
-
throw
|
|
288
|
+
throw this.#createError("Expected colon after key");
|
|
196
289
|
}
|
|
197
290
|
this.#consumeWhitespace();
|
|
198
291
|
// Check if we have an operator
|
|
@@ -215,7 +308,7 @@ export class ConditionParser {
|
|
|
215
308
|
if (this.#peek() === ":") {
|
|
216
309
|
if (wasParenthesized) {
|
|
217
310
|
this.#pos = _startPos;
|
|
218
|
-
throw
|
|
311
|
+
throw this.#createError("Operator cannot be a parenthesized expression");
|
|
219
312
|
}
|
|
220
313
|
operator = value;
|
|
221
314
|
this.#consume(); // consume the second colon
|
|
@@ -281,7 +374,7 @@ export class ConditionParser {
|
|
|
281
374
|
this.#consumeWhitespace();
|
|
282
375
|
if (this.#peek() !== ")") {
|
|
283
376
|
this.#pos = _startPos;
|
|
284
|
-
throw
|
|
377
|
+
throw this.#createError("Expected closing parenthesis");
|
|
285
378
|
}
|
|
286
379
|
// consume closing parenthesis
|
|
287
380
|
this.#consume();
|
|
@@ -367,7 +460,41 @@ export class ConditionParser {
|
|
|
367
460
|
this.#depth--;
|
|
368
461
|
return out;
|
|
369
462
|
}
|
|
370
|
-
/**
|
|
463
|
+
/**
|
|
464
|
+
* Parses a human-friendly search condition string into a structured format.
|
|
465
|
+
*
|
|
466
|
+
* @param input - The search expression string to parse
|
|
467
|
+
* @param options - Optional configuration for parsing behavior
|
|
468
|
+
* @returns An object containing:
|
|
469
|
+
* - `parsed`: Array of parsed condition expressions in ConditionDump format
|
|
470
|
+
* - `unparsed`: Any trailing text that couldn't be parsed (useful for free-text search)
|
|
471
|
+
* - `meta`: Metadata about the parsed expressions (unique keys, operators, values)
|
|
472
|
+
*
|
|
473
|
+
* @example
|
|
474
|
+
* Basic parsing:
|
|
475
|
+
* ```ts
|
|
476
|
+
* const { parsed, unparsed } = ConditionParser.parse("foo:bar and baz:bat");
|
|
477
|
+
* ```
|
|
478
|
+
*
|
|
479
|
+
* @example
|
|
480
|
+
* With options:
|
|
481
|
+
* ```ts
|
|
482
|
+
* const result = ConditionParser.parse("FOO:bar", {
|
|
483
|
+
* defaultOperator: "contains",
|
|
484
|
+
* transform: (ctx) => ({ ...ctx, key: ctx.key.toLowerCase() })
|
|
485
|
+
* });
|
|
486
|
+
* ```
|
|
487
|
+
*
|
|
488
|
+
* @example
|
|
489
|
+
* Handling unparsed content:
|
|
490
|
+
* ```ts
|
|
491
|
+
* const { parsed, unparsed } = ConditionParser.parse(
|
|
492
|
+
* "category:books free text search"
|
|
493
|
+
* );
|
|
494
|
+
* // parsed: [{ expression: { key: "category", operator: "eq", value: "books" }, ... }]
|
|
495
|
+
* // unparsed: "free text search"
|
|
496
|
+
* ```
|
|
497
|
+
*/
|
|
371
498
|
static parse(input, options = {}) {
|
|
372
499
|
const parser = new ConditionParser(input, options);
|
|
373
500
|
let parsed = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/condition-parser",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/mod.js",
|
|
6
6
|
"types": "dist/mod.d.ts",
|
|
@@ -14,6 +14,6 @@
|
|
|
14
14
|
"url": "https://github.com/marianmeres/condition-parser/issues"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@marianmeres/condition-builder": "^1.
|
|
17
|
+
"@marianmeres/condition-builder": "^1.9.0"
|
|
18
18
|
}
|
|
19
19
|
}
|