From b05a7531b76d580fc2378d3fed12f57e5f4b35b1 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 8 Feb 2026 15:53:14 +0000 Subject: feat: add source filter to query language (#2465) * feat: add source filter to query language Co-Authored-By: Claude Opus 4.6 * autocomplete source --------- Co-authored-by: Claude Opus 4.6 --- packages/shared/searchQueryParser.test.ts | 36 +++++++++++++++++++++++++++++++ packages/shared/searchQueryParser.ts | 24 +++++++++++++++++++-- packages/shared/types/search.ts | 10 ++++++++- 3 files changed, 67 insertions(+), 3 deletions(-) (limited to 'packages/shared') diff --git a/packages/shared/searchQueryParser.test.ts b/packages/shared/searchQueryParser.test.ts index 3954e871..37275284 100644 --- a/packages/shared/searchQueryParser.test.ts +++ b/packages/shared/searchQueryParser.test.ts @@ -332,6 +332,42 @@ describe("Search Query Parser", () => { inverse: true, }, }); + expect(parseSearchQuery("source:rss")).toEqual({ + result: "full", + text: "", + matcher: { + type: "source", + source: "rss", + inverse: false, + }, + }); + expect(parseSearchQuery("-source:rss")).toEqual({ + result: "full", + text: "", + matcher: { + type: "source", + source: "rss", + inverse: true, + }, + }); + expect(parseSearchQuery("source:web")).toEqual({ + result: "full", + text: "", + matcher: { + type: "source", + source: "web", + inverse: false, + }, + }); + expect(parseSearchQuery("-source:web")).toEqual({ + result: "full", + text: "", + matcher: { + type: "source", + source: "web", + inverse: true, + }, + }); }); test("! negation alias for -", () => { // ! should work exactly like - for negation diff --git a/packages/shared/searchQueryParser.ts b/packages/shared/searchQueryParser.ts index 027a662f..7eb3b185 100644 --- a/packages/shared/searchQueryParser.ts +++ b/packages/shared/searchQueryParser.ts @@ -16,7 +16,7 @@ import { } from "typescript-parsec"; import { z } from "zod"; -import { BookmarkTypes } from "./types/bookmarks"; +import { BookmarkTypes, zBookmarkSourceSchema } from "./types/bookmarks"; import { Matcher } from "./types/search"; import { parseRelativeDate } from "./utils/relativeDateUtils"; @@ -42,7 +42,10 @@ const lexerRules: [RegExp, TokenType][] = [ [/^\s+or/i, TokenType.Or], [/^#/, TokenType.Hash], - [/^(is|url|list|after|before|age|feed|title|tag):/, TokenType.Qualifier], + [ + /^(is|url|list|after|before|age|feed|title|tag|source):/, + TokenType.Qualifier, + ], [/^"([^"]+)"/, TokenType.StringLiteral], @@ -230,6 +233,23 @@ MATCHER.setPattern( inverse: !!minus, }, }; + case "source:": { + const parsed = zBookmarkSourceSchema.safeParse(ident); + if (!parsed.success) { + return { + text: (minus?.text ?? "") + qualifier.text + ident, + matcher: undefined, + }; + } + return { + text: "", + matcher: { + type: "source", + source: parsed.data, + inverse: !!minus, + }, + }; + } case "after:": try { return { diff --git a/packages/shared/types/search.ts b/packages/shared/types/search.ts index c29270b8..b653d883 100644 --- a/packages/shared/types/search.ts +++ b/packages/shared/types/search.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { BookmarkTypes } from "./bookmarks"; +import { BookmarkTypes, zBookmarkSourceSchema } from "./bookmarks"; const zTagNameMatcher = z.object({ type: z.literal("tagName"), @@ -88,6 +88,12 @@ const zBrokenLinksMatcher = z.object({ brokenLinks: z.boolean(), }); +const zSourceMatcher = z.object({ + type: z.literal("source"), + source: zBookmarkSourceSchema, + inverse: z.boolean(), +}); + const zNonRecursiveMatcher = z.union([ zTagNameMatcher, zListNameMatcher, @@ -103,6 +109,7 @@ const zNonRecursiveMatcher = z.union([ zTypeMatcher, zRssFeedNameMatcher, zBrokenLinksMatcher, + zSourceMatcher, ]); type NonRecursiveMatcher = z.infer; @@ -127,6 +134,7 @@ export const zMatcherSchema: z.ZodType = z.lazy(() => { zTypeMatcher, zRssFeedNameMatcher, zBrokenLinksMatcher, + zSourceMatcher, z.object({ type: z.literal("and"), matchers: z.array(zMatcherSchema), -- cgit v1.2.3-70-g09d2