@papra/search-parser 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +419 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Corentin THOMASSET
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# @papra/search-parser
|
|
2
|
+
|
|
3
|
+
A search query parser library for building GitHub-style search syntax with filters, logical operators, and full-text search.
|
|
4
|
+
|
|
5
|
+
## 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
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @papra/search-parser
|
|
24
|
+
# or
|
|
25
|
+
pnpm add @papra/search-parser
|
|
26
|
+
# or
|
|
27
|
+
yarn add @papra/search-parser
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { parseSearchQuery } from '@papra/search-parser';
|
|
34
|
+
|
|
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
|
+
// }
|
|
42
|
+
|
|
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
|
+
// }
|
|
55
|
+
|
|
56
|
+
// Complex query with operators and grouping
|
|
57
|
+
const result3 = parseSearchQuery({
|
|
58
|
+
query: '(tag:invoice OR tag:receipt) AND createdAt:>2024-01-01'
|
|
59
|
+
});
|
|
60
|
+
// {
|
|
61
|
+
// expression: {
|
|
62
|
+
// type: 'and',
|
|
63
|
+
// operands: [
|
|
64
|
+
// {
|
|
65
|
+
// type: 'or',
|
|
66
|
+
// operands: [
|
|
67
|
+
// { type: 'filter', field: 'tag', operator: '=', value: 'invoice' },
|
|
68
|
+
// { type: 'filter', field: 'tag', operator: '=', value: 'receipt' }
|
|
69
|
+
// ]
|
|
70
|
+
// },
|
|
71
|
+
// { type: 'filter', field: 'createdAt', operator: '>', value: '2024-01-01' }
|
|
72
|
+
// ]
|
|
73
|
+
// },
|
|
74
|
+
// search: undefined,
|
|
75
|
+
// issues: []
|
|
76
|
+
// }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Query Syntax
|
|
80
|
+
|
|
81
|
+
### Full-text Search
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
my invoice
|
|
85
|
+
"my special invoice" # Quoted for multi-word terms
|
|
86
|
+
"my \"special\" invoice" # Escaped quotes
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 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
|
+
```
|
|
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
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 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
|
+
```
|
|
121
|
+
-tag:personal # Minus prefix
|
|
122
|
+
NOT tag:personal # NOT keyword
|
|
123
|
+
NOT (tag:personal OR tag:private) # Negated group
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Grouping
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
(tag:invoice OR tag:receipt)
|
|
130
|
+
(tag:invoice OR tag:receipt) AND status:active
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Combining Filters and Search
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
tag:invoice my document # Filter + search
|
|
137
|
+
foo tag:invoice bar # Search terms can be anywhere
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Escaping
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
tag\:invoice # Escape colon to prevent filter parsing
|
|
144
|
+
# Results in search text "tag:invoice"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## API
|
|
148
|
+
|
|
149
|
+
### parseSearchQuery
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
function parseSearchQuery(options: {
|
|
153
|
+
query: string;
|
|
154
|
+
maxDepth?: number; // Default: 10
|
|
155
|
+
maxTokens?: number; // Default: 200
|
|
156
|
+
}): 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
|
+
|
|
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
|
|
187
|
+
};
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Error Codes
|
|
191
|
+
|
|
192
|
+
The library exports an `ERROR_CODES` constant with all available error codes:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import { ERROR_CODES } from '@papra/search-parser';
|
|
196
|
+
|
|
197
|
+
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
|
+
// {
|
|
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
|
+
// ]
|
|
246
|
+
// }
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## License
|
|
250
|
+
|
|
251
|
+
MIT
|
|
252
|
+
|
|
253
|
+
## Contributing
|
|
254
|
+
|
|
255
|
+
Contributions are welcome! Please open an issue or pull request on GitHub.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//#region src/errors.d.ts
|
|
2
|
+
declare const ERROR_CODES: {
|
|
3
|
+
MAX_TOKENS_EXCEEDED: string;
|
|
4
|
+
MAX_NESTING_DEPTH_EXCEEDED: string;
|
|
5
|
+
UNMATCHED_OPENING_PARENTHESIS: string;
|
|
6
|
+
UNMATCHED_CLOSING_PARENTHESIS: string;
|
|
7
|
+
UNCLOSED_QUOTED_STRING: string;
|
|
8
|
+
MISSING_OPERAND_FOR_NOT: string;
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/parser.types.d.ts
|
|
12
|
+
type Issue = {
|
|
13
|
+
message: string;
|
|
14
|
+
code: string;
|
|
15
|
+
};
|
|
16
|
+
type ParsedQuery = {
|
|
17
|
+
expression: Expression;
|
|
18
|
+
issues: Issue[];
|
|
19
|
+
};
|
|
20
|
+
type Expression = AndExpression | OrExpression | NotExpression | FilterExpression | TextExpression | EmptyExpression;
|
|
21
|
+
type EmptyExpression = {
|
|
22
|
+
type: 'empty';
|
|
23
|
+
};
|
|
24
|
+
type AndExpression = {
|
|
25
|
+
type: 'and';
|
|
26
|
+
operands: Expression[];
|
|
27
|
+
};
|
|
28
|
+
type OrExpression = {
|
|
29
|
+
type: 'or';
|
|
30
|
+
operands: Expression[];
|
|
31
|
+
};
|
|
32
|
+
type NotExpression = {
|
|
33
|
+
type: 'not';
|
|
34
|
+
operand: Expression;
|
|
35
|
+
};
|
|
36
|
+
type Operator = '>' | '<' | '>=' | '<=' | '=';
|
|
37
|
+
type FilterExpression = {
|
|
38
|
+
type: 'filter';
|
|
39
|
+
field: string;
|
|
40
|
+
operator: Operator;
|
|
41
|
+
value: string;
|
|
42
|
+
};
|
|
43
|
+
type TextExpression = {
|
|
44
|
+
type: 'text';
|
|
45
|
+
value: string;
|
|
46
|
+
};
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/optimization.d.ts
|
|
49
|
+
/**
|
|
50
|
+
* Simplifies an expression tree by applying optimization rules.
|
|
51
|
+
* - AND/OR expressions with a single child are replaced by that child.
|
|
52
|
+
* - NOT expressions with a NOT child are replaced by the grandchild (double negation).
|
|
53
|
+
* - Nested AND/OR expressions are flattened.
|
|
54
|
+
* - Redundant expressions are removed (duplicates, empty expressions).
|
|
55
|
+
* - Empty AND/OR expressions are converted to 'empty'.
|
|
56
|
+
*/
|
|
57
|
+
declare function simplifyExpression({
|
|
58
|
+
expression
|
|
59
|
+
}: {
|
|
60
|
+
expression: Expression;
|
|
61
|
+
}): {
|
|
62
|
+
expression: Expression;
|
|
63
|
+
};
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/parser.d.ts
|
|
66
|
+
declare function parseSearchQuery({
|
|
67
|
+
query,
|
|
68
|
+
maxDepth,
|
|
69
|
+
maxTokens,
|
|
70
|
+
optimize
|
|
71
|
+
}: {
|
|
72
|
+
query: string;
|
|
73
|
+
maxDepth?: number;
|
|
74
|
+
maxTokens?: number;
|
|
75
|
+
optimize?: boolean;
|
|
76
|
+
}): ParsedQuery;
|
|
77
|
+
//#endregion
|
|
78
|
+
export { type AndExpression, ERROR_CODES, type EmptyExpression, type Expression, type FilterExpression, type Issue, type NotExpression, type Operator, type OrExpression, type ParsedQuery, type TextExpression, parseSearchQuery, simplifyExpression };
|
|
79
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/errors.ts","../src/parser.types.ts","../src/optimization.ts","../src/parser.ts"],"sourcesContent":[],"mappings":";cAAa;EAAA,mBAOZ,EAAA,MAAA;;;;ECPW,sBAAK,EAAA,MAAA;EAKL,uBAAW,EAAA,MAAA;CAAA;;;KALX,KAAA;EDAC,OAAA,EAAA,MAOZ;;;KCFW,WAAA;EALA,UAAK,EAMH,UANG;EAKL,MAAA,EAEF,KAFE,EAAW;CAAA;AACT,KAIF,UAAA,GACN,aALQ,GAMR,YANQ,GAOR,aAPQ,GAQR,gBARQ,GASR,cATQ,GAUR,eAVQ;AACJ,KAWE,eAAA,GAXF;EAAK,IAAA,EAAA,OAAA;AAGf,CAAA;AAAsB,KAYV,aAAA,GAZU;EAAA,IAChB,EAAA,KAAA;EAAa,QACb,EAYM,UAZN,EAAA;CAAY;AAEZ,KAaM,YAAA,GAbN;EAAgB,IAChB,EAAA,IAAA;EAAc,QACd,EAaM,UAbN,EAAA;AAAe,CAAA;AAET,KAcA,aAAA,GAde;EAIf,IAAA,EAAA,KAAA;EAKA,OAAA,EAOD,UAPa;AAKxB,CAAA;AAKY,KAAA,QAAA,GAAQ,GAAA,GAAA,GAAA,GAAA,IAAA,GAAA,IAAA,GAAA,GAAA;AAER,KAAA,gBAAA,GAAgB;EAOhB,IAAA,EAAA,QAAA;;YAJA;;AChCZ,CAAA;AAAkC,KDoCtB,cAAA,GCpCsB;EAAA,IAAG,EAAA,MAAA;EAAU,KAAkB,EAAA,MAAA;CAAU;;;AFV3E;;;;ACAA;AAKA;;;AAEU,iBCGM,kBAAA,CDHN;EAAA;AAGV,CAHU,EAAA;EAAK,UAAA,ECGkD,UDHlD;AAGf,CAAA,CAAA,EAAY;EAAU,UAAA,ECAwE,UDAxE;CAAA;;;ADVT,iBGMG,gBAAA,CHCf;EAAA,KAAA;EAAA,QAAA;EAAA,SAAA;EAAA;ACFD,CDEC,EAAA;;;;ECPW,QAAK,CAAA,EAAA,OAAA;AAKjB,CAAA,CAAA,EEaG,WFbS"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
const ERROR_CODES = {
|
|
3
|
+
MAX_TOKENS_EXCEEDED: "max-tokens-exceeded",
|
|
4
|
+
MAX_NESTING_DEPTH_EXCEEDED: "max-nesting-depth-exceeded",
|
|
5
|
+
UNMATCHED_OPENING_PARENTHESIS: "unmatched-opening-parenthesis",
|
|
6
|
+
UNMATCHED_CLOSING_PARENTHESIS: "unmatched-closing-parenthesis",
|
|
7
|
+
UNCLOSED_QUOTED_STRING: "unclosed-quoted-string",
|
|
8
|
+
MISSING_OPERAND_FOR_NOT: "missing-operand-for-not"
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/optimization.ts
|
|
13
|
+
/**
|
|
14
|
+
* Simplifies an expression tree by applying optimization rules.
|
|
15
|
+
* - AND/OR expressions with a single child are replaced by that child.
|
|
16
|
+
* - NOT expressions with a NOT child are replaced by the grandchild (double negation).
|
|
17
|
+
* - Nested AND/OR expressions are flattened.
|
|
18
|
+
* - Redundant expressions are removed (duplicates, empty expressions).
|
|
19
|
+
* - Empty AND/OR expressions are converted to 'empty'.
|
|
20
|
+
*/
|
|
21
|
+
function simplifyExpression({ expression }) {
|
|
22
|
+
if (expression.type === "empty" || expression.type === "text" || expression.type === "filter") return { expression };
|
|
23
|
+
if (expression.type === "not") return simplifyNotExpression({ expression });
|
|
24
|
+
if (expression.type === "and" || expression.type === "or") return simplifyAndOrExpression({ expression });
|
|
25
|
+
return { expression };
|
|
26
|
+
}
|
|
27
|
+
function simplifyOperands({ operands }) {
|
|
28
|
+
const simplifiedOperands = operands.map((expression) => simplifyExpression({ expression }).expression);
|
|
29
|
+
return { simplifiedOperands };
|
|
30
|
+
}
|
|
31
|
+
function simplifyNotExpression({ expression }) {
|
|
32
|
+
const { expression: simplifiedOperandExpression } = simplifyExpression({ expression: expression.operand });
|
|
33
|
+
if (simplifiedOperandExpression.type === "not") return { expression: simplifiedOperandExpression.operand };
|
|
34
|
+
if (simplifiedOperandExpression.type === "empty") return { expression: { type: "empty" } };
|
|
35
|
+
return { expression: {
|
|
36
|
+
type: "not",
|
|
37
|
+
operand: simplifiedOperandExpression
|
|
38
|
+
} };
|
|
39
|
+
}
|
|
40
|
+
function simplifyAndOrExpression({ expression }) {
|
|
41
|
+
const { simplifiedOperands } = simplifyOperands({ operands: expression.operands });
|
|
42
|
+
const filteredOperands = simplifiedOperands.filter((op) => op.type !== "empty");
|
|
43
|
+
const { flattenedOperands } = flattenOperands({
|
|
44
|
+
type: expression.type,
|
|
45
|
+
operands: filteredOperands
|
|
46
|
+
});
|
|
47
|
+
const { deduplicatedOperands } = deduplicateOperands({ operands: flattenedOperands });
|
|
48
|
+
if (deduplicatedOperands.length === 0) return { expression: { type: "empty" } };
|
|
49
|
+
if (deduplicatedOperands.length === 1) return { expression: deduplicatedOperands[0] };
|
|
50
|
+
return { expression: {
|
|
51
|
+
type: expression.type,
|
|
52
|
+
operands: deduplicatedOperands
|
|
53
|
+
} };
|
|
54
|
+
}
|
|
55
|
+
function flattenOperands({ type, operands }) {
|
|
56
|
+
const flattenedOperands = [];
|
|
57
|
+
for (const operand of operands) if (operand.type === type) flattenedOperands.push(...operand.operands);
|
|
58
|
+
else flattenedOperands.push(operand);
|
|
59
|
+
return { flattenedOperands };
|
|
60
|
+
}
|
|
61
|
+
function deduplicateOperands({ operands }) {
|
|
62
|
+
const deduplicatedOperands = [];
|
|
63
|
+
for (const operand of operands) {
|
|
64
|
+
const isDuplicate = deduplicatedOperands.some((existing) => areExpressionsIdentical(existing, operand));
|
|
65
|
+
if (!isDuplicate) deduplicatedOperands.push(operand);
|
|
66
|
+
}
|
|
67
|
+
return { deduplicatedOperands };
|
|
68
|
+
}
|
|
69
|
+
function areExpressionsIdentical(a, b) {
|
|
70
|
+
if (a.type !== b.type) return false;
|
|
71
|
+
if (a.type === "empty" && b.type === "empty") return true;
|
|
72
|
+
if (a.type === "text" && b.type === "text") return a.value === b.value;
|
|
73
|
+
if (a.type === "filter" && b.type === "filter") return a.field === b.field && a.operator === b.operator && a.value === b.value;
|
|
74
|
+
if (a.type === "not" && b.type === "not") return areExpressionsIdentical(a.operand, b.operand);
|
|
75
|
+
if (a.type === "and" && b.type === "and" || a.type === "or" && b.type === "or") {
|
|
76
|
+
if (a.operands.length !== b.operands.length) return false;
|
|
77
|
+
for (let i = 0; i < a.operands.length; i++) if (!areExpressionsIdentical(a.operands[i], b.operands[i])) return false;
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/tokenizer.ts
|
|
85
|
+
function tokenize({ query, maxTokens }) {
|
|
86
|
+
const tokens = [];
|
|
87
|
+
const issues = [];
|
|
88
|
+
let pos = 0;
|
|
89
|
+
const peek = () => query[pos];
|
|
90
|
+
const advance = () => query[pos++] || "";
|
|
91
|
+
const skipWhitespace = () => {
|
|
92
|
+
while (pos < query.length) {
|
|
93
|
+
const char = query[pos];
|
|
94
|
+
if (!char || !/\s/.test(char)) break;
|
|
95
|
+
pos++;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const readQuotedString = () => {
|
|
99
|
+
if (peek() !== "\"") return void 0;
|
|
100
|
+
advance();
|
|
101
|
+
let value = "";
|
|
102
|
+
let escaped = false;
|
|
103
|
+
while (pos < query.length) {
|
|
104
|
+
const char = peek();
|
|
105
|
+
if (!char) break;
|
|
106
|
+
if (escaped) {
|
|
107
|
+
value += char;
|
|
108
|
+
escaped = false;
|
|
109
|
+
advance();
|
|
110
|
+
} else if (char === "\\") {
|
|
111
|
+
escaped = true;
|
|
112
|
+
advance();
|
|
113
|
+
} else if (char === "\"") {
|
|
114
|
+
advance();
|
|
115
|
+
return value;
|
|
116
|
+
} else {
|
|
117
|
+
value += char;
|
|
118
|
+
advance();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
issues.push({
|
|
122
|
+
code: ERROR_CODES.UNCLOSED_QUOTED_STRING,
|
|
123
|
+
message: "Unclosed quoted string"
|
|
124
|
+
});
|
|
125
|
+
return value;
|
|
126
|
+
};
|
|
127
|
+
const readUnquotedToken = (stopAtQuote = false) => {
|
|
128
|
+
let value = "";
|
|
129
|
+
while (pos < query.length) {
|
|
130
|
+
const char = peek();
|
|
131
|
+
if (!char || /\s/.test(char) || char === "(" || char === ")") break;
|
|
132
|
+
if (stopAtQuote && char === "\"") break;
|
|
133
|
+
value += advance();
|
|
134
|
+
}
|
|
135
|
+
return value;
|
|
136
|
+
};
|
|
137
|
+
const readFilterValue = () => {
|
|
138
|
+
skipWhitespace();
|
|
139
|
+
const quotedValue = readQuotedString();
|
|
140
|
+
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, ":");
|
|
151
|
+
};
|
|
152
|
+
const hasUnescapedColon = (str) => {
|
|
153
|
+
for (let i = 0; i < str.length; i++) if (str[i] === ":" && (i === 0 || str[i - 1] !== "\\")) return true;
|
|
154
|
+
return false;
|
|
155
|
+
};
|
|
156
|
+
const parseFilter = (token, negated) => {
|
|
157
|
+
if (!hasUnescapedColon(token)) return void 0;
|
|
158
|
+
let firstColonIndex = -1;
|
|
159
|
+
for (let i = 0; i < token.length; i++) if (token[i] === ":" && (i === 0 || token[i - 1] !== "\\")) {
|
|
160
|
+
firstColonIndex = i;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
if (firstColonIndex === -1) return void 0;
|
|
164
|
+
const field = token.slice(0, firstColonIndex).replace(/\\:/g, ":");
|
|
165
|
+
const afterColon = token.slice(firstColonIndex + 1);
|
|
166
|
+
let operator = "=";
|
|
167
|
+
let operatorLength = 0;
|
|
168
|
+
if (afterColon.startsWith(">=")) {
|
|
169
|
+
operator = ">=";
|
|
170
|
+
operatorLength = 2;
|
|
171
|
+
} else if (afterColon.startsWith("<=")) {
|
|
172
|
+
operator = "<=";
|
|
173
|
+
operatorLength = 2;
|
|
174
|
+
} else if (afterColon.startsWith(">")) {
|
|
175
|
+
operator = ">";
|
|
176
|
+
operatorLength = 1;
|
|
177
|
+
} else if (afterColon.startsWith("<")) {
|
|
178
|
+
operator = "<";
|
|
179
|
+
operatorLength = 1;
|
|
180
|
+
} else if (afterColon.startsWith("=")) {
|
|
181
|
+
operator = "=";
|
|
182
|
+
operatorLength = 1;
|
|
183
|
+
}
|
|
184
|
+
let value = afterColon.slice(operatorLength).replace(/\\:/g, ":");
|
|
185
|
+
if (!value) value = readFilterValue();
|
|
186
|
+
return {
|
|
187
|
+
type: "FILTER",
|
|
188
|
+
field,
|
|
189
|
+
operator,
|
|
190
|
+
value,
|
|
191
|
+
negated
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
while (pos < query.length) {
|
|
195
|
+
if (tokens.length >= maxTokens) {
|
|
196
|
+
issues.push({
|
|
197
|
+
code: ERROR_CODES.MAX_TOKENS_EXCEEDED,
|
|
198
|
+
message: `Maximum token limit of ${maxTokens} exceeded`
|
|
199
|
+
});
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
skipWhitespace();
|
|
203
|
+
if (pos >= query.length) break;
|
|
204
|
+
const char = peek();
|
|
205
|
+
if (char === "(") {
|
|
206
|
+
advance();
|
|
207
|
+
tokens.push({ type: "LPAREN" });
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (char === ")") {
|
|
211
|
+
advance();
|
|
212
|
+
tokens.push({ type: "RPAREN" });
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const nextChar = query[pos + 1];
|
|
216
|
+
if (char === "-" && nextChar && !/\s/.test(nextChar)) {
|
|
217
|
+
advance();
|
|
218
|
+
skipWhitespace();
|
|
219
|
+
const quotedValue$1 = readQuotedString();
|
|
220
|
+
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");
|
|
225
|
+
tokens.push({ type: "NOT" });
|
|
226
|
+
tokens.push({
|
|
227
|
+
type: "TEXT",
|
|
228
|
+
value: unescapedText$1
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const quotedValue = readQuotedString();
|
|
234
|
+
const token = quotedValue !== void 0 ? quotedValue : readUnquotedToken(true);
|
|
235
|
+
if (!token) {
|
|
236
|
+
advance();
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const upperToken = token.toUpperCase();
|
|
240
|
+
if (upperToken === "AND") {
|
|
241
|
+
tokens.push({ type: "AND" });
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (upperToken === "OR") {
|
|
245
|
+
tokens.push({ type: "OR" });
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (upperToken === "NOT") {
|
|
249
|
+
tokens.push({ type: "NOT" });
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (quotedValue === void 0) {
|
|
253
|
+
const filter = parseFilter(token, false);
|
|
254
|
+
if (filter) {
|
|
255
|
+
tokens.push(filter);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const unescapedText = token.replace(/\\(.)/g, "$1");
|
|
260
|
+
tokens.push({
|
|
261
|
+
type: "TEXT",
|
|
262
|
+
value: unescapedText
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
tokens.push({ type: "EOF" });
|
|
266
|
+
return {
|
|
267
|
+
tokens,
|
|
268
|
+
issues
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/parser.ts
|
|
274
|
+
function parseSearchQuery({ query, maxDepth = 10, maxTokens = 200, optimize = false }) {
|
|
275
|
+
const { tokens, issues: tokenizerIssues } = tokenize({
|
|
276
|
+
query,
|
|
277
|
+
maxTokens
|
|
278
|
+
});
|
|
279
|
+
const { expression, issues: parserIssues } = parseExpression({
|
|
280
|
+
tokens,
|
|
281
|
+
maxDepth
|
|
282
|
+
});
|
|
283
|
+
const issues = [...tokenizerIssues, ...parserIssues];
|
|
284
|
+
if (!optimize) return {
|
|
285
|
+
expression,
|
|
286
|
+
issues
|
|
287
|
+
};
|
|
288
|
+
const { expression: optimizedExpression } = simplifyExpression({ expression });
|
|
289
|
+
return {
|
|
290
|
+
expression: optimizedExpression,
|
|
291
|
+
issues
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function parseExpression({ tokens, maxDepth }) {
|
|
295
|
+
const parserIssues = [];
|
|
296
|
+
let currentTokenIndex = 0;
|
|
297
|
+
let currentDepth = 0;
|
|
298
|
+
const peek = () => tokens[currentTokenIndex] ?? { type: "EOF" };
|
|
299
|
+
const advance = () => tokens[currentTokenIndex++] ?? { type: "EOF" };
|
|
300
|
+
const checkDepth = () => {
|
|
301
|
+
if (currentDepth >= maxDepth) {
|
|
302
|
+
parserIssues.push({
|
|
303
|
+
code: ERROR_CODES.MAX_NESTING_DEPTH_EXCEEDED,
|
|
304
|
+
message: `Maximum nesting depth of ${maxDepth} exceeded`
|
|
305
|
+
});
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
return true;
|
|
309
|
+
};
|
|
310
|
+
function parsePrimaryExpression() {
|
|
311
|
+
const token = peek();
|
|
312
|
+
if (token.type === "LPAREN") {
|
|
313
|
+
advance();
|
|
314
|
+
if (!checkDepth()) return void 0;
|
|
315
|
+
currentDepth++;
|
|
316
|
+
const expr = parseOrExpression();
|
|
317
|
+
currentDepth--;
|
|
318
|
+
if (peek().type === "RPAREN") advance();
|
|
319
|
+
else parserIssues.push({
|
|
320
|
+
code: ERROR_CODES.UNMATCHED_OPENING_PARENTHESIS,
|
|
321
|
+
message: "Unmatched opening parenthesis"
|
|
322
|
+
});
|
|
323
|
+
return expr;
|
|
324
|
+
}
|
|
325
|
+
if (token.type === "FILTER") {
|
|
326
|
+
advance();
|
|
327
|
+
const filterExpr = {
|
|
328
|
+
type: "filter",
|
|
329
|
+
field: token.field,
|
|
330
|
+
operator: token.operator,
|
|
331
|
+
value: token.value
|
|
332
|
+
};
|
|
333
|
+
if (token.negated) return {
|
|
334
|
+
type: "not",
|
|
335
|
+
operand: filterExpr
|
|
336
|
+
};
|
|
337
|
+
return filterExpr;
|
|
338
|
+
}
|
|
339
|
+
if (token.type === "TEXT") {
|
|
340
|
+
advance();
|
|
341
|
+
return {
|
|
342
|
+
type: "text",
|
|
343
|
+
value: token.value
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return void 0;
|
|
347
|
+
}
|
|
348
|
+
function parseUnaryExpression() {
|
|
349
|
+
if (peek().type === "NOT") {
|
|
350
|
+
advance();
|
|
351
|
+
if (!checkDepth()) return void 0;
|
|
352
|
+
currentDepth++;
|
|
353
|
+
const operand = parseUnaryExpression();
|
|
354
|
+
currentDepth--;
|
|
355
|
+
if (!operand) {
|
|
356
|
+
parserIssues.push({
|
|
357
|
+
code: ERROR_CODES.MISSING_OPERAND_FOR_NOT,
|
|
358
|
+
message: "NOT operator requires an operand"
|
|
359
|
+
});
|
|
360
|
+
return void 0;
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
type: "not",
|
|
364
|
+
operand
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
return parsePrimaryExpression();
|
|
368
|
+
}
|
|
369
|
+
function parseAndExpression() {
|
|
370
|
+
const operands = [];
|
|
371
|
+
while (true) {
|
|
372
|
+
const next = peek();
|
|
373
|
+
if (next.type === "EOF" || next.type === "OR" || next.type === "RPAREN") break;
|
|
374
|
+
if (next.type === "AND") {
|
|
375
|
+
advance();
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const expr = parseUnaryExpression();
|
|
379
|
+
if (expr) operands.push(expr);
|
|
380
|
+
}
|
|
381
|
+
if (operands.length === 0) return void 0;
|
|
382
|
+
if (operands.length === 1) return operands[0];
|
|
383
|
+
return {
|
|
384
|
+
type: "and",
|
|
385
|
+
operands
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function parseOrExpression() {
|
|
389
|
+
const left = parseAndExpression();
|
|
390
|
+
if (!left) return void 0;
|
|
391
|
+
const operands = [left];
|
|
392
|
+
while (peek().type === "OR") {
|
|
393
|
+
advance();
|
|
394
|
+
const right = parseAndExpression();
|
|
395
|
+
if (right) operands.push(right);
|
|
396
|
+
}
|
|
397
|
+
if (operands.length === 1) return operands[0];
|
|
398
|
+
return {
|
|
399
|
+
type: "or",
|
|
400
|
+
operands
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
const expression = parseOrExpression();
|
|
404
|
+
while (peek().type === "RPAREN") {
|
|
405
|
+
parserIssues.push({
|
|
406
|
+
message: "Unmatched closing parenthesis",
|
|
407
|
+
code: ERROR_CODES.UNMATCHED_CLOSING_PARENTHESIS
|
|
408
|
+
});
|
|
409
|
+
advance();
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
expression: expression ?? { type: "empty" },
|
|
413
|
+
issues: parserIssues
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
//#endregion
|
|
418
|
+
export { ERROR_CODES, parseSearchQuery, simplifyExpression };
|
|
419
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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":";AAAA,MAAa,cAAc;CACzB,qBAAqB;CACrB,4BAA4B;CAC5B,+BAA+B;CAC/B,+BAA+B;CAC/B,wBAAwB;CACxB,yBAAyB;AAC1B;;;;;;;;;;;;ACGD,SAAgB,mBAAmB,EAAE,YAAwC,EAA8B;AACzG,KACE,WAAW,SAAS,WACjB,WAAW,SAAS,UACpB,WAAW,SAAS,SAEvB,QAAO,EAAE,WAAY;AAGvB,KAAI,WAAW,SAAS,MACtB,QAAO,sBAAsB,EAAE,WAAY,EAAC;AAG9C,KAAI,WAAW,SAAS,SAAS,WAAW,SAAS,KACnD,QAAO,wBAAwB,EAAE,WAAY,EAAC;AAIhD,QAAO,EAAE,WAAY;AACtB;AAED,SAAgB,iBAAiB,EAAE,UAAsC,EAAwC;CAC/G,MAAM,qBAAqB,SAAS,IAAI,gBAAc,mBAAmB,EAAE,WAAY,EAAC,CAAC,WAAW;AAEpG,QAAO,EAAE,mBAAoB;AAC9B;AAED,SAAS,sBAAsB,EAAE,YAA2C,EAA8B;CACxG,MAAM,EAAE,YAAY,6BAA6B,GAAG,mBAAmB,EAAE,YAAY,WAAW,QAAS,EAAC;AAG1G,KAAI,4BAA4B,SAAS,MACvC,QAAO,EAAE,YAAY,4BAA4B,QAAS;AAI5D,KAAI,4BAA4B,SAAS,QACvC,QAAO,EAAE,YAAY,EAAE,MAAM,QAAS,EAAE;AAG1C,QAAO,EAAE,YAAY;EAAE,MAAM;EAAO,SAAS;CAA6B,EAAE;AAC7E;AAED,SAAS,wBAAwB,EAAE,YAA0D,EAA8B;CACzH,MAAM,EAAE,oBAAoB,GAAG,iBAAiB,EAAE,UAAU,WAAW,SAAU,EAAC;CAClF,MAAM,mBAAmB,mBAAmB,OAAO,QAAM,GAAG,SAAS,QAAQ;CAC7E,MAAM,EAAE,mBAAmB,GAAG,gBAAgB;EAAE,MAAM,WAAW;EAAM,UAAU;CAAkB,EAAC;CACpG,MAAM,EAAE,sBAAsB,GAAG,oBAAoB,EAAE,UAAU,kBAAmB,EAAC;AAErF,KAAI,qBAAqB,WAAW,EAClC,QAAO,EAAE,YAAY,EAAE,MAAM,QAAS,EAAE;AAI1C,KAAI,qBAAqB,WAAW,EAClC,QAAO,EAAE,YAAY,qBAAqB,GAAK;AAGjD,QAAO,EACL,YAAY;EACV,MAAM,WAAW;EACjB,UAAU;CACX,EACF;AACF;AAED,SAAS,gBAAgB,EAAE,MAAM,UAA0D,EAAuC;CAChI,MAAMA,oBAAkC,CAAE;AAE1C,MAAK,MAAM,WAAW,SAGpB,KAAI,QAAQ,SAAS,MACnB,kBAAkB,KAAK,GAAG,QAAQ,SAAS;MAE3C,kBAAkB,KAAK,QAAQ;AAInC,QAAO,EAAE,kBAAmB;AAC7B;AAED,SAAS,oBAAoB,EAAE,UAAsC,EAA0C;CAC7G,MAAMC,uBAAqC,CAAE;AAE7C,MAAK,MAAM,WAAW,UAAU;EAC9B,MAAM,cAAc,qBAAqB,KAAK,cAAY,wBAAwB,UAAU,QAAQ,CAAC;AACrG,MAAI,CAAC,aACH,qBAAqB,KAAK,QAAQ;CAErC;AAED,QAAO,EAAE,qBAAsB;AAChC;AAED,SAAgB,wBAAwBC,GAAeC,GAAwB;AAC7E,KAAI,EAAE,SAAS,EAAE,KACf,QAAO;AAGT,KAAI,EAAE,SAAS,WAAW,EAAE,SAAS,QACnC,QAAO;AAGT,KAAI,EAAE,SAAS,UAAU,EAAE,SAAS,OAClC,QAAO,EAAE,UAAU,EAAE;AAGvB,KAAI,EAAE,SAAS,YAAY,EAAE,SAAS,SACpC,QAAO,EAAE,UAAU,EAAE,SAChB,EAAE,aAAa,EAAE,YACjB,EAAE,UAAU,EAAE;AAGrB,KAAI,EAAE,SAAS,SAAS,EAAE,SAAS,MACjC,QAAO,wBAAwB,EAAE,SAAS,EAAE,QAAQ;AAGtD,KAAK,EAAE,SAAS,SAAS,EAAE,SAAS,SAAW,EAAE,SAAS,QAAQ,EAAE,SAAS,MAAO;AAClF,MAAI,EAAE,SAAS,WAAW,EAAE,SAAS,OACnC,QAAO;AAGT,OAAK,IAAI,IAAI,GAAG,IAAI,EAAE,SAAS,QAAQ,IACrC,KAAI,CAAC,wBAAwB,EAAE,SAAS,IAAK,EAAE,SAAS,GAAI,CAC1D,QAAO;AAIX,SAAO;CACR;AAED,QAAO;AACR;;;;AC7HD,SAAgB,SAAS,EAAE,OAAO,WAAiD,EAAkB;CACnG,MAAMC,SAAkB,CAAE;CAC1B,MAAMC,SAAkB,CAAE;CAC1B,IAAI,MAAM;CAEV,MAAM,OAAO,MAA0B,MAAM;CAC7C,MAAM,UAAU,MAAc,MAAM,UAAU;CAE9C,MAAM,iBAAiB,MAAY;AACjC,SAAO,MAAM,MAAM,QAAQ;GACzB,MAAM,OAAO,MAAM;AACnB,OAAI,CAAC,QAAQ,CAAC,KAAK,KAAK,KAAK,CAC3B;GAEF;EACD;CACF;CAED,MAAM,mBAAmB,MAA0B;AACjD,MAAI,MAAM,KAAK,KACb,QAAO;EAGT,SAAS;EACT,IAAI,QAAQ;EACZ,IAAI,UAAU;AAEd,SAAO,MAAM,MAAM,QAAQ;GACzB,MAAM,OAAO,MAAM;AAEnB,OAAI,CAAC,KACH;AAGF,OAAI,SAAS;IACX,SAAS;IACT,UAAU;IACV,SAAS;GACV,WAAU,SAAS,MAAM;IACxB,UAAU;IACV,SAAS;GACV,WAAU,SAAS,MAAK;IACvB,SAAS;AACT,WAAO;GACR,OAAM;IACL,SAAS;IACT,SAAS;GACV;EACF;EAED,OAAO,KAAK;GACV,MAAM,YAAY;GAClB,SAAS;EACV,EAAC;AACF,SAAO;CACR;CAED,MAAM,oBAAoB,CAAC,cAAc,UAAkB;EACzD,IAAI,QAAQ;AAEZ,SAAO,MAAM,MAAM,QAAQ;GACzB,MAAM,OAAO,MAAM;AAEnB,OAAI,CAAC,QAAQ,KAAK,KAAK,KAAK,IAAI,SAAS,OAAO,SAAS,IACvD;AAIF,OAAI,eAAe,SAAS,KAC1B;GAIF,SAAS,SAAS;EACnB;AAED,SAAO;CACR;CAED,MAAM,kBAAkB,MAAc;EACpC,gBAAgB;EAGhB,MAAM,cAAc,kBAAkB;AACtC,MAAI,gBAAgB,OAClB,QAAO;EAIT,IAAI,QAAQ;AACZ,SAAO,MAAM,MAAM,QAAQ;GACzB,MAAM,OAAO,MAAM;AAEnB,OAAI,CAAC,QAAQ,KAAK,KAAK,KAAK,IAAI,SAAS,OAAO,SAAS,IACvD;AAGF,OAAI,SAAS,MAAM;IACjB,SAAS;AACT,QAAI,MAAM,MAAM,QACd,SAAS,SAAS;GAErB,OACC,SAAS,SAAS;EAErB;AAED,SAAO,MAAM,QAAQ,QAAQ,IAAI;CAClC;CAED,MAAM,oBAAoB,CAACC,QAAyB;AAClD,OAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,IAC9B,KAAI,IAAI,OAAO,QAAQ,MAAM,KAAK,IAAI,IAAI,OAAO,MAC/C,QAAO;AAGX,SAAO;CACR;CAED,MAAM,cAAc,CAACC,OAAeC,YAAwC;AAE1E,MAAI,CAAC,kBAAkB,MAAM,CAC3B,QAAO;EAIT,IAAI,kBAAkB;AACtB,OAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,IAChC,KAAI,MAAM,OAAO,QAAQ,MAAM,KAAK,MAAM,IAAI,OAAO,OAAO;GAC1D,kBAAkB;AAClB;EACD;AAGH,MAAI,oBAAoB,GACtB,QAAO;EAGT,MAAM,QAAQ,MAAM,MAAM,GAAG,gBAAgB,CAAC,QAAQ,QAAQ,IAAI;EAClE,MAAM,aAAa,MAAM,MAAM,kBAAkB,EAAE;EAGnD,IAAIC,WAAqB;EACzB,IAAI,iBAAiB;AAErB,MAAI,WAAW,WAAW,KAAK,EAAE;GAC/B,WAAW;GACX,iBAAiB;EAClB,WAAU,WAAW,WAAW,KAAK,EAAE;GACtC,WAAW;GACX,iBAAiB;EAClB,WAAU,WAAW,WAAW,IAAI,EAAE;GACrC,WAAW;GACX,iBAAiB;EAClB,WAAU,WAAW,WAAW,IAAI,EAAE;GACrC,WAAW;GACX,iBAAiB;EAClB,WAAU,WAAW,WAAW,IAAI,EAAE;GACrC,WAAW;GACX,iBAAiB;EAClB;EAGD,IAAI,QAAQ,WAAW,MAAM,eAAe,CAAC,QAAQ,QAAQ,IAAI;AAGjE,MAAI,CAAC,OACH,QAAQ,iBAAiB;AAG3B,SAAO;GAAE,MAAM;GAAU;GAAO;GAAU;GAAO;EAAS;CAC3D;AAED,QAAO,MAAM,MAAM,QAAQ;AACzB,MAAI,OAAO,UAAU,WAAW;GAC9B,OAAO,KAAK;IACV,MAAM,YAAY;IAClB,SAAS,CAAC,uBAAuB,EAAE,UAAU,SAAS,CAAC;GACxD,EAAC;AACF;EACD;EAED,gBAAgB;AAEhB,MAAI,OAAO,MAAM,OACf;EAGF,MAAM,OAAO,MAAM;AAGnB,MAAI,SAAS,KAAK;GAChB,SAAS;GACT,OAAO,KAAK,EAAE,MAAM,SAAU,EAAC;AAC/B;EACD;AAED,MAAI,SAAS,KAAK;GAChB,SAAS;GACT,OAAO,KAAK,EAAE,MAAM,SAAU,EAAC;AAC/B;EACD;EAGD,MAAM,WAAW,MAAM,MAAM;AAC7B,MAAI,SAAS,OAAO,YAAY,CAAC,KAAK,KAAK,SAAS,EAAE;GACpD,SAAS;GACT,gBAAgB;GAGhB,MAAMC,gBAAc,kBAAkB;GACtC,MAAMC,UAAQD,kBAAgB,SAAYA,gBAAc,mBAAmB;GAG3E,MAAM,SAAS,YAAYC,SAAO,KAAK;AACvC,OAAI,QACF,OAAO,KAAK,OAAO;QACd;IAEL,MAAMC,kBAAgBD,QAAM,QAAQ,UAAU,KAAK;IACnD,OAAO,KAAK,EAAE,MAAM,MAAO,EAAC;IAC5B,OAAO,KAAK;KAAE,MAAM;KAAQ,OAAOC;IAAe,EAAC;GACpD;AACD;EACD;EAGD,MAAM,cAAc,kBAAkB;EACtC,MAAM,QAAQ,gBAAgB,SAAY,cAAc,kBAAkB,KAAK;AAE/E,MAAI,CAAC,OAAO;GACV,SAAS;AACT;EACD;EAGD,MAAM,aAAa,MAAM,aAAa;AACtC,MAAI,eAAe,OAAO;GACxB,OAAO,KAAK,EAAE,MAAM,MAAO,EAAC;AAC5B;EACD;AAED,MAAI,eAAe,MAAM;GACvB,OAAO,KAAK,EAAE,MAAM,KAAM,EAAC;AAC3B;EACD;AAED,MAAI,eAAe,OAAO;GACxB,OAAO,KAAK,EAAE,MAAM,MAAO,EAAC;AAC5B;EACD;AAGD,MAAI,gBAAgB,QAAW;GAC7B,MAAM,SAAS,YAAY,OAAO,MAAM;AACxC,OAAI,QAAQ;IACV,OAAO,KAAK,OAAO;AACnB;GACD;EACF;EAGD,MAAM,gBAAgB,MAAM,QAAQ,UAAU,KAAK;EACnD,OAAO,KAAK;GAAE,MAAM;GAAQ,OAAO;EAAe,EAAC;CACpD;CAED,OAAO,KAAK,EAAE,MAAM,MAAO,EAAC;AAE5B,QAAO;EAAE;EAAQ;CAAQ;AAC1B;;;;ACzRD,SAAgB,iBACd,EACE,OACA,WAAW,IACX,YAAY,KACZ,WAAW,OAMZ,EACY;CACb,MAAM,EAAE,QAAQ,QAAQ,iBAAiB,GAAG,SAAS;EAAE;EAAO;CAAW,EAAC;CAE1E,MAAM,EAAE,YAAY,QAAQ,cAAc,GAAG,gBAAgB;EAAE;EAAQ;CAAU,EAAC;CAElF,MAAM,SAAS,CAAC,GAAG,iBAAiB,GAAG,YAAa;AAEpD,KAAI,CAAC,SACH,QAAO;EACL;EACA;CACD;CAGH,MAAM,EAAE,YAAY,qBAAqB,GAAG,mBAAmB,EAAE,WAAY,EAAC;AAE9E,QAAO;EACL,YAAY;EACZ;CACD;AACF;AAED,SAAS,gBAAgB,EAAE,QAAQ,UAAiD,EAAe;CACjG,MAAMC,eAAwB,CAAE;CAEhC,IAAI,oBAAoB;CACxB,IAAI,eAAe;CAEnB,MAAM,OAAO,MAAa,OAAO,sBAAsB,EAAE,MAAM,MAAO;CACtE,MAAM,UAAU,MAAa,OAAO,wBAAwB,EAAE,MAAM,MAAO;CAE3E,MAAM,aAAa,MAAe;AAChC,MAAI,gBAAgB,UAAU;GAC5B,aAAa,KAAK;IAChB,MAAM,YAAY;IAClB,SAAS,CAAC,yBAAyB,EAAE,SAAS,SAAS,CAAC;GACzD,EAAC;AACF,UAAO;EACR;AACD,SAAO;CACR;CAGD,SAAS,yBAAiD;EACxD,MAAM,QAAQ,MAAM;AAEpB,MAAI,MAAM,SAAS,UAAU;GAC3B,SAAS;AAET,OAAI,CAAC,YAAY,CACf,QAAO;GAGT;GACA,MAAM,OAAO,mBAAmB;GAChC;AAEA,OAAI,MAAM,CAAC,SAAS,UAClB,SAAS;QAET,aAAa,KAAK;IAChB,MAAM,YAAY;IAClB,SAAS;GACV,EAAC;AAGJ,UAAO;EACR;AAED,MAAI,MAAM,SAAS,UAAU;GAC3B,SAAS;GACT,MAAMC,aAAyB;IAC7B,MAAM;IACN,OAAO,MAAM;IACb,UAAU,MAAM;IAChB,OAAO,MAAM;GACd;AAED,OAAI,MAAM,QACR,QAAO;IAAE,MAAM;IAAO,SAAS;GAAY;AAG7C,UAAO;EACR;AAED,MAAI,MAAM,SAAS,QAAQ;GACzB,SAAS;AACT,UAAO;IACL,MAAM;IACN,OAAO,MAAM;GACd;EACF;AAED,SAAO;CACR;CAED,SAAS,uBAA+C;AACtD,MAAI,MAAM,CAAC,SAAS,OAAO;GACzB,SAAS;AAET,OAAI,CAAC,YAAY,CACf,QAAO;GAGT;GACA,MAAM,UAAU,sBAAsB;GACtC;AAEA,OAAI,CAAC,SAAS;IACZ,aAAa,KAAK;KAChB,MAAM,YAAY;KAClB,SAAS;IACV,EAAC;AACF,WAAO;GACR;AAED,UAAO;IAAE,MAAM;IAAO;GAAS;EAChC;AAED,SAAO,wBAAwB;CAChC;CAED,SAAS,qBAA6C;EACpD,MAAMC,WAAyB,CAAE;AAEjC,SAAO,MAAM;GACX,MAAM,OAAO,MAAM;AAGnB,OAAI,KAAK,SAAS,SAAS,KAAK,SAAS,QAAQ,KAAK,SAAS,SAC7D;AAIF,OAAI,KAAK,SAAS,OAAO;IACvB,SAAS;AACT;GACD;GAED,MAAM,OAAO,sBAAsB;AACnC,OAAI,MACF,SAAS,KAAK,KAAK;EAEtB;AAED,MAAI,SAAS,WAAW,EACtB,QAAO;AAGT,MAAI,SAAS,WAAW,EACtB,QAAO,SAAS;AAGlB,SAAO;GAAE,MAAM;GAAO;EAAU;CACjC;CAED,SAAS,oBAA4C;EACnD,MAAM,OAAO,oBAAoB;AACjC,MAAI,CAAC,KACH,QAAO;EAGT,MAAMA,WAAyB,CAAC,IAAK;AAErC,SAAO,MAAM,CAAC,SAAS,MAAM;GAC3B,SAAS;GACT,MAAM,QAAQ,oBAAoB;AAClC,OAAI,OACF,SAAS,KAAK,MAAM;EAEvB;AAED,MAAI,SAAS,WAAW,EACtB,QAAO,SAAS;AAGlB,SAAO;GAAE,MAAM;GAAM;EAAU;CAChC;CAED,MAAM,aAAa,mBAAmB;AAGtC,QAAO,MAAM,CAAC,SAAS,UAAU;EAC/B,aAAa,KAAK;GAChB,SAAS;GACT,MAAM,YAAY;EACnB,EAAC;EACF,SAAS;CACV;AAED,QAAO;EACL,YAAY,cAAc,EAAE,MAAM,QAAS;EAC3C,QAAQ;CACT;AACF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@papra/search-parser",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "Search query parser library",
|
|
6
|
+
"author": "Corentin Thomasset <corentinth@proton.me> (https://corentin.tech)",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/papra-hq/papra",
|
|
11
|
+
"directory": "packages/search-parser"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/papra-hq/papra/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"papra",
|
|
18
|
+
"search",
|
|
19
|
+
"parser",
|
|
20
|
+
"query",
|
|
21
|
+
"filter",
|
|
22
|
+
"operator"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./dist/index.js",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"module": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": {},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@antfu/eslint-config": "^6.2.0",
|
|
37
|
+
"eslint": "^9.39.1",
|
|
38
|
+
"tsdown": "^0.13.4",
|
|
39
|
+
"typescript": "^5.9.3",
|
|
40
|
+
"vitest": "4.0.3"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"lint": "eslint .",
|
|
44
|
+
"lint:fix": "eslint --fix .",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:watch": "vitest watch",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"build": "tsdown",
|
|
49
|
+
"build:watch": "tsdown --watch",
|
|
50
|
+
"dev": "pnpm build:watch"
|
|
51
|
+
}
|
|
52
|
+
}
|