@papra/search-parser 0.0.1 → 0.1.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/README.md CHANGED
@@ -1,29 +1,23 @@
1
1
  # @papra/search-parser
2
2
 
3
3
  A search query parser library for building GitHub-style search syntax with filters, logical operators, and full-text search.
4
+ You can play with the parser in the [demo application](https://search-parser.papra.app/).
4
5
 
5
6
  ## Features
6
-
7
- - **Full-text search**: Extract search terms from queries
8
- - **Filters**: Support for field:value syntax with multiple operators (=, >, <, >=, <=)
9
- - **Logical operators**: AND, OR, NOT with proper precedence
10
- - **Grouping**: Parentheses for controlling evaluation order
11
- - **Negation**: Both `-filter:value` and `NOT filter:value` syntax
12
- - **Quoted values**: Support for spaces in filter values and search terms
13
- - **Escape sequences**: Escape quotes and colons with backslash
14
- - **Configurable limits**: Max depth and token count for safety
15
- - **Graceful error handling**: Best-effort parsing with issue reporting
16
- - **Zero dependencies**: No runtime dependencies
17
- - **Universal**: Works in Node.js, browsers, Deno, Cloudflare Workers, etc.
18
- - **Type-safe**: Fully typed with TypeScript
7
+ - **TypeScript-first**: Fully typed API and AST structures.
8
+ - **Dependency-free**: No external dependencies, lightweight and fast.
9
+ - **Error-resilient**: Best-effort parsing with detailed issue reporting.
10
+ - **Rich syntax support**: Logical operators (AND, OR, NOT), grouping with parentheses, and field-based filters.
11
+ - **Configurable limits**: Control maximum depth and token count to prevent abuse.
12
+ - **Optimization**: Simplifies the parsed expression tree by removing redundancies, and basic boolean algebra simplifications.
19
13
 
20
14
  ## Installation
21
15
 
22
16
  ```bash
23
- npm install @papra/search-parser
24
- # or
25
17
  pnpm add @papra/search-parser
26
18
  # or
19
+ npm install @papra/search-parser
20
+ # or
27
21
  yarn add @papra/search-parser
28
22
  ```
29
23
 
@@ -32,31 +26,16 @@ yarn add @papra/search-parser
32
26
  ```typescript
33
27
  import { parseSearchQuery } from '@papra/search-parser';
34
28
 
35
- // Simple full-text search
36
- const result1 = parseSearchQuery({ query: 'my invoice' });
37
- // {
38
- // expression: { type: 'and', operands: [] },
39
- // search: 'my invoice',
40
- // issues: []
41
- // }
29
+ // Simple text search
30
+ parseSearchQuery({ query: 'foobar' });
31
+ // { expression: { type: 'text', value: 'foobar' }, issues: [] }
42
32
 
43
- // Filter with equality
44
- const result2 = parseSearchQuery({ query: 'tag:invoice' });
45
- // {
46
- // expression: {
47
- // type: 'filter',
48
- // field: 'tag',
49
- // operator: '=',
50
- // value: 'invoice'
51
- // },
52
- // search: undefined,
53
- // issues: []
54
- // }
33
+ // Filter query
34
+ parseSearchQuery({ query: 'tag:invoice' });
35
+ // { expression: { type: 'filter', field: 'tag', operator: '=', value: 'invoice' }, issues: [] }
55
36
 
56
- // Complex query with operators and grouping
57
- const result3 = parseSearchQuery({
58
- query: '(tag:invoice OR tag:receipt) AND createdAt:>2024-01-01'
59
- });
37
+ // Complex query with operators
38
+ parseSearchQuery({ query: '(tag:invoice OR tag:receipt) AND createdAt:>2024-01-01' });
60
39
  // {
61
40
  // expression: {
62
41
  // type: 'and',
@@ -65,191 +44,131 @@ const result3 = parseSearchQuery({
65
44
  // type: 'or',
66
45
  // operands: [
67
46
  // { type: 'filter', field: 'tag', operator: '=', value: 'invoice' },
68
- // { type: 'filter', field: 'tag', operator: '=', value: 'receipt' }
69
- // ]
47
+ // { type: 'filter', field: 'tag', operator: '=', value: 'receipt' },
48
+ // ],
70
49
  // },
71
- // { type: 'filter', field: 'createdAt', operator: '>', value: '2024-01-01' }
72
- // ]
50
+ // { type: 'filter', field: 'createdAt', operator: '>', value: '2024-01-01' },
51
+ // ],
73
52
  // },
74
- // search: undefined,
75
- // issues: []
53
+ // issues: [],
76
54
  // }
77
55
  ```
78
56
 
79
57
  ## Query Syntax
80
58
 
81
- ### Full-text Search
82
-
59
+ ### Text Search
83
60
  ```
84
61
  my invoice
85
- "my special invoice" # Quoted for multi-word terms
86
- "my \"special\" invoice" # Escaped quotes
62
+ "quoted text"
87
63
  ```
88
64
 
89
65
  ### Filters
90
-
91
- ```
92
- tag:invoice # Equality (implicit)
93
- tag:=invoice # Equality (explicit)
94
- createdAt:>2024-01-01 # Greater than
95
- createdAt:<2024-12-31 # Less than
96
- createdAt:>=2024-01-01 # Greater than or equal
97
- createdAt:<=2024-12-31 # Less than or equal
98
- ```
99
-
100
- ### Quoted Filter Values
101
-
102
66
  ```
103
- tag:"my invoices" # Spaces in value
104
- tag:"my \"special\" invoices" # Escaped quotes in value
105
- tag:my\:\:special\:\:tag # Escaped colons in value
67
+ tag:invoice # Equality (same as tag:=invoice)
68
+ createdAt:>2024-01-01 # Comparison operators: >, <, >=, <=, =
106
69
  ```
107
70
 
108
71
  ### Logical Operators
109
-
110
- ```
111
- tag:invoice AND status:active # Explicit AND
112
- tag:invoice status:active # Implicit AND
113
- tag:invoice OR tag:receipt # OR
114
- NOT tag:personal # NOT
115
- tag:invoice OR tag:receipt AND status:active # Precedence: AND > OR
116
- ```
117
-
118
- ### Negation
119
-
120
72
  ```
121
- -tag:personal # Minus prefix
122
- NOT tag:personal # NOT keyword
123
- NOT (tag:personal OR tag:private) # Negated group
73
+ tag:invoice AND status:active
74
+ tag:invoice OR tag:receipt
75
+ NOT tag:personal
76
+ -tag:personal # Negation shorthand
124
77
  ```
125
78
 
126
79
  ### Grouping
127
-
128
80
  ```
129
- (tag:invoice OR tag:receipt)
130
81
  (tag:invoice OR tag:receipt) AND status:active
131
82
  ```
132
83
 
133
- ### Combining Filters and Search
84
+ ### Optimization
85
+ You can enable optimization to simplify the parsed expression tree:
134
86
 
135
- ```
136
- tag:invoice my document # Filter + search
137
- foo tag:invoice bar # Search terms can be anywhere
138
- ```
87
+ ```typescript
88
+ // The query has redundant ANDs and double negations
89
+ const query = 'tag:invoice AND (tag:receipt AND (tag:invoice AND NOT (NOT foo)))';
139
90
 
140
- ### Escaping
91
+ parseSearchQuery({ query, optimize: false });
92
+ // {
93
+ // expression: {
94
+ // type: 'and',
95
+ // operands: [
96
+ // { type: 'filter', field: 'tag', operator: '=', value: 'invoice' },
97
+ // {
98
+ // type: 'and',
99
+ // operands: [
100
+ // { type: 'filter', field: 'tag', operator: '=', value: 'receipt' },
101
+ // {
102
+ // type: 'and',
103
+ // operands: [
104
+ // { type: 'filter', field: 'tag', operator: '=', value: 'invoice' },
105
+ // {
106
+ // type: 'not',
107
+ // operand: {
108
+ // type: 'not',
109
+ // operand: { type: 'text', value: 'foo' },
110
+ // },
111
+ // },
112
+ // ],
113
+ // },
114
+ // ],
115
+ // },
116
+ // ],
117
+ // },
118
+ // issues: [],
119
+ // }
120
+
121
+ parseSearchQuery({ query, optimize: true });
122
+ // {
123
+ // expression: {
124
+ // type: 'and',
125
+ // operands: [
126
+ // { type: 'filter', field: 'tag', operator: '=', value: 'invoice' },
127
+ // { type: 'filter', field: 'tag', operator: '=', value: 'receipt' },
128
+ // { type: 'text', value: 'foo' },
129
+ // ],
130
+ // },
131
+ // issues: [],
132
+ // }
141
133
 
142
- ```
143
- tag\:invoice # Escape colon to prevent filter parsing
144
- # Results in search text "tag:invoice"
145
134
  ```
146
135
 
147
- ## API
148
136
 
149
- ### parseSearchQuery
137
+ ## API
150
138
 
151
139
  ```typescript
152
140
  function parseSearchQuery(options: {
153
141
  query: string;
154
142
  maxDepth?: number; // Default: 10
155
143
  maxTokens?: number; // Default: 200
144
+ optimize?: boolean; // Default: true
156
145
  }): ParsedQuery;
157
- ```
158
-
159
- ## Operator Precedence
160
-
161
- From highest to lowest:
162
-
163
- 1. **NOT** (highest)
164
- 2. **AND**
165
- 3. **OR** (lowest)
166
-
167
- Use parentheses to override precedence:
168
146
 
169
- ```
170
- tag:invoice OR tag:receipt AND status:active
171
- # Parsed as: tag:invoice OR (tag:receipt AND status:active)
172
-
173
- (tag:invoice OR tag:receipt) AND status:active
174
- # Parsed as: (tag:invoice OR tag:receipt) AND status:active
175
- ```
176
-
177
- ## Error Handling
178
-
179
- ### Issue Structure
180
-
181
- All parsing issues are returned with both a machine-readable error code and a human-readable message:
182
-
183
- ```typescript
184
- type Issue = {
185
- code: string; // Machine-readable error code
186
- message: string; // Human-readable error message
147
+ type ParsedQuery = {
148
+ expression: Expression;
149
+ issues: Issue[];
187
150
  };
188
151
  ```
189
152
 
190
- ### Error Codes
153
+ ## Error Handling
191
154
 
192
- The library exports an `ERROR_CODES` constant with all available error codes:
155
+ The parser returns issues for malformed queries while doing best-effort parsing:
193
156
 
194
157
  ```typescript
195
158
  import { ERROR_CODES } from '@papra/search-parser';
196
159
 
197
160
  const result = parseSearchQuery({ query: '(tag:invoice' });
198
-
199
- // Check for specific error by code
200
- const hasUnmatchedParen = result.issues.some(
201
- issue => issue.code === ERROR_CODES.UNMATCHED_OPENING_PARENTHESIS
202
- );
203
- ```
204
-
205
- Available error codes are defined in [`src/errors.ts`](src/errors.ts).
206
-
207
- ## Safety Features
208
-
209
- ### Maximum Depth
210
-
211
- Prevents deeply nested expressions (e.g., excessive parentheses):
212
-
213
- ```typescript
214
- parseSearchQuery({
215
- query: '((((((((((tag:invoice))))))))))',
216
- maxDepth: 5 // Will report issue if exceeded
217
- });
218
- ```
219
-
220
- ### Maximum Tokens
221
-
222
- Prevents excessively long queries:
223
-
224
- ```typescript
225
- parseSearchQuery({
226
- query: 'tag1:val1 tag2:val2 ... tag100:val100',
227
- maxTokens: 50 // Will report issue if exceeded
228
- });
229
- ```
230
-
231
- ### Graceful Error Handling
232
-
233
- Malformed queries are handled gracefully with best-effort parsing:
234
-
235
- ```typescript
236
- parseSearchQuery({ query: '(tag:invoice' });
237
161
  // {
238
- // expression: { type: 'filter', field: 'tag', operator: '=', value: 'invoice' },
239
- // search: undefined,
240
- // issues: [
241
- // {
242
- // code: 'unmatched-opening-parenthesis',
243
- // message: 'Unmatched opening parenthesis'
244
- // }
245
- // ]
162
+ // expression: { type: 'filter', ... },
163
+ // issues: [{ code: 'unmatched-opening-parenthesis', message: '...' }]
246
164
  // }
247
165
  ```
248
166
 
249
167
  ## License
250
168
 
251
- MIT
169
+ This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.
252
170
 
253
- ## Contributing
171
+ ## Credits
254
172
 
255
- Contributions are welcome! Please open an issue or pull request on GitHub.
173
+ This project is crafted with ❤️ by [Corentin Thomasset](https://corentin.tech).
174
+ If you find this project helpful, please consider [supporting my work](https://buymeacoffee.com/cthmsst).
package/dist/index.js CHANGED
@@ -80,8 +80,19 @@ function areExpressionsIdentical(a, b) {
80
80
  return false;
81
81
  }
82
82
 
83
+ //#endregion
84
+ //#region src/string.ts
85
+ function isWhitespace(char) {
86
+ return /\s/.test(char);
87
+ }
88
+
83
89
  //#endregion
84
90
  //#region src/tokenizer.ts
91
+ const unescapeBackslashes = (str) => str.replace(/\\(.)/g, "$1");
92
+ const unescapeColons = (str) => str.replace(/\\:/g, ":");
93
+ function isWhitespaceOrParen(char) {
94
+ return isWhitespace(char) || char === "(" || char === ")";
95
+ }
85
96
  function tokenize({ query, maxTokens }) {
86
97
  const tokens = [];
87
98
  const issues = [];
@@ -91,7 +102,7 @@ function tokenize({ query, maxTokens }) {
91
102
  const skipWhitespace = () => {
92
103
  while (pos < query.length) {
93
104
  const char = query[pos];
94
- if (!char || !/\s/.test(char)) break;
105
+ if (!char || !isWhitespace(char)) break;
95
106
  pos++;
96
107
  }
97
108
  };
@@ -124,36 +135,41 @@ function tokenize({ query, maxTokens }) {
124
135
  });
125
136
  return value;
126
137
  };
127
- const readUnquotedToken = (stopAtQuote = false) => {
138
+ const readUnquotedValue = ({ stopCondition, processEscapes, stopAtQuote = false }) => {
128
139
  let value = "";
129
140
  while (pos < query.length) {
130
141
  const char = peek();
131
- if (!char || /\s/.test(char) || char === "(" || char === ")") break;
142
+ if (!char || stopCondition(char)) break;
132
143
  if (stopAtQuote && char === "\"") break;
133
- value += advance();
144
+ if (processEscapes && char === "\\") {
145
+ advance();
146
+ if (pos < query.length) value += advance();
147
+ } else value += advance();
134
148
  }
135
149
  return value;
136
150
  };
151
+ const readUnquotedToken = () => {
152
+ return readUnquotedValue({
153
+ stopCondition: isWhitespaceOrParen,
154
+ processEscapes: false,
155
+ stopAtQuote: true
156
+ });
157
+ };
137
158
  const readFilterValue = () => {
138
159
  skipWhitespace();
139
160
  const quotedValue = readQuotedString();
140
161
  if (quotedValue !== void 0) return quotedValue;
141
- let value = "";
142
- while (pos < query.length) {
143
- const char = peek();
144
- if (!char || /\s/.test(char) || char === "(" || char === ")") break;
145
- if (char === "\\") {
146
- advance();
147
- if (pos < query.length) value += advance();
148
- } else value += advance();
149
- }
150
- return value.replace(/\\:/g, ":");
162
+ const value = readUnquotedValue({
163
+ stopCondition: isWhitespaceOrParen,
164
+ processEscapes: true
165
+ });
166
+ return unescapeColons(value);
151
167
  };
152
168
  const hasUnescapedColon = (str) => {
153
169
  for (let i = 0; i < str.length; i++) if (str[i] === ":" && (i === 0 || str[i - 1] !== "\\")) return true;
154
170
  return false;
155
171
  };
156
- const parseFilter = (token, negated) => {
172
+ const parseFilter = (token) => {
157
173
  if (!hasUnescapedColon(token)) return void 0;
158
174
  let firstColonIndex = -1;
159
175
  for (let i = 0; i < token.length; i++) if (token[i] === ":" && (i === 0 || token[i - 1] !== "\\")) {
@@ -161,7 +177,7 @@ function tokenize({ query, maxTokens }) {
161
177
  break;
162
178
  }
163
179
  if (firstColonIndex === -1) return void 0;
164
- const field = token.slice(0, firstColonIndex).replace(/\\:/g, ":");
180
+ const field = unescapeColons(token.slice(0, firstColonIndex));
165
181
  const afterColon = token.slice(firstColonIndex + 1);
166
182
  let operator = "=";
167
183
  let operatorLength = 0;
@@ -181,14 +197,13 @@ function tokenize({ query, maxTokens }) {
181
197
  operator = "=";
182
198
  operatorLength = 1;
183
199
  }
184
- let value = afterColon.slice(operatorLength).replace(/\\:/g, ":");
200
+ let value = unescapeColons(afterColon.slice(operatorLength));
185
201
  if (!value) value = readFilterValue();
186
202
  return {
187
203
  type: "FILTER",
188
204
  field,
189
205
  operator,
190
- value,
191
- negated
206
+ value
192
207
  };
193
208
  };
194
209
  while (pos < query.length) {
@@ -213,25 +228,26 @@ function tokenize({ query, maxTokens }) {
213
228
  continue;
214
229
  }
215
230
  const nextChar = query[pos + 1];
216
- if (char === "-" && nextChar && !/\s/.test(nextChar)) {
231
+ if (char === "-" && nextChar && !isWhitespace(nextChar)) {
217
232
  advance();
218
233
  skipWhitespace();
219
234
  const quotedValue$1 = readQuotedString();
220
235
  const token$1 = quotedValue$1 !== void 0 ? quotedValue$1 : readUnquotedToken();
221
- const filter = parseFilter(token$1, true);
222
- if (filter) tokens.push(filter);
223
- else {
224
- const unescapedText$1 = token$1.replace(/\\(.)/g, "$1");
236
+ const filter = parseFilter(token$1);
237
+ if (filter) {
238
+ tokens.push({ type: "NOT" });
239
+ tokens.push(filter);
240
+ } else {
225
241
  tokens.push({ type: "NOT" });
226
242
  tokens.push({
227
243
  type: "TEXT",
228
- value: unescapedText$1
244
+ value: unescapeBackslashes(token$1)
229
245
  });
230
246
  }
231
247
  continue;
232
248
  }
233
249
  const quotedValue = readQuotedString();
234
- const token = quotedValue !== void 0 ? quotedValue : readUnquotedToken(true);
250
+ const token = quotedValue !== void 0 ? quotedValue : readUnquotedToken();
235
251
  if (!token) {
236
252
  advance();
237
253
  continue;
@@ -250,16 +266,15 @@ function tokenize({ query, maxTokens }) {
250
266
  continue;
251
267
  }
252
268
  if (quotedValue === void 0) {
253
- const filter = parseFilter(token, false);
269
+ const filter = parseFilter(token);
254
270
  if (filter) {
255
271
  tokens.push(filter);
256
272
  continue;
257
273
  }
258
274
  }
259
- const unescapedText = token.replace(/\\(.)/g, "$1");
260
275
  tokens.push({
261
276
  type: "TEXT",
262
- value: unescapedText
277
+ value: unescapeBackslashes(token)
263
278
  });
264
279
  }
265
280
  tokens.push({ type: "EOF" });
@@ -271,7 +286,7 @@ function tokenize({ query, maxTokens }) {
271
286
 
272
287
  //#endregion
273
288
  //#region src/parser.ts
274
- function parseSearchQuery({ query, maxDepth = 10, maxTokens = 200, optimize = false }) {
289
+ function parseSearchQuery({ query, maxDepth = 10, maxTokens = 200, optimize = true }) {
275
290
  const { tokens, issues: tokenizerIssues } = tokenize({
276
291
  query,
277
292
  maxTokens
@@ -324,17 +339,12 @@ function parseExpression({ tokens, maxDepth }) {
324
339
  }
325
340
  if (token.type === "FILTER") {
326
341
  advance();
327
- const filterExpr = {
342
+ return {
328
343
  type: "filter",
329
344
  field: token.field,
330
345
  operator: token.operator,
331
346
  value: token.value
332
347
  };
333
- if (token.negated) return {
334
- type: "not",
335
- operand: filterExpr
336
- };
337
- return filterExpr;
338
348
  }
339
349
  if (token.type === "TEXT") {
340
350
  advance();
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["flattenedOperands: Expression[]","deduplicatedOperands: Expression[]","a: Expression","b: Expression","tokens: Token[]","issues: Issue[]","str: string","token: string","negated: boolean","operator: Operator","quotedValue","token","unescapedText","parserIssues: Issue[]","filterExpr: Expression","operands: Expression[]"],"sources":["../src/errors.ts","../src/optimization.ts","../src/tokenizer.ts","../src/parser.ts"],"sourcesContent":["export const ERROR_CODES = {\n MAX_TOKENS_EXCEEDED: 'max-tokens-exceeded',\n MAX_NESTING_DEPTH_EXCEEDED: 'max-nesting-depth-exceeded',\n UNMATCHED_OPENING_PARENTHESIS: 'unmatched-opening-parenthesis',\n UNMATCHED_CLOSING_PARENTHESIS: 'unmatched-closing-parenthesis',\n UNCLOSED_QUOTED_STRING: 'unclosed-quoted-string',\n MISSING_OPERAND_FOR_NOT: 'missing-operand-for-not',\n};\n","import type { AndExpression, Expression, NotExpression, OrExpression } from './parser.types';\n\n/**\n * Simplifies an expression tree by applying optimization rules.\n * - AND/OR expressions with a single child are replaced by that child.\n * - NOT expressions with a NOT child are replaced by the grandchild (double negation).\n * - Nested AND/OR expressions are flattened.\n * - Redundant expressions are removed (duplicates, empty expressions).\n * - Empty AND/OR expressions are converted to 'empty'.\n */\nexport function simplifyExpression({ expression }: { expression: Expression }): { expression: Expression } {\n if (\n expression.type === 'empty'\n || expression.type === 'text'\n || expression.type === 'filter'\n ) {\n return { expression };\n }\n\n if (expression.type === 'not') {\n return simplifyNotExpression({ expression });\n }\n\n if (expression.type === 'and' || expression.type === 'or') {\n return simplifyAndOrExpression({ expression });\n }\n\n // This should never be reached, but hey, you never know\n return { expression };\n}\n\nexport function simplifyOperands({ operands }: { operands: Expression[] }): { simplifiedOperands: Expression[] } {\n const simplifiedOperands = operands.map(expression => simplifyExpression({ expression }).expression);\n\n return { simplifiedOperands };\n}\n\nfunction simplifyNotExpression({ expression }: { expression: NotExpression }): { expression: Expression } {\n const { expression: simplifiedOperandExpression } = simplifyExpression({ expression: expression.operand });\n\n // NOT(NOT(A)) -> A\n if (simplifiedOperandExpression.type === 'not') {\n return { expression: simplifiedOperandExpression.operand };\n }\n\n // NOT(empty) -> empty\n if (simplifiedOperandExpression.type === 'empty') {\n return { expression: { type: 'empty' } };\n }\n\n return { expression: { type: 'not', operand: simplifiedOperandExpression } };\n}\n\nfunction simplifyAndOrExpression({ expression }: { expression: AndExpression | OrExpression }): { expression: Expression } {\n const { simplifiedOperands } = simplifyOperands({ operands: expression.operands });\n const filteredOperands = simplifiedOperands.filter(op => op.type !== 'empty');\n const { flattenedOperands } = flattenOperands({ type: expression.type, operands: filteredOperands });\n const { deduplicatedOperands } = deduplicateOperands({ operands: flattenedOperands });\n\n if (deduplicatedOperands.length === 0) {\n return { expression: { type: 'empty' } };\n }\n\n // AND(A) -> A, OR(A) -> A\n if (deduplicatedOperands.length === 1) {\n return { expression: deduplicatedOperands[0]! };\n }\n\n return {\n expression: {\n type: expression.type,\n operands: deduplicatedOperands,\n },\n };\n}\n\nfunction flattenOperands({ type, operands }: { type: 'and' | 'or'; operands: Expression[] }): { flattenedOperands: Expression[] } {\n const flattenedOperands: Expression[] = [];\n\n for (const operand of operands) {\n // When the operand is of the same type, inline its operands\n // AND(A, AND(B, C)) -> AND(A, B, C)\n if (operand.type === type) {\n flattenedOperands.push(...operand.operands);\n } else {\n flattenedOperands.push(operand);\n }\n }\n\n return { flattenedOperands };\n}\n\nfunction deduplicateOperands({ operands }: { operands: Expression[] }): { deduplicatedOperands: Expression[] } {\n const deduplicatedOperands: Expression[] = [];\n\n for (const operand of operands) {\n const isDuplicate = deduplicatedOperands.some(existing => areExpressionsIdentical(existing, operand));\n if (!isDuplicate) {\n deduplicatedOperands.push(operand);\n }\n }\n\n return { deduplicatedOperands };\n}\n\nexport function areExpressionsIdentical(a: Expression, b: Expression): boolean {\n if (a.type !== b.type) {\n return false;\n }\n\n if (a.type === 'empty' && b.type === 'empty') {\n return true;\n }\n\n if (a.type === 'text' && b.type === 'text') {\n return a.value === b.value;\n }\n\n if (a.type === 'filter' && b.type === 'filter') {\n return a.field === b.field\n && a.operator === b.operator\n && a.value === b.value;\n }\n\n if (a.type === 'not' && b.type === 'not') {\n return areExpressionsIdentical(a.operand, b.operand);\n }\n\n if ((a.type === 'and' && b.type === 'and') || (a.type === 'or' && b.type === 'or')) {\n if (a.operands.length !== b.operands.length) {\n return false;\n }\n\n for (let i = 0; i < a.operands.length; i++) {\n if (!areExpressionsIdentical(a.operands[i]!, b.operands[i]!)) {\n return false;\n }\n }\n\n return true;\n }\n\n return false;\n}\n","import type { Issue, Operator } from './parser.types';\nimport { ERROR_CODES } from './errors';\n\nexport type Token\n = | { type: 'LPAREN' }\n | { type: 'RPAREN' }\n | { type: 'AND' }\n | { type: 'OR' }\n | { type: 'NOT' }\n | { type: 'FILTER'; field: string; operator: Operator; value: string; negated: boolean }\n | { type: 'TEXT'; value: string }\n | { type: 'EOF' };\n\nexport type TokenizeResult = {\n tokens: Token[];\n issues: Issue[];\n};\n\nexport function tokenize({ query, maxTokens }: { query: string; maxTokens: number }): TokenizeResult {\n const tokens: Token[] = [];\n const issues: Issue[] = [];\n let pos = 0;\n\n const peek = (): string | undefined => query[pos];\n const advance = (): string => query[pos++] || '';\n\n const skipWhitespace = (): void => {\n while (pos < query.length) {\n const char = query[pos];\n if (!char || !/\\s/.test(char)) {\n break;\n }\n pos++;\n }\n };\n\n const readQuotedString = (): string | undefined => {\n if (peek() !== '\"') {\n return undefined;\n }\n\n advance(); // Skip opening quote\n let value = '';\n let escaped = false;\n\n while (pos < query.length) {\n const char = peek();\n\n if (!char) {\n break;\n }\n\n if (escaped) {\n value += char;\n escaped = false;\n advance();\n } else if (char === '\\\\') {\n escaped = true;\n advance();\n } else if (char === '\"') {\n advance(); // Skip closing quote\n return value;\n } else {\n value += char;\n advance();\n }\n }\n\n issues.push({\n code: ERROR_CODES.UNCLOSED_QUOTED_STRING,\n message: 'Unclosed quoted string',\n });\n return value;\n };\n\n const readUnquotedToken = (stopAtQuote = false): string => {\n let value = '';\n\n while (pos < query.length) {\n const char = peek();\n\n if (!char || /\\s/.test(char) || char === '(' || char === ')') {\n break;\n }\n\n // Stop at quote if requested (for filter field names before quoted values)\n if (stopAtQuote && char === '\"') {\n break;\n }\n\n // Keep backslashes in unquoted tokens so parseFilter can detect escaped colons\n value += advance();\n }\n\n return value;\n };\n\n const readFilterValue = (): string => {\n skipWhitespace();\n\n // Check if value is quoted\n const quotedValue = readQuotedString();\n if (quotedValue !== undefined) {\n return quotedValue;\n }\n\n // Read unquoted value (up to whitespace or special chars)\n let value = '';\n while (pos < query.length) {\n const char = peek();\n\n if (!char || /\\s/.test(char) || char === '(' || char === ')') {\n break;\n }\n\n if (char === '\\\\') {\n advance();\n if (pos < query.length) {\n value += advance();\n }\n } else {\n value += advance();\n }\n }\n\n return value.replace(/\\\\:/g, ':');\n };\n\n const hasUnescapedColon = (str: string): boolean => {\n for (let i = 0; i < str.length; i++) {\n if (str[i] === ':' && (i === 0 || str[i - 1] !== '\\\\')) {\n return true;\n }\n }\n return false;\n };\n\n const parseFilter = (token: string, negated: boolean): Token | undefined => {\n // Check for unescaped colons\n if (!hasUnescapedColon(token)) {\n return undefined;\n }\n\n // Find first unescaped colon\n let firstColonIndex = -1;\n for (let i = 0; i < token.length; i++) {\n if (token[i] === ':' && (i === 0 || token[i - 1] !== '\\\\')) {\n firstColonIndex = i;\n break;\n }\n }\n\n if (firstColonIndex === -1) {\n return undefined;\n }\n\n const field = token.slice(0, firstColonIndex).replace(/\\\\:/g, ':');\n const afterColon = token.slice(firstColonIndex + 1);\n\n // Check for operator at the start\n let operator: Operator = '=';\n let operatorLength = 0;\n\n if (afterColon.startsWith('>=')) {\n operator = '>=';\n operatorLength = 2;\n } else if (afterColon.startsWith('<=')) {\n operator = '<=';\n operatorLength = 2;\n } else if (afterColon.startsWith('>')) {\n operator = '>';\n operatorLength = 1;\n } else if (afterColon.startsWith('<')) {\n operator = '<';\n operatorLength = 1;\n } else if (afterColon.startsWith('=')) {\n operator = '=';\n operatorLength = 1;\n }\n\n // If there's nothing after the operator in the token, read the value from input\n let value = afterColon.slice(operatorLength).replace(/\\\\:/g, ':');\n\n // If the value is empty and we still have input, this might be a case like \"tag:\" followed by a quoted string\n if (!value) {\n value = readFilterValue();\n }\n\n return { type: 'FILTER', field, operator, value, negated };\n };\n\n while (pos < query.length) {\n if (tokens.length >= maxTokens) {\n issues.push({\n code: ERROR_CODES.MAX_TOKENS_EXCEEDED,\n message: `Maximum token limit of ${maxTokens} exceeded`,\n });\n break;\n }\n\n skipWhitespace();\n\n if (pos >= query.length) {\n break;\n }\n\n const char = peek();\n\n // Handle parentheses\n if (char === '(') {\n advance();\n tokens.push({ type: 'LPAREN' });\n continue;\n }\n\n if (char === ')') {\n advance();\n tokens.push({ type: 'RPAREN' });\n continue;\n }\n\n // Handle negation prefix\n const nextChar = query[pos + 1];\n if (char === '-' && nextChar && !/\\s/.test(nextChar)) {\n advance();\n skipWhitespace();\n\n // Read the next token (could be quoted or unquoted)\n const quotedValue = readQuotedString();\n const token = quotedValue !== undefined ? quotedValue : readUnquotedToken();\n\n // Try to parse as filter\n const filter = parseFilter(token, true);\n if (filter) {\n tokens.push(filter);\n } else {\n // If not a filter, treat as negated text search (which we'll handle as NOT operator)\n const unescapedText = token.replace(/\\\\(.)/g, '$1');\n tokens.push({ type: 'NOT' });\n tokens.push({ type: 'TEXT', value: unescapedText });\n }\n continue;\n }\n\n // Read quoted or unquoted token\n const quotedValue = readQuotedString();\n const token = quotedValue !== undefined ? quotedValue : readUnquotedToken(true);\n\n if (!token) {\n advance(); // Skip invalid character\n continue;\n }\n\n // Check for operators\n const upperToken = token.toUpperCase();\n if (upperToken === 'AND') {\n tokens.push({ type: 'AND' });\n continue;\n }\n\n if (upperToken === 'OR') {\n tokens.push({ type: 'OR' });\n continue;\n }\n\n if (upperToken === 'NOT') {\n tokens.push({ type: 'NOT' });\n continue;\n }\n\n // Try to parse as filter (only if not quoted)\n if (quotedValue === undefined) {\n const filter = parseFilter(token, false);\n if (filter) {\n tokens.push(filter);\n continue;\n }\n }\n\n // Otherwise, treat as text (unescape backslashes)\n const unescapedText = token.replace(/\\\\(.)/g, '$1');\n tokens.push({ type: 'TEXT', value: unescapedText });\n }\n\n tokens.push({ type: 'EOF' });\n\n return { tokens, issues };\n}\n","import type { Expression, Issue, ParsedQuery } from './parser.types';\nimport type { Token } from './tokenizer';\nimport { ERROR_CODES } from './errors';\nimport { simplifyExpression } from './optimization';\nimport { tokenize } from './tokenizer';\n\nexport function parseSearchQuery(\n {\n query,\n maxDepth = 10,\n maxTokens = 200,\n optimize = false,\n }: {\n query: string;\n maxDepth?: number;\n maxTokens?: number;\n optimize?: boolean;\n },\n): ParsedQuery {\n const { tokens, issues: tokenizerIssues } = tokenize({ query, maxTokens });\n\n const { expression, issues: parserIssues } = parseExpression({ tokens, maxDepth });\n\n const issues = [...tokenizerIssues, ...parserIssues];\n\n if (!optimize) {\n return {\n expression,\n issues,\n };\n }\n\n const { expression: optimizedExpression } = simplifyExpression({ expression });\n\n return {\n expression: optimizedExpression,\n issues,\n };\n}\n\nfunction parseExpression({ tokens, maxDepth }: { tokens: Token[]; maxDepth: number }): ParsedQuery {\n const parserIssues: Issue[] = [];\n\n let currentTokenIndex = 0;\n let currentDepth = 0;\n\n const peek = (): Token => tokens[currentTokenIndex] ?? { type: 'EOF' };\n const advance = (): Token => tokens[currentTokenIndex++] ?? { type: 'EOF' };\n\n const checkDepth = (): boolean => {\n if (currentDepth >= maxDepth) {\n parserIssues.push({\n code: ERROR_CODES.MAX_NESTING_DEPTH_EXCEEDED,\n message: `Maximum nesting depth of ${maxDepth} exceeded`,\n });\n return false;\n }\n return true;\n };\n\n // Parse primary expression (filter, parentheses, text)\n function parsePrimaryExpression(): Expression | undefined {\n const token = peek();\n\n if (token.type === 'LPAREN') {\n advance(); // Consume (\n\n if (!checkDepth()) {\n return undefined;\n }\n\n currentDepth++;\n const expr = parseOrExpression();\n currentDepth--;\n\n if (peek().type === 'RPAREN') {\n advance(); // Consume )\n } else {\n parserIssues.push({\n code: ERROR_CODES.UNMATCHED_OPENING_PARENTHESIS,\n message: 'Unmatched opening parenthesis',\n });\n }\n\n return expr;\n }\n\n if (token.type === 'FILTER') {\n advance();\n const filterExpr: Expression = {\n type: 'filter',\n field: token.field,\n operator: token.operator,\n value: token.value,\n };\n\n if (token.negated) {\n return { type: 'not', operand: filterExpr };\n }\n\n return filterExpr;\n }\n\n if (token.type === 'TEXT') {\n advance();\n return {\n type: 'text',\n value: token.value,\n };\n }\n\n return undefined;\n }\n\n function parseUnaryExpression(): Expression | undefined {\n if (peek().type === 'NOT') {\n advance(); // Consume NOT\n\n if (!checkDepth()) {\n return undefined;\n }\n\n currentDepth++;\n const operand = parseUnaryExpression();\n currentDepth--;\n\n if (!operand) {\n parserIssues.push({\n code: ERROR_CODES.MISSING_OPERAND_FOR_NOT,\n message: 'NOT operator requires an operand',\n });\n return undefined;\n }\n\n return { type: 'not', operand };\n }\n\n return parsePrimaryExpression();\n }\n\n function parseAndExpression(): Expression | undefined {\n const operands: Expression[] = [];\n\n while (true) {\n const next = peek();\n\n // Stop if we hit EOF, OR operator, or closing paren\n if (next.type === 'EOF' || next.type === 'OR' || next.type === 'RPAREN') {\n break;\n }\n\n // Consume explicit AND operator\n if (next.type === 'AND') {\n advance();\n continue;\n }\n\n const expr = parseUnaryExpression();\n if (expr) {\n operands.push(expr);\n }\n }\n\n if (operands.length === 0) {\n return undefined;\n }\n\n if (operands.length === 1) {\n return operands[0];\n }\n\n return { type: 'and', operands };\n };\n\n function parseOrExpression(): Expression | undefined {\n const left = parseAndExpression();\n if (!left) {\n return undefined;\n }\n\n const operands: Expression[] = [left];\n\n while (peek().type === 'OR') {\n advance(); // Consume OR\n const right = parseAndExpression();\n if (right) {\n operands.push(right);\n }\n }\n\n if (operands.length === 1) {\n return operands[0];\n }\n\n return { type: 'or', operands };\n };\n\n const expression = parseOrExpression();\n\n // Check for unmatched closing parentheses\n while (peek().type === 'RPAREN') {\n parserIssues.push({\n message: 'Unmatched closing parenthesis',\n code: ERROR_CODES.UNMATCHED_CLOSING_PARENTHESIS,\n });\n advance();\n }\n\n return {\n expression: expression ?? { type: 'empty' },\n issues: parserIssues,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["flattenedOperands: Expression[]","deduplicatedOperands: Expression[]","a: Expression","b: Expression","char: string","str: string","char: string","tokens: Token[]","issues: Issue[]","token: string","operator: Operator","quotedValue","token","parserIssues: Issue[]","operands: Expression[]"],"sources":["../src/errors.ts","../src/optimization.ts","../src/string.ts","../src/tokenizer.ts","../src/parser.ts"],"sourcesContent":["export const ERROR_CODES = {\n MAX_TOKENS_EXCEEDED: 'max-tokens-exceeded',\n MAX_NESTING_DEPTH_EXCEEDED: 'max-nesting-depth-exceeded',\n UNMATCHED_OPENING_PARENTHESIS: 'unmatched-opening-parenthesis',\n UNMATCHED_CLOSING_PARENTHESIS: 'unmatched-closing-parenthesis',\n UNCLOSED_QUOTED_STRING: 'unclosed-quoted-string',\n MISSING_OPERAND_FOR_NOT: 'missing-operand-for-not',\n};\n","import type { AndExpression, Expression, NotExpression, OrExpression } from './parser.types';\n\n/**\n * Simplifies an expression tree by applying optimization rules.\n * - AND/OR expressions with a single child are replaced by that child.\n * - NOT expressions with a NOT child are replaced by the grandchild (double negation).\n * - Nested AND/OR expressions are flattened.\n * - Redundant expressions are removed (duplicates, empty expressions).\n * - Empty AND/OR expressions are converted to 'empty'.\n */\nexport function simplifyExpression({ expression }: { expression: Expression }): { expression: Expression } {\n if (\n expression.type === 'empty'\n || expression.type === 'text'\n || expression.type === 'filter'\n ) {\n return { expression };\n }\n\n if (expression.type === 'not') {\n return simplifyNotExpression({ expression });\n }\n\n if (expression.type === 'and' || expression.type === 'or') {\n return simplifyAndOrExpression({ expression });\n }\n\n // This should never be reached, but hey, you never know\n return { expression };\n}\n\nexport function simplifyOperands({ operands }: { operands: Expression[] }): { simplifiedOperands: Expression[] } {\n const simplifiedOperands = operands.map(expression => simplifyExpression({ expression }).expression);\n\n return { simplifiedOperands };\n}\n\nfunction simplifyNotExpression({ expression }: { expression: NotExpression }): { expression: Expression } {\n const { expression: simplifiedOperandExpression } = simplifyExpression({ expression: expression.operand });\n\n // NOT(NOT(A)) -> A\n if (simplifiedOperandExpression.type === 'not') {\n return { expression: simplifiedOperandExpression.operand };\n }\n\n // NOT(empty) -> empty\n if (simplifiedOperandExpression.type === 'empty') {\n return { expression: { type: 'empty' } };\n }\n\n return { expression: { type: 'not', operand: simplifiedOperandExpression } };\n}\n\nfunction simplifyAndOrExpression({ expression }: { expression: AndExpression | OrExpression }): { expression: Expression } {\n const { simplifiedOperands } = simplifyOperands({ operands: expression.operands });\n const filteredOperands = simplifiedOperands.filter(op => op.type !== 'empty');\n const { flattenedOperands } = flattenOperands({ type: expression.type, operands: filteredOperands });\n const { deduplicatedOperands } = deduplicateOperands({ operands: flattenedOperands });\n\n if (deduplicatedOperands.length === 0) {\n return { expression: { type: 'empty' } };\n }\n\n // AND(A) -> A, OR(A) -> A\n if (deduplicatedOperands.length === 1) {\n return { expression: deduplicatedOperands[0]! };\n }\n\n return {\n expression: {\n type: expression.type,\n operands: deduplicatedOperands,\n },\n };\n}\n\nfunction flattenOperands({ type, operands }: { type: 'and' | 'or'; operands: Expression[] }): { flattenedOperands: Expression[] } {\n const flattenedOperands: Expression[] = [];\n\n for (const operand of operands) {\n // When the operand is of the same type, inline its operands\n // AND(A, AND(B, C)) -> AND(A, B, C)\n if (operand.type === type) {\n flattenedOperands.push(...operand.operands);\n } else {\n flattenedOperands.push(operand);\n }\n }\n\n return { flattenedOperands };\n}\n\nfunction deduplicateOperands({ operands }: { operands: Expression[] }): { deduplicatedOperands: Expression[] } {\n const deduplicatedOperands: Expression[] = [];\n\n for (const operand of operands) {\n const isDuplicate = deduplicatedOperands.some(existing => areExpressionsIdentical(existing, operand));\n if (!isDuplicate) {\n deduplicatedOperands.push(operand);\n }\n }\n\n return { deduplicatedOperands };\n}\n\nexport function areExpressionsIdentical(a: Expression, b: Expression): boolean {\n if (a.type !== b.type) {\n return false;\n }\n\n if (a.type === 'empty' && b.type === 'empty') {\n return true;\n }\n\n if (a.type === 'text' && b.type === 'text') {\n return a.value === b.value;\n }\n\n if (a.type === 'filter' && b.type === 'filter') {\n return a.field === b.field\n && a.operator === b.operator\n && a.value === b.value;\n }\n\n if (a.type === 'not' && b.type === 'not') {\n return areExpressionsIdentical(a.operand, b.operand);\n }\n\n if ((a.type === 'and' && b.type === 'and') || (a.type === 'or' && b.type === 'or')) {\n if (a.operands.length !== b.operands.length) {\n return false;\n }\n\n for (let i = 0; i < a.operands.length; i++) {\n if (!areExpressionsIdentical(a.operands[i]!, b.operands[i]!)) {\n return false;\n }\n }\n\n return true;\n }\n\n return false;\n}\n","export function isWhitespace(char: string): boolean {\n return /\\s/.test(char);\n}\n","import type { Issue, Operator } from './parser.types';\nimport { ERROR_CODES } from './errors';\nimport { isWhitespace } from './string';\n\nexport type Token\n = | { type: 'LPAREN' }\n | { type: 'RPAREN' }\n | { type: 'AND' }\n | { type: 'OR' }\n | { type: 'NOT' }\n | { type: 'FILTER'; field: string; operator: Operator; value: string }\n | { type: 'TEXT'; value: string }\n | { type: 'EOF' };\n\nexport type TokenizeResult = {\n tokens: Token[];\n issues: Issue[];\n};\n\n// Unified escape handling utilities\nconst unescapeBackslashes = (str: string): string => str.replace(/\\\\(.)/g, '$1');\nconst unescapeColons = (str: string): string => str.replace(/\\\\:/g, ':');\n\ntype StopCondition = (char: string) => boolean;\n\nfunction isWhitespaceOrParen(char: string): boolean {\n return isWhitespace(char) || char === '(' || char === ')';\n}\n\nexport function tokenize({ query, maxTokens }: { query: string; maxTokens: number }): TokenizeResult {\n const tokens: Token[] = [];\n const issues: Issue[] = [];\n let pos = 0;\n\n const peek = (): string | undefined => query[pos];\n const advance = (): string => query[pos++] || '';\n\n const skipWhitespace = (): void => {\n while (pos < query.length) {\n const char = query[pos];\n if (!char || !isWhitespace(char)) {\n break;\n }\n pos++;\n }\n };\n\n const readQuotedString = (): string | undefined => {\n if (peek() !== '\"') {\n return undefined;\n }\n\n advance(); // Skip opening quote\n let value = '';\n let escaped = false;\n\n while (pos < query.length) {\n const char = peek();\n\n if (!char) {\n break;\n }\n\n if (escaped) {\n value += char;\n escaped = false;\n advance();\n } else if (char === '\\\\') {\n escaped = true;\n advance();\n } else if (char === '\"') {\n advance(); // Skip closing quote\n return value;\n } else {\n value += char;\n advance();\n }\n }\n\n issues.push({\n code: ERROR_CODES.UNCLOSED_QUOTED_STRING,\n message: 'Unclosed quoted string',\n });\n return value;\n };\n\n // Unified function to read unquoted values with configurable stop conditions and escape handling\n const readUnquotedValue = ({\n stopCondition,\n processEscapes,\n stopAtQuote = false,\n }: {\n stopCondition: StopCondition;\n processEscapes: boolean;\n stopAtQuote?: boolean;\n }): string => {\n let value = '';\n\n while (pos < query.length) {\n const char = peek();\n\n if (!char || stopCondition(char)) {\n break;\n }\n\n if (stopAtQuote && char === '\"') {\n break;\n }\n\n if (processEscapes && char === '\\\\') {\n advance();\n if (pos < query.length) {\n value += advance();\n }\n } else {\n value += advance();\n }\n }\n\n return value;\n };\n\n const readUnquotedToken = (): string => {\n // Keep backslashes in unquoted tokens so parseFilter can detect escaped colons\n return readUnquotedValue({\n stopCondition: isWhitespaceOrParen,\n processEscapes: false,\n stopAtQuote: true,\n });\n };\n\n const readFilterValue = (): string => {\n skipWhitespace();\n\n // Check if value is quoted\n const quotedValue = readQuotedString();\n if (quotedValue !== undefined) {\n return quotedValue;\n }\n\n // Read unquoted value with escape processing, then unescape colons\n const value = readUnquotedValue({\n stopCondition: isWhitespaceOrParen,\n processEscapes: true,\n });\n\n return unescapeColons(value);\n };\n\n const hasUnescapedColon = (str: string): boolean => {\n for (let i = 0; i < str.length; i++) {\n if (str[i] === ':' && (i === 0 || str[i - 1] !== '\\\\')) {\n return true;\n }\n }\n return false;\n };\n\n const parseFilter = (token: string): Token | undefined => {\n // Check for unescaped colons\n if (!hasUnescapedColon(token)) {\n return undefined;\n }\n\n // Find first unescaped colon\n let firstColonIndex = -1;\n for (let i = 0; i < token.length; i++) {\n if (token[i] === ':' && (i === 0 || token[i - 1] !== '\\\\')) {\n firstColonIndex = i;\n break;\n }\n }\n\n if (firstColonIndex === -1) {\n return undefined;\n }\n\n const field = unescapeColons(token.slice(0, firstColonIndex));\n const afterColon = token.slice(firstColonIndex + 1);\n\n // Check for operator at the start\n let operator: Operator = '=';\n let operatorLength = 0;\n\n if (afterColon.startsWith('>=')) {\n operator = '>=';\n operatorLength = 2;\n } else if (afterColon.startsWith('<=')) {\n operator = '<=';\n operatorLength = 2;\n } else if (afterColon.startsWith('>')) {\n operator = '>';\n operatorLength = 1;\n } else if (afterColon.startsWith('<')) {\n operator = '<';\n operatorLength = 1;\n } else if (afterColon.startsWith('=')) {\n operator = '=';\n operatorLength = 1;\n }\n\n // If there's nothing after the operator in the token, read the value from input\n let value = unescapeColons(afterColon.slice(operatorLength));\n\n // If the value is empty and we still have input, this might be a case like \"tag:\" followed by a quoted string\n if (!value) {\n value = readFilterValue();\n }\n\n return { type: 'FILTER', field, operator, value };\n };\n\n while (pos < query.length) {\n if (tokens.length >= maxTokens) {\n issues.push({\n code: ERROR_CODES.MAX_TOKENS_EXCEEDED,\n message: `Maximum token limit of ${maxTokens} exceeded`,\n });\n break;\n }\n\n skipWhitespace();\n\n if (pos >= query.length) {\n break;\n }\n\n const char = peek();\n\n // Handle parentheses\n if (char === '(') {\n advance();\n tokens.push({ type: 'LPAREN' });\n continue;\n }\n\n if (char === ')') {\n advance();\n tokens.push({ type: 'RPAREN' });\n continue;\n }\n\n // Handle negation prefix\n const nextChar = query[pos + 1];\n if (char === '-' && nextChar && !isWhitespace(nextChar)) {\n advance();\n skipWhitespace();\n\n // Read the next token (could be quoted or unquoted)\n const quotedValue = readQuotedString();\n const token = quotedValue !== undefined ? quotedValue : readUnquotedToken();\n\n // Try to parse as filter\n const filter = parseFilter(token);\n if (filter) {\n tokens.push({ type: 'NOT' });\n tokens.push(filter);\n } else {\n // If not a filter, treat as negated text search\n tokens.push({ type: 'NOT' });\n tokens.push({ type: 'TEXT', value: unescapeBackslashes(token) });\n }\n continue;\n }\n\n // Read quoted or unquoted token\n const quotedValue = readQuotedString();\n const token = quotedValue !== undefined ? quotedValue : readUnquotedToken();\n\n if (!token) {\n advance(); // Skip invalid character\n continue;\n }\n\n // Check for operators\n const upperToken = token.toUpperCase();\n if (upperToken === 'AND') {\n tokens.push({ type: 'AND' });\n continue;\n }\n\n if (upperToken === 'OR') {\n tokens.push({ type: 'OR' });\n continue;\n }\n\n if (upperToken === 'NOT') {\n tokens.push({ type: 'NOT' });\n continue;\n }\n\n // Try to parse as filter (only if not quoted)\n if (quotedValue === undefined) {\n const filter = parseFilter(token);\n if (filter) {\n tokens.push(filter);\n continue;\n }\n }\n\n // Otherwise, treat as text (unescape backslashes)\n tokens.push({ type: 'TEXT', value: unescapeBackslashes(token) });\n }\n\n tokens.push({ type: 'EOF' });\n\n return { tokens, issues };\n}\n","import type { Expression, Issue, ParsedQuery } from './parser.types';\nimport type { Token } from './tokenizer';\nimport { ERROR_CODES } from './errors';\nimport { simplifyExpression } from './optimization';\nimport { tokenize } from './tokenizer';\n\nexport function parseSearchQuery(\n {\n query,\n maxDepth = 10,\n maxTokens = 200,\n optimize = true,\n }: {\n query: string;\n maxDepth?: number;\n maxTokens?: number;\n optimize?: boolean;\n },\n): ParsedQuery {\n const { tokens, issues: tokenizerIssues } = tokenize({ query, maxTokens });\n\n const { expression, issues: parserIssues } = parseExpression({ tokens, maxDepth });\n\n const issues = [...tokenizerIssues, ...parserIssues];\n\n if (!optimize) {\n return {\n expression,\n issues,\n };\n }\n\n const { expression: optimizedExpression } = simplifyExpression({ expression });\n\n return {\n expression: optimizedExpression,\n issues,\n };\n}\n\nfunction parseExpression({ tokens, maxDepth }: { tokens: Token[]; maxDepth: number }): ParsedQuery {\n const parserIssues: Issue[] = [];\n\n let currentTokenIndex = 0;\n let currentDepth = 0;\n\n const peek = (): Token => tokens[currentTokenIndex] ?? { type: 'EOF' };\n const advance = (): Token => tokens[currentTokenIndex++] ?? { type: 'EOF' };\n\n const checkDepth = (): boolean => {\n if (currentDepth >= maxDepth) {\n parserIssues.push({\n code: ERROR_CODES.MAX_NESTING_DEPTH_EXCEEDED,\n message: `Maximum nesting depth of ${maxDepth} exceeded`,\n });\n return false;\n }\n return true;\n };\n\n // Parse primary expression (filter, parentheses, text)\n function parsePrimaryExpression(): Expression | undefined {\n const token = peek();\n\n if (token.type === 'LPAREN') {\n advance(); // Consume (\n\n if (!checkDepth()) {\n return undefined;\n }\n\n currentDepth++;\n const expr = parseOrExpression();\n currentDepth--;\n\n if (peek().type === 'RPAREN') {\n advance(); // Consume )\n } else {\n parserIssues.push({\n code: ERROR_CODES.UNMATCHED_OPENING_PARENTHESIS,\n message: 'Unmatched opening parenthesis',\n });\n }\n\n return expr;\n }\n\n if (token.type === 'FILTER') {\n advance();\n return {\n type: 'filter',\n field: token.field,\n operator: token.operator,\n value: token.value,\n };\n }\n\n if (token.type === 'TEXT') {\n advance();\n return {\n type: 'text',\n value: token.value,\n };\n }\n\n return undefined;\n }\n\n function parseUnaryExpression(): Expression | undefined {\n if (peek().type === 'NOT') {\n advance(); // Consume NOT\n\n if (!checkDepth()) {\n return undefined;\n }\n\n currentDepth++;\n const operand = parseUnaryExpression();\n currentDepth--;\n\n if (!operand) {\n parserIssues.push({\n code: ERROR_CODES.MISSING_OPERAND_FOR_NOT,\n message: 'NOT operator requires an operand',\n });\n return undefined;\n }\n\n return { type: 'not', operand };\n }\n\n return parsePrimaryExpression();\n }\n\n function parseAndExpression(): Expression | undefined {\n const operands: Expression[] = [];\n\n while (true) {\n const next = peek();\n\n // Stop if we hit EOF, OR operator, or closing paren\n if (next.type === 'EOF' || next.type === 'OR' || next.type === 'RPAREN') {\n break;\n }\n\n // Consume explicit AND operator\n if (next.type === 'AND') {\n advance();\n continue;\n }\n\n const expr = parseUnaryExpression();\n if (expr) {\n operands.push(expr);\n }\n }\n\n if (operands.length === 0) {\n return undefined;\n }\n\n if (operands.length === 1) {\n return operands[0];\n }\n\n return { type: 'and', operands };\n };\n\n function parseOrExpression(): Expression | undefined {\n const left = parseAndExpression();\n if (!left) {\n return undefined;\n }\n\n const operands: Expression[] = [left];\n\n while (peek().type === 'OR') {\n advance(); // Consume OR\n const right = parseAndExpression();\n if (right) {\n operands.push(right);\n }\n }\n\n if (operands.length === 1) {\n return operands[0];\n }\n\n return { type: 'or', operands };\n };\n\n const expression = parseOrExpression();\n\n // Check for unmatched closing parentheses\n while (peek().type === 'RPAREN') {\n parserIssues.push({\n message: 'Unmatched closing parenthesis',\n code: ERROR_CODES.UNMATCHED_CLOSING_PARENTHESIS,\n });\n advance();\n }\n\n return {\n expression: expression ?? { type: 'empty' },\n issues: parserIssues,\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@papra/search-parser",
3
3
  "type": "module",
4
- "version": "0.0.1",
5
- "description": "Search query parser library",
4
+ "version": "0.1.0",
5
+ "description": "A typesafe, dependency-free, error-resilient AST-based search query parser for TypeScript. Supports logical operators, grouping, filters, and more.",
6
6
  "author": "Corentin Thomasset <corentinth@proton.me> (https://corentin.tech)",
7
7
  "license": "MIT",
8
+ "homepage": "https://search-parser.papra.app/",
8
9
  "repository": {
9
10
  "type": "git",
10
11
  "url": "https://github.com/papra-hq/papra",