diff options
| -rw-r--r-- | docs/docs/04-using-karakeep/search-query-language.md | 12 | ||||
| -rw-r--r-- | packages/shared/searchQueryParser.test.ts | 133 | ||||
| -rw-r--r-- | packages/shared/searchQueryParser.ts | 12 |
3 files changed, 151 insertions, 6 deletions
diff --git a/docs/docs/04-using-karakeep/search-query-language.md b/docs/docs/04-using-karakeep/search-query-language.md index a1e830e8..de11e533 100644 --- a/docs/docs/04-using-karakeep/search-query-language.md +++ b/docs/docs/04-using-karakeep/search-query-language.md @@ -11,7 +11,7 @@ Karakeep provides a search query language to filter and find bookmarks. Here are - Use spaces to separate multiple conditions (implicit AND) - Use `and`/`or` keywords for explicit boolean logic -- Prefix qualifiers with `-` to negate them +- Prefix qualifiers with `-` or `!` to negate them (e.g., `-is:archived` or `!is:archived`) - Use parentheses `()` for grouping conditions (note that groups can't be negated) ## Qualifiers @@ -29,8 +29,8 @@ Here's a comprehensive table of all supported qualifiers: | `url:<value>` | Match bookmarks with URL substring | `url:example.com` | | `title:<value>` | Match bookmarks with title substring | `title:example` | | | Supports quoted strings for titles with spaces | `title:"my title"` | -| `#<tag>` | Match bookmarks with specific tag | `#important` | -| | Supports quoted strings for tags with spaces | `#"work in progress"` | +| `#<tag>` or `tag:<tag>` | Match bookmarks with specific tag | `#important` or `tag:important` | +| | Supports quoted strings for tags with spaces | `#"work in progress"` or `tag:"work in progress"` | | `list:<name>` | Match bookmarks in specific list | `list:reading` | | | Supports quoted strings for list names with spaces | `list:"to review"` | | `after:<date>` | Bookmarks created on or after date (YYYY-MM-DD) | `after:2023-01-01` | @@ -66,6 +66,12 @@ is:archived and (list:reading or #work) # Find bookmarks that are not favorited and not archived -is:fav -is:archived + +# Using ! as an alias for negation +!is:fav !is:archived + +# Using tag: as an alias for # +tag:important tag:"work in progress" ``` ## Text Search diff --git a/packages/shared/searchQueryParser.test.ts b/packages/shared/searchQueryParser.test.ts index aa11433f..3954e871 100644 --- a/packages/shared/searchQueryParser.test.ts +++ b/packages/shared/searchQueryParser.test.ts @@ -333,6 +333,139 @@ describe("Search Query Parser", () => { }, }); }); + test("! negation alias for -", () => { + // ! should work exactly like - for negation + expect(parseSearchQuery("!is:archived")).toEqual({ + result: "full", + text: "", + matcher: { + type: "archived", + archived: false, + }, + }); + expect(parseSearchQuery("!is:fav")).toEqual({ + result: "full", + text: "", + matcher: { + type: "favourited", + favourited: false, + }, + }); + expect(parseSearchQuery("!#my-tag")).toEqual({ + result: "full", + text: "", + matcher: { + type: "tagName", + tagName: "my-tag", + inverse: true, + }, + }); + expect(parseSearchQuery("!tag:my-tag")).toEqual({ + result: "full", + text: "", + matcher: { + type: "tagName", + tagName: "my-tag", + inverse: true, + }, + }); + expect(parseSearchQuery("!url:example.com")).toEqual({ + result: "full", + text: "", + matcher: { + type: "url", + url: "example.com", + inverse: true, + }, + }); + expect(parseSearchQuery("!list:my-list")).toEqual({ + result: "full", + text: "", + matcher: { + type: "listName", + listName: "my-list", + inverse: true, + }, + }); + expect(parseSearchQuery("!is:link")).toEqual({ + result: "full", + text: "", + matcher: { + type: "type", + typeName: BookmarkTypes.LINK, + inverse: true, + }, + }); + // Combined with complex queries + expect(parseSearchQuery("is:fav !is:archived")).toEqual({ + result: "full", + text: "", + matcher: { + type: "and", + matchers: [ + { + type: "favourited", + favourited: true, + }, + { + type: "archived", + archived: false, + }, + ], + }, + }); + }); + + test("tag: qualifier alias for #", () => { + // tag: should work exactly like # + expect(parseSearchQuery("tag:my-tag")).toEqual({ + result: "full", + text: "", + matcher: { + type: "tagName", + tagName: "my-tag", + inverse: false, + }, + }); + expect(parseSearchQuery("-tag:my-tag")).toEqual({ + result: "full", + text: "", + matcher: { + type: "tagName", + tagName: "my-tag", + inverse: true, + }, + }); + expect(parseSearchQuery('tag:"my tag"')).toEqual({ + result: "full", + text: "", + matcher: { + type: "tagName", + tagName: "my tag", + inverse: false, + }, + }); + expect(parseSearchQuery('-tag:"my tag"')).toEqual({ + result: "full", + text: "", + matcher: { + type: "tagName", + tagName: "my tag", + inverse: true, + }, + }); + // Tags starting with qualifiers should be treated correctly + expect(parseSearchQuery("tag:android")).toEqual({ + result: "full", + text: "", + matcher: { + type: "tagName", + tagName: "android", + inverse: false, + }, + }); + }); + test("date queries", () => { expect(parseSearchQuery("after:2023-10-12")).toEqual({ result: "full", diff --git a/packages/shared/searchQueryParser.ts b/packages/shared/searchQueryParser.ts index 7447593a..027a662f 100644 --- a/packages/shared/searchQueryParser.ts +++ b/packages/shared/searchQueryParser.ts @@ -33,6 +33,7 @@ enum TokenType { Space = "SPACE", Hash = "HASH", Minus = "MINUS", + Exclamation = "EXCLAMATION", } // Rules are in order of priority @@ -41,7 +42,7 @@ const lexerRules: [RegExp, TokenType][] = [ [/^\s+or/i, TokenType.Or], [/^#/, TokenType.Hash], - [/^(is|url|list|after|before|age|feed|title):/, TokenType.Qualifier], + [/^(is|url|list|after|before|age|feed|title|tag):/, TokenType.Qualifier], [/^"([^"]+)"/, TokenType.StringLiteral], @@ -49,6 +50,7 @@ const lexerRules: [RegExp, TokenType][] = [ [/^\)/, TokenType.RParen], [/^\s+/, TokenType.Space], [/^-/, TokenType.Minus], + [/^!/, TokenType.Exclamation], // This needs to be last as it matches a lot of stuff [/^[^ )(]+/, TokenType.Ident], @@ -116,7 +118,10 @@ const EXP = rule<TokenType, TextAndMatcher>(); MATCHER.setPattern( alt_sc( apply( - seq(opt(str("-")), kright(str("is:"), tok(TokenType.Ident))), + seq( + opt(alt(str("-"), str("!"))), + kright(str("is:"), tok(TokenType.Ident)), + ), ([minus, ident]) => { switch (ident.text) { case "fav": @@ -182,7 +187,7 @@ MATCHER.setPattern( ), apply( seq( - opt(str("-")), + opt(alt(str("-"), str("!"))), alt(tok(TokenType.Qualifier), tok(TokenType.Hash)), alt( apply(tok(TokenType.Ident), (tok) => { @@ -206,6 +211,7 @@ MATCHER.setPattern( matcher: { type: "title", title: ident, inverse: !!minus }, }; case "#": + case "tag:": return { text: "", matcher: { type: "tagName", tagName: ident, inverse: !!minus }, |
