@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 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.
@@ -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":"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"}
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
+ }