@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 CHANGED
@@ -10,31 +10,160 @@ interface Meta {
10
10
  values: any[];
11
11
  expressions: ExpressionData[];
12
12
  }
13
- /** ConditionParser.parse options */
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
- /** If provided, will use the output of this fn as a final parsed expression. */
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
- /** Applied as a last step before adding. If returns falsey, will effectively skip
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 friendly conditions notation parser. See README.md for examples.
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
- * Designed to play nicely with @marianmeres/condition-builder.
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
- /** Main api. Will parse the provided input. */
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 friendly conditions notation parser. See README.md for examples.
2
+ * Human-friendly conditions notation parser for search expressions.
3
3
  *
4
- * Designed to play nicely with @marianmeres/condition-builder.
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
- * Internally uses series of layered parsers, each handling a specific part of the grammar,
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 new Error("Not parenthesized string");
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 new Error("Unterminated parenthesized string");
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 new Error("Not quoted string");
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 new Error("Unterminated quoted string");
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 new Error(`Parentheses level mismatch (${preLevel}, ${postLevel})`);
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 new Error("Expected colon after key");
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 new Error("Operator cannot be a parenthesized expression");
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 new Error("Expected closing parenthesis");
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
- /** Main api. Will parse the provided input. */
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.5.0",
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.8.0"
17
+ "@marianmeres/condition-builder": "^1.9.0"
18
18
  }
19
19
  }