aboutsummaryrefslogtreecommitdiffstats
path: root/packages/shared
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2024-12-31 13:58:23 +0000
committerMohamed Bassem <me@mbassem.com>2024-12-31 14:01:09 +0000
commit4deda9d477141e864f472ba95003e3974346f10d (patch)
treef2cee981872359b2d601a3009fd27e8dc4daec91 /packages/shared
parent17af22bb6df42e1f42809261db3eda45fb2ffe3f (diff)
downloadkarakeep-4deda9d477141e864f472ba95003e3974346f10d.tar.zst
feat: Add support for negative search terms
Diffstat (limited to 'packages/shared')
-rw-r--r--packages/shared/searchQueryParser.test.ts92
-rw-r--r--packages/shared/searchQueryParser.ts79
-rw-r--r--packages/shared/types/search.ts5
3 files changed, 132 insertions, 44 deletions
diff --git a/packages/shared/searchQueryParser.test.ts b/packages/shared/searchQueryParser.test.ts
index 428d5929..5bbb3f77 100644
--- a/packages/shared/searchQueryParser.test.ts
+++ b/packages/shared/searchQueryParser.test.ts
@@ -12,7 +12,7 @@ describe("Search Query Parser", () => {
archived: true,
},
});
- expect(parseSearchQuery("is:not_archived")).toEqual({
+ expect(parseSearchQuery("-is:archived")).toEqual({
result: "full",
text: "",
matcher: {
@@ -28,7 +28,7 @@ describe("Search Query Parser", () => {
favourited: true,
},
});
- expect(parseSearchQuery("is:not_fav")).toEqual({
+ expect(parseSearchQuery("-is:fav")).toEqual({
result: "full",
text: "",
matcher: {
@@ -45,6 +45,16 @@ describe("Search Query Parser", () => {
matcher: {
type: "url",
url: "https://example.com",
+ inverse: false,
+ },
+ });
+ expect(parseSearchQuery("-url:https://example.com")).toEqual({
+ result: "full",
+ text: "",
+ matcher: {
+ type: "url",
+ url: "https://example.com",
+ inverse: true,
},
});
expect(parseSearchQuery('url:"https://example.com"')).toEqual({
@@ -53,6 +63,16 @@ describe("Search Query Parser", () => {
matcher: {
type: "url",
url: "https://example.com",
+ inverse: false,
+ },
+ });
+ expect(parseSearchQuery('-url:"https://example.com"')).toEqual({
+ result: "full",
+ text: "",
+ matcher: {
+ type: "url",
+ url: "https://example.com",
+ inverse: true,
},
});
expect(parseSearchQuery("#my-tag")).toEqual({
@@ -61,6 +81,16 @@ describe("Search Query Parser", () => {
matcher: {
type: "tagName",
tagName: "my-tag",
+ inverse: false,
+ },
+ });
+ expect(parseSearchQuery("-#my-tag")).toEqual({
+ result: "full",
+ text: "",
+ matcher: {
+ type: "tagName",
+ tagName: "my-tag",
+ inverse: true,
},
});
expect(parseSearchQuery('#"my tag"')).toEqual({
@@ -69,6 +99,16 @@ describe("Search Query Parser", () => {
matcher: {
type: "tagName",
tagName: "my tag",
+ inverse: false,
+ },
+ });
+ expect(parseSearchQuery('-#"my tag"')).toEqual({
+ result: "full",
+ text: "",
+ matcher: {
+ type: "tagName",
+ tagName: "my tag",
+ inverse: true,
},
});
expect(parseSearchQuery("list:my-list")).toEqual({
@@ -77,6 +117,16 @@ describe("Search Query Parser", () => {
matcher: {
type: "listName",
listName: "my-list",
+ inverse: false,
+ },
+ });
+ expect(parseSearchQuery("-list:my-list")).toEqual({
+ result: "full",
+ text: "",
+ matcher: {
+ type: "listName",
+ listName: "my-list",
+ inverse: true,
},
});
expect(parseSearchQuery('list:"my list"')).toEqual({
@@ -85,6 +135,16 @@ describe("Search Query Parser", () => {
matcher: {
type: "listName",
listName: "my list",
+ inverse: false,
+ },
+ });
+ expect(parseSearchQuery('-list:"my list"')).toEqual({
+ result: "full",
+ text: "",
+ matcher: {
+ type: "listName",
+ listName: "my list",
+ inverse: true,
},
});
});
@@ -95,6 +155,16 @@ describe("Search Query Parser", () => {
matcher: {
type: "dateAfter",
dateAfter: new Date("2023-10-12"),
+ inverse: false,
+ },
+ });
+ expect(parseSearchQuery("-after:2023-10-12")).toEqual({
+ result: "full",
+ text: "",
+ matcher: {
+ type: "dateAfter",
+ dateAfter: new Date("2023-10-12"),
+ inverse: true,
},
});
expect(parseSearchQuery("before:2023-10-12")).toEqual({
@@ -103,12 +173,22 @@ describe("Search Query Parser", () => {
matcher: {
type: "dateBefore",
dateBefore: new Date("2023-10-12"),
+ inverse: false,
+ },
+ });
+ expect(parseSearchQuery("-before:2023-10-12")).toEqual({
+ result: "full",
+ text: "",
+ matcher: {
+ type: "dateBefore",
+ dateBefore: new Date("2023-10-12"),
+ inverse: true,
},
});
});
test("complex queries", () => {
- expect(parseSearchQuery("is:fav is:archived")).toEqual({
+ expect(parseSearchQuery("is:fav -is:archived")).toEqual({
result: "full",
text: "",
matcher: {
@@ -120,7 +200,7 @@ describe("Search Query Parser", () => {
},
{
type: "archived",
- archived: true,
+ archived: false,
},
],
},
@@ -143,6 +223,7 @@ describe("Search Query Parser", () => {
{
type: "tagName",
tagName: "my-tag",
+ inverse: false,
},
],
},
@@ -170,6 +251,7 @@ describe("Search Query Parser", () => {
{
type: "tagName",
tagName: "my-tag",
+ inverse: false,
},
],
},
@@ -197,6 +279,7 @@ describe("Search Query Parser", () => {
{
type: "tagName",
tagName: "my-tag",
+ inverse: false,
},
],
},
@@ -237,6 +320,7 @@ describe("Search Query Parser", () => {
{
type: "tagName",
tagName: "my-tag",
+ inverse: false,
},
],
},
diff --git a/packages/shared/searchQueryParser.ts b/packages/shared/searchQueryParser.ts
index faf74d08..02129c14 100644
--- a/packages/shared/searchQueryParser.ts
+++ b/packages/shared/searchQueryParser.ts
@@ -5,6 +5,7 @@ import {
kmid,
kright,
lrec_sc,
+ opt,
rule,
seq,
str,
@@ -28,6 +29,7 @@ enum TokenType {
RParen = "RPAREN",
Space = "SPACE",
Hash = "HASH",
+ Minus = "MINUS",
}
// Rules are in order of priority
@@ -43,6 +45,7 @@ const lexerRules: [RegExp, TokenType][] = [
[/^\(/, TokenType.LParen],
[/^\)/, TokenType.RParen],
[/^\s+/, TokenType.Space],
+ [/^-/, TokenType.Minus],
// This needs to be last as it matches a lot of stuff
[/^[^ )(]+/, TokenType.Ident],
@@ -109,38 +112,32 @@ const EXP = rule<TokenType, TextAndMatcher>();
MATCHER.setPattern(
alt_sc(
- apply(kright(str("is:"), tok(TokenType.Ident)), (toks) => {
- switch (toks.text) {
- case "fav":
- return {
- text: "",
- matcher: { type: "favourited", favourited: true },
- };
- case "not_fav":
- return {
- text: "",
- matcher: { type: "favourited", favourited: false },
- };
- case "archived":
- return {
- text: "",
- matcher: { type: "archived", archived: true },
- };
- case "not_archived":
- return {
- text: "",
- matcher: { type: "archived", archived: false },
- };
- default:
- // If the token is not known, emit it as pure text
- return {
- text: `is:${toks.text}`,
- matcher: undefined,
- };
- }
- }),
+ apply(
+ seq(opt(str("-")), kright(str("is:"), tok(TokenType.Ident))),
+ ([minus, ident]) => {
+ switch (ident.text) {
+ case "fav":
+ return {
+ text: "",
+ matcher: { type: "favourited", favourited: !minus },
+ };
+ case "archived":
+ return {
+ text: "",
+ matcher: { type: "archived", archived: !minus },
+ };
+ default:
+ // If the token is not known, emit it as pure text
+ return {
+ text: `${minus?.text ?? ""}is:${ident.text}`,
+ matcher: undefined,
+ };
+ }
+ },
+ ),
apply(
seq(
+ opt(str("-")),
alt(tok(TokenType.Qualifier), tok(TokenType.Hash)),
alt(
apply(tok(TokenType.Ident), (tok) => {
@@ -151,22 +148,22 @@ MATCHER.setPattern(
}),
),
),
- (toks) => {
- switch (toks[0].text) {
+ ([minus, qualifier, ident]) => {
+ switch (qualifier.text) {
case "url:":
return {
text: "",
- matcher: { type: "url", url: toks[1] },
+ matcher: { type: "url", url: ident, inverse: !!minus },
};
case "#":
return {
text: "",
- matcher: { type: "tagName", tagName: toks[1] },
+ matcher: { type: "tagName", tagName: ident, inverse: !!minus },
};
case "list:":
return {
text: "",
- matcher: { type: "listName", listName: toks[1] },
+ matcher: { type: "listName", listName: ident, inverse: !!minus },
};
case "after:":
try {
@@ -174,13 +171,14 @@ MATCHER.setPattern(
text: "",
matcher: {
type: "dateAfter",
- dateAfter: z.coerce.date().parse(toks[1]),
+ dateAfter: z.coerce.date().parse(ident),
+ inverse: !!minus,
},
};
} catch (e) {
return {
// If parsing the date fails, emit it as pure text
- text: toks[0].text + toks[1],
+ text: (minus?.text ?? "") + qualifier.text + ident,
matcher: undefined,
};
}
@@ -190,20 +188,21 @@ MATCHER.setPattern(
text: "",
matcher: {
type: "dateBefore",
- dateBefore: z.coerce.date().parse(toks[1]),
+ dateBefore: z.coerce.date().parse(ident),
+ inverse: !!minus,
},
};
} catch (e) {
return {
// If parsing the date fails, emit it as pure text
- text: toks[0].text + toks[1],
+ text: (minus?.text ?? "") + qualifier.text + ident,
matcher: undefined,
};
}
default:
// If the token is not known, emit it as pure text
return {
- text: toks[0].text + toks[1],
+ text: (minus?.text ?? "") + qualifier.text + ident,
matcher: undefined,
};
}
diff --git a/packages/shared/types/search.ts b/packages/shared/types/search.ts
index d430dad5..4d947a05 100644
--- a/packages/shared/types/search.ts
+++ b/packages/shared/types/search.ts
@@ -3,11 +3,13 @@ import { z } from "zod";
const zTagNameMatcher = z.object({
type: z.literal("tagName"),
tagName: z.string(),
+ inverse: z.boolean(),
});
const zListNameMatcher = z.object({
type: z.literal("listName"),
listName: z.string(),
+ inverse: z.boolean(),
});
const zArchivedMatcher = z.object({
@@ -18,6 +20,7 @@ const zArchivedMatcher = z.object({
const urlMatcher = z.object({
type: z.literal("url"),
url: z.string(),
+ inverse: z.boolean(),
});
const zFavouritedMatcher = z.object({
@@ -28,11 +31,13 @@ const zFavouritedMatcher = z.object({
const zDateAfterMatcher = z.object({
type: z.literal("dateAfter"),
dateAfter: z.date(),
+ inverse: z.boolean(),
});
const zDateBeforeMatcher = z.object({
type: z.literal("dateBefore"),
dateBefore: z.date(),
+ inverse: z.boolean(),
});
const zNonRecursiveMatcher = z.union([