@marianmeres/condition-parser 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/parser.d.ts +137 -8
- package/dist/parser.js +124 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -133,4 +133,11 @@ assertEquals(
|
|
|
133
133
|
|
|
134
134
|
## Related
|
|
135
135
|
|
|
136
|
-
[@marianmeres/condition-builder](https://github.com/marianmeres/condition-builder)
|
|
136
|
+
[@marianmeres/condition-builder](https://github.com/marianmeres/condition-builder)
|
|
137
|
+
|
|
138
|
+
## Package Identity
|
|
139
|
+
|
|
140
|
+
- **Name:** @marianmeres/condition-parser
|
|
141
|
+
- **Author:** Marian Meres
|
|
142
|
+
- **Repository:** https://github.com/marianmeres/condition-parser
|
|
143
|
+
- **License:** MIT
|
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() {
|
|
@@ -185,7 +263,7 @@ export class ConditionParser {
|
|
|
185
263
|
const preLevel = openingParenthesesLevel;
|
|
186
264
|
const postLevel = this.#countSameCharsAhead(")");
|
|
187
265
|
if (preLevel !== postLevel) {
|
|
188
|
-
throw
|
|
266
|
+
throw this.#createError(`Parentheses level mismatch (opening: ${preLevel}, closing: ${postLevel})`);
|
|
189
267
|
}
|
|
190
268
|
}
|
|
191
269
|
this.#debug("parseConditionOperator:result", result);
|
|
@@ -207,7 +285,7 @@ export class ConditionParser {
|
|
|
207
285
|
this.#consumeWhitespace();
|
|
208
286
|
if (this.#consume() !== ":") {
|
|
209
287
|
this.#pos = _startPos;
|
|
210
|
-
throw
|
|
288
|
+
throw this.#createError("Expected colon after key");
|
|
211
289
|
}
|
|
212
290
|
this.#consumeWhitespace();
|
|
213
291
|
// Check if we have an operator
|
|
@@ -230,7 +308,7 @@ export class ConditionParser {
|
|
|
230
308
|
if (this.#peek() === ":") {
|
|
231
309
|
if (wasParenthesized) {
|
|
232
310
|
this.#pos = _startPos;
|
|
233
|
-
throw
|
|
311
|
+
throw this.#createError("Operator cannot be a parenthesized expression");
|
|
234
312
|
}
|
|
235
313
|
operator = value;
|
|
236
314
|
this.#consume(); // consume the second colon
|
|
@@ -296,7 +374,7 @@ export class ConditionParser {
|
|
|
296
374
|
this.#consumeWhitespace();
|
|
297
375
|
if (this.#peek() !== ")") {
|
|
298
376
|
this.#pos = _startPos;
|
|
299
|
-
throw
|
|
377
|
+
throw this.#createError("Expected closing parenthesis");
|
|
300
378
|
}
|
|
301
379
|
// consume closing parenthesis
|
|
302
380
|
this.#consume();
|
|
@@ -382,7 +460,41 @@ export class ConditionParser {
|
|
|
382
460
|
this.#depth--;
|
|
383
461
|
return out;
|
|
384
462
|
}
|
|
385
|
-
/**
|
|
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
|
+
*/
|
|
386
498
|
static parse(input, options = {}) {
|
|
387
499
|
const parser = new ConditionParser(input, options);
|
|
388
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.1",
|
|
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.9.
|
|
17
|
+
"@marianmeres/condition-builder": "^1.9.2"
|
|
18
18
|
}
|
|
19
19
|
}
|