@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 +90 -171
- package/dist/index.js +46 -36
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
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
|
-
- **
|
|
8
|
-
- **
|
|
9
|
-
- **Logical operators
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
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
|
|
36
|
-
|
|
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
|
|
44
|
-
|
|
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
|
|
57
|
-
|
|
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
|
-
//
|
|
75
|
-
// issues: []
|
|
53
|
+
// issues: [],
|
|
76
54
|
// }
|
|
77
55
|
```
|
|
78
56
|
|
|
79
57
|
## Query Syntax
|
|
80
58
|
|
|
81
|
-
###
|
|
82
|
-
|
|
59
|
+
### Text Search
|
|
83
60
|
```
|
|
84
61
|
my invoice
|
|
85
|
-
"
|
|
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:
|
|
104
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
NOT
|
|
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
|
-
###
|
|
84
|
+
### Optimization
|
|
85
|
+
You can enable optimization to simplify the parsed expression tree:
|
|
134
86
|
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
153
|
+
## Error Handling
|
|
191
154
|
|
|
192
|
-
The
|
|
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',
|
|
239
|
-
//
|
|
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
|
-
##
|
|
171
|
+
## Credits
|
|
254
172
|
|
|
255
|
-
|
|
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 ||
|
|
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
|
|
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 ||
|
|
142
|
+
if (!char || stopCondition(char)) break;
|
|
132
143
|
if (stopAtQuote && char === "\"") break;
|
|
133
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
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)
|
|
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)
|
|
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 &&
|
|
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
|
|
222
|
-
if (filter)
|
|
223
|
-
|
|
224
|
-
|
|
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:
|
|
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(
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
|
5
|
-
"description": "
|
|
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",
|