aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/docs/04-using-karakeep/search-query-language.md12
-rw-r--r--packages/shared/searchQueryParser.test.ts133
-rw-r--r--packages/shared/searchQueryParser.ts12
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 },