diff --git a/internal/cmd/print.go b/internal/cmd/print.go index e90cd5b..e1c8ac2 100644 --- a/internal/cmd/print.go +++ b/internal/cmd/print.go @@ -25,6 +25,7 @@ func printCmd() *cobra.Command { cmd.Flags().BoolP("index-only", "i", false, "Only print the index of bookmarks") cmd.Flags().StringP("search", "s", "", "Search bookmark with specified keyword") cmd.Flags().StringSliceP("tags", "t", []string{}, "Print bookmarks with matching tag(s)") + cmd.Flags().StringSliceP("exclude-tags", "e", []string{}, "Print bookmarks without these tag(s)") return cmd } @@ -36,6 +37,7 @@ func printHandler(cmd *cobra.Command, args []string) { useJSON, _ := cmd.Flags().GetBool("json") indexOnly, _ := cmd.Flags().GetBool("index-only") orderLatest, _ := cmd.Flags().GetBool("latest") + excludedTags, _ := cmd.Flags().GetStringSlice("exclude-tags") // Convert args to ids ids, err := parseStrIndices(args) @@ -51,10 +53,11 @@ func printHandler(cmd *cobra.Command, args []string) { } searchOptions := database.GetBookmarksOptions{ - IDs: ids, - Tags: tags, - Keyword: keyword, - OrderMethod: orderMethod, + IDs: ids, + Tags: tags, + ExcludedTags: excludedTags, + Keyword: keyword, + OrderMethod: orderMethod, } bookmarks, err := db.GetBookmarks(searchOptions) diff --git a/internal/database/database.go b/internal/database/database.go index e41238c..3e69750 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -20,13 +20,14 @@ const ( // GetBookmarksOptions is options for fetching bookmarks from database. type GetBookmarksOptions struct { - IDs []int - Tags []string - Keyword string - WithContent bool - OrderMethod OrderMethod - Limit int - Offset int + IDs []int + Tags []string + ExcludedTags []string + Keyword string + WithContent bool + OrderMethod OrderMethod + Limit int + Offset int } // DB is interface for accessing and manipulating data in database. diff --git a/internal/database/mysql.go b/internal/database/mysql.go index 34ca0cb..c53a75a 100644 --- a/internal/database/mysql.go +++ b/internal/database/mysql.go @@ -228,28 +228,72 @@ func (db *MySQLDatabase) GetBookmarks(opts GetBookmarksOptions) ([]model.Bookmar // Add where clause args := []interface{}{} + // Add where clause for IDs if len(opts.IDs) > 0 { query += ` AND id IN (?)` args = append(args, opts.IDs) } + // Add where clause for search keyword if opts.Keyword != "" { query += ` AND ( url LIKE ? OR MATCH(title, excerpt, content) AGAINST (? IN BOOLEAN MODE) )` - args = append(args, - "%"+opts.Keyword+"%", - opts.Keyword) + args = append(args, "%"+opts.Keyword+"%", opts.Keyword) } + // Add where clause for tags. + // First we check for * in excluded and included tags, + // which means all tags will be excluded and included, respectively. + excludeAllTags := false + for _, excludedTag := range opts.ExcludedTags { + if excludedTag == "*" { + excludeAllTags = true + opts.ExcludedTags = []string{} + break + } + } + + includeAllTags := false + for _, includedTag := range opts.Tags { + if includedTag == "*" { + includeAllTags = true + opts.Tags = []string{} + break + } + } + + // If all tags excluded, we will only show bookmark without tags. + // In other hand, if all tags included, we will only show bookmark with tags. + if excludeAllTags { + query += ` AND id NOT IN (SELECT DISTINCT bookmark_id FROM bookmark_tag)` + } else if includeAllTags { + query += ` AND id IN (SELECT DISTINCT bookmark_id FROM bookmark_tag)` + } + + // Now we only need to find the normal tags if len(opts.Tags) > 0 { query += ` AND id IN ( - SELECT bookmark_id FROM bookmark_tag - WHERE tag_id IN (SELECT id FROM tag WHERE name IN (?)))` + SELECT bt.bookmark_id + FROM bookmark_tag bt + LEFT JOIN tag t ON bt.tag_id = t.id + WHERE t.name IN(?) + GROUP BY bt.bookmark_id + HAVING COUNT(bt.bookmark_id) = ?)` - args = append(args, opts.Tags) + args = append(args, opts.Tags, len(opts.Tags)) + } + + if len(opts.ExcludedTags) > 0 { + query += ` AND id NOT IN ( + SELECT DISTINCT bt.bookmark_id + FROM bookmark_tag bt + LEFT JOIN tag t ON bt.tag_id = t.id + WHERE t.name IN(?))` + + args = append(args, opts.ExcludedTags) } // Add order clause @@ -312,11 +356,13 @@ func (db *MySQLDatabase) GetBookmarksCount(opts GetBookmarksOptions) (int, error // Add where clause args := []interface{}{} + // Add where clause for IDs if len(opts.IDs) > 0 { query += ` AND id IN (?)` args = append(args, opts.IDs) } + // Add where clause for search keyword if opts.Keyword != "" { query += ` AND ( url LIKE ? OR @@ -328,12 +374,56 @@ func (db *MySQLDatabase) GetBookmarksCount(opts GetBookmarksOptions) (int, error opts.Keyword) } + // Add where clause for tags. + // First we check for * in excluded and included tags, + // which means all tags will be excluded and included, respectively. + excludeAllTags := false + for _, excludedTag := range opts.ExcludedTags { + if excludedTag == "*" { + excludeAllTags = true + opts.ExcludedTags = []string{} + break + } + } + + includeAllTags := false + for _, includedTag := range opts.Tags { + if includedTag == "*" { + includeAllTags = true + opts.Tags = []string{} + break + } + } + + // If all tags excluded, we will only show bookmark without tags. + // In other hand, if all tags included, we will only show bookmark with tags. + if excludeAllTags { + query += ` AND id NOT IN (SELECT DISTINCT bookmark_id FROM bookmark_tag)` + } else if includeAllTags { + query += ` AND id IN (SELECT DISTINCT bookmark_id FROM bookmark_tag)` + } + + // Now we only need to find the normal tags if len(opts.Tags) > 0 { query += ` AND id IN ( - SELECT bookmark_id FROM bookmark_tag - WHERE tag_id IN (SELECT id FROM tag WHERE name IN (?)))` + SELECT bt.bookmark_id + FROM bookmark_tag bt + LEFT JOIN tag t ON bt.tag_id = t.id + WHERE t.name IN(?) + GROUP BY bt.bookmark_id + HAVING COUNT(bt.bookmark_id) = ?)` - args = append(args, opts.Tags) + args = append(args, opts.Tags, len(opts.Tags)) + } + + if len(opts.ExcludedTags) > 0 { + query += ` AND id NOT IN ( + SELECT DISTINCT bt.bookmark_id + FROM bookmark_tag bt + LEFT JOIN tag t ON bt.tag_id = t.id + WHERE t.name IN(?))` + + args = append(args, opts.ExcludedTags) } // Expand query, because some of the args might be an array diff --git a/internal/database/sqlite.go b/internal/database/sqlite.go index 4e933eb..198472b 100644 --- a/internal/database/sqlite.go +++ b/internal/database/sqlite.go @@ -224,11 +224,13 @@ func (db *SQLiteDatabase) GetBookmarks(opts GetBookmarksOptions) ([]model.Bookma // Add where clause args := []interface{}{} + // Add where clause for IDs if len(opts.IDs) > 0 { query += ` AND b.id IN (?)` args = append(args, opts.IDs) } + // Add where clause for search keyword if opts.Keyword != "" { query += ` AND (b.url LIKE ? OR b.excerpt LIKE ? OR b.id IN ( SELECT docid id @@ -242,12 +244,56 @@ func (db *SQLiteDatabase) GetBookmarks(opts GetBookmarksOptions) ([]model.Bookma opts.Keyword) } + // Add where clause for tags. + // First we check for * in excluded and included tags, + // which means all tags will be excluded and included, respectively. + excludeAllTags := false + for _, excludedTag := range opts.ExcludedTags { + if excludedTag == "*" { + excludeAllTags = true + opts.ExcludedTags = []string{} + break + } + } + + includeAllTags := false + for _, includedTag := range opts.Tags { + if includedTag == "*" { + includeAllTags = true + opts.Tags = []string{} + break + } + } + + // If all tags excluded, we will only show bookmark without tags. + // In other hand, if all tags included, we will only show bookmark with tags. + if excludeAllTags { + query += ` AND b.id NOT IN (SELECT DISTINCT bookmark_id FROM bookmark_tag)` + } else if includeAllTags { + query += ` AND b.id IN (SELECT DISTINCT bookmark_id FROM bookmark_tag)` + } + + // Now we only need to find the normal tags if len(opts.Tags) > 0 { query += ` AND b.id IN ( - SELECT bookmark_id FROM bookmark_tag - WHERE tag_id IN (SELECT id FROM tag WHERE name IN (?)))` + SELECT bt.bookmark_id + FROM bookmark_tag bt + LEFT JOIN tag t ON bt.tag_id = t.id + WHERE t.name IN(?) + GROUP BY bt.bookmark_id + HAVING COUNT(bt.bookmark_id) = ?)` - args = append(args, opts.Tags) + args = append(args, opts.Tags, len(opts.Tags)) + } + + if len(opts.ExcludedTags) > 0 { + query += ` AND b.id NOT IN ( + SELECT DISTINCT bt.bookmark_id + FROM bookmark_tag bt + LEFT JOIN tag t ON bt.tag_id = t.id + WHERE t.name IN(?))` + + args = append(args, opts.ExcludedTags) } // Add order clause @@ -313,11 +359,13 @@ func (db *SQLiteDatabase) GetBookmarksCount(opts GetBookmarksOptions) (int, erro // Add where clause args := []interface{}{} + // Add where clause for IDs if len(opts.IDs) > 0 { query += ` AND b.id IN (?)` args = append(args, opts.IDs) } + // Add where clause for search keyword if opts.Keyword != "" { query += ` AND (b.url LIKE ? OR b.excerpt LIKE ? OR b.id IN ( SELECT docid id @@ -331,12 +379,56 @@ func (db *SQLiteDatabase) GetBookmarksCount(opts GetBookmarksOptions) (int, erro opts.Keyword) } + // Add where clause for tags. + // First we check for * in excluded and included tags, + // which means all tags will be excluded and included, respectively. + excludeAllTags := false + for _, excludedTag := range opts.ExcludedTags { + if excludedTag == "*" { + excludeAllTags = true + opts.ExcludedTags = []string{} + break + } + } + + includeAllTags := false + for _, includedTag := range opts.Tags { + if includedTag == "*" { + includeAllTags = true + opts.Tags = []string{} + break + } + } + + // If all tags excluded, we will only show bookmark without tags. + // In other hand, if all tags included, we will only show bookmark with tags. + if excludeAllTags { + query += ` AND b.id NOT IN (SELECT DISTINCT bookmark_id FROM bookmark_tag)` + } else if includeAllTags { + query += ` AND b.id IN (SELECT DISTINCT bookmark_id FROM bookmark_tag)` + } + + // Now we only need to find the normal tags if len(opts.Tags) > 0 { query += ` AND b.id IN ( - SELECT bookmark_id FROM bookmark_tag - WHERE tag_id IN (SELECT id FROM tag WHERE name IN (?)))` + SELECT bt.bookmark_id + FROM bookmark_tag bt + LEFT JOIN tag t ON bt.tag_id = t.id + WHERE t.name IN(?) + GROUP BY bt.bookmark_id + HAVING COUNT(bt.bookmark_id) = ?)` - args = append(args, opts.Tags) + args = append(args, opts.Tags, len(opts.Tags)) + } + + if len(opts.ExcludedTags) > 0 { + query += ` AND b.id NOT IN ( + SELECT DISTINCT bt.bookmark_id + FROM bookmark_tag bt + LEFT JOIN tag t ON bt.tag_id = t.id + WHERE t.name IN(?))` + + args = append(args, opts.ExcludedTags) } // Expand query, because some of the args might be an array diff --git a/internal/view/js/component/bookmark.js b/internal/view/js/component/bookmark.js index 0a653d9..1748f20 100644 --- a/internal/view/js/component/bookmark.js +++ b/internal/view/js/component/bookmark.js @@ -11,7 +11,7 @@ var template = `
{{id}}