aboutsummaryrefslogtreecommitdiffstats
path: root/packages/shared
diff options
context:
space:
mode:
authorBrandon Wong <29965003+brandonw3612@users.noreply.github.com>2025-04-13 22:50:45 +0200
committerGitHub <noreply@github.com>2025-04-13 21:50:45 +0100
commit95f504c0bc0b9ec0930b6c6facefc1a8ea093192 (patch)
tree967221d7f4f5b716cc8fd3d92b5869bacf1d6089 /packages/shared
parent5bdb2d944a08f63772497e203f47533ffb640d82 (diff)
downloadkarakeep-95f504c0bc0b9ec0930b6c6facefc1a8ea093192.tar.zst
feat: add support for filtering by bookmark age (#1228)
Diffstat (limited to 'packages/shared')
-rw-r--r--packages/shared/searchQueryParser.test.ts26
-rw-r--r--packages/shared/searchQueryParser.ts20
-rw-r--r--packages/shared/types/search.ts11
-rw-r--r--packages/shared/utils/relativeDateUtils.ts42
4 files changed, 98 insertions, 1 deletions
diff --git a/packages/shared/searchQueryParser.test.ts b/packages/shared/searchQueryParser.test.ts
index ff69756c..7a86ecb5 100644
--- a/packages/shared/searchQueryParser.test.ts
+++ b/packages/shared/searchQueryParser.test.ts
@@ -319,6 +319,32 @@ describe("Search Query Parser", () => {
},
});
});
+ test("age queries", () => {
+ expect(parseSearchQuery("age:<3d")).toEqual({
+ result: "full",
+ text: "",
+ matcher: {
+ type: "age",
+ relativeDate: {
+ direction: "newer",
+ amount: 3,
+ unit: "day",
+ },
+ },
+ });
+ expect(parseSearchQuery("age:>2y")).toEqual({
+ result: "full",
+ text: "",
+ matcher: {
+ type: "age",
+ relativeDate: {
+ direction: "older",
+ amount: 2,
+ unit: "year",
+ },
+ },
+ });
+ });
test("complex queries", () => {
expect(parseSearchQuery("is:fav -is:archived")).toEqual({
diff --git a/packages/shared/searchQueryParser.ts b/packages/shared/searchQueryParser.ts
index d4e2bf2b..80f033b0 100644
--- a/packages/shared/searchQueryParser.ts
+++ b/packages/shared/searchQueryParser.ts
@@ -18,6 +18,7 @@ import { z } from "zod";
import { BookmarkTypes } from "./types/bookmarks";
import { Matcher } from "./types/search";
+import { parseRelativeDate } from "./utils/relativeDateUtils";
enum TokenType {
And = "AND",
@@ -40,7 +41,7 @@ const lexerRules: [RegExp, TokenType][] = [
[/^\s+or/i, TokenType.Or],
[/^#/, TokenType.Hash],
- [/^(is|url|list|after|before|feed):/, TokenType.Qualifier],
+ [/^(is|url|list|after|before|age|feed):/, TokenType.Qualifier],
[/^"([^"]+)"/, TokenType.StringLiteral],
@@ -247,6 +248,23 @@ MATCHER.setPattern(
matcher: undefined,
};
}
+ case "age:":
+ try {
+ const { direction, amount, unit } = parseRelativeDate(ident);
+ return {
+ text: "",
+ matcher: {
+ type: "age",
+ relativeDate: { direction, amount, unit },
+ },
+ };
+ } catch (e) {
+ return {
+ // If parsing the relative time fails, emit it as pure text
+ text: (minus?.text ?? "") + qualifier.text + ident,
+ matcher: undefined,
+ };
+ }
default:
// If the token is not known, emit it as pure text
return {
diff --git a/packages/shared/types/search.ts b/packages/shared/types/search.ts
index 533eea25..4c64c0f5 100644
--- a/packages/shared/types/search.ts
+++ b/packages/shared/types/search.ts
@@ -48,6 +48,15 @@ const zDateBeforeMatcher = z.object({
inverse: z.boolean(),
});
+const zAgeMatcher = z.object({
+ type: z.literal("age"),
+ relativeDate: z.object({
+ direction: z.enum(["newer", "older"]),
+ amount: z.number(),
+ unit: z.enum(["day", "week", "month", "year"]),
+ }),
+});
+
const zIsTaggedMatcher = z.object({
type: z.literal("tagged"),
tagged: z.boolean(),
@@ -76,6 +85,7 @@ const zNonRecursiveMatcher = z.union([
zFavouritedMatcher,
zDateAfterMatcher,
zDateBeforeMatcher,
+ zAgeMatcher,
zIsTaggedMatcher,
zIsInListMatcher,
zTypeMatcher,
@@ -97,6 +107,7 @@ export const zMatcherSchema: z.ZodType<Matcher> = z.lazy(() => {
zFavouritedMatcher,
zDateAfterMatcher,
zDateBeforeMatcher,
+ zAgeMatcher,
zIsTaggedMatcher,
zIsInListMatcher,
zTypeMatcher,
diff --git a/packages/shared/utils/relativeDateUtils.ts b/packages/shared/utils/relativeDateUtils.ts
new file mode 100644
index 00000000..437c3ea8
--- /dev/null
+++ b/packages/shared/utils/relativeDateUtils.ts
@@ -0,0 +1,42 @@
+interface RelativeDate {
+ direction: "newer" | "older";
+ amount: number;
+ unit: "day" | "week" | "month" | "year";
+}
+
+const parseRelativeDate = (date: string): RelativeDate => {
+ const match = date.match(/^([<>])(\d+)([dwmy])$/);
+ if (!match) {
+ throw new Error(`Invalid relative date format: ${date}`);
+ }
+ const direction = match[1] === "<" ? "newer" : "older";
+ const amount = parseInt(match[2], 10);
+ const unit = {
+ d: "day",
+ w: "week",
+ m: "month",
+ y: "year",
+ }[match[3]] as "day" | "week" | "month" | "year";
+ return { direction, amount, unit };
+};
+
+const toAbsoluteDate = (relativeDate: RelativeDate): Date => {
+ const date = new Date();
+ switch (relativeDate.unit) {
+ case "day":
+ date.setDate(date.getDate() - relativeDate.amount);
+ break;
+ case "week":
+ date.setDate(date.getDate() - relativeDate.amount * 7);
+ break;
+ case "month":
+ date.setMonth(date.getMonth() - relativeDate.amount);
+ break;
+ case "year":
+ date.setFullYear(date.getFullYear() - relativeDate.amount);
+ break;
+ }
+ return date;
+};
+
+export { parseRelativeDate, toAbsoluteDate };