marky

Lightweight bookmarking + plaintext archive
Log | Files | Refs | README | LICENSE

commit fea6da8272de9d85a7b5caa977fd32c1837309a0
parent c65210f8a563cab8046bfa7b6e8d08e681317e08
Author: David Eisinger <[email protected]>
Date:   Tue,  7 Oct 2025 01:12:37 -0400

Fix search highlighting bug

Diffstat:
Minternal/database.go | 38+++++++++++++++++++++++++++++++++-----
Minternal/user_db.go | 2+-
Minternal/web/index.html | 15++++++++++-----
3 files changed, 44 insertions(+), 11 deletions(-)

diff --git a/internal/database.go b/internal/database.go @@ -12,6 +12,7 @@ import ( "sort" "strings" "time" + "unicode/utf8" _ "modernc.org/sqlite" ) @@ -292,7 +293,7 @@ func (db *DB) GetBookmarkByURL(ctx context.Context, userID int64, u string) (*Bo return &b, nil } -// ListBookmarks returns bookmarks ordered by created_at desc. +// ListBookmarks returns bookmarks ordered by updated_at desc. func (db *DB) ListBookmarks(ctx context.Context, userID int64, limit, offset int) ([]Bookmark, error) { if limit <= 0 { limit = 50 @@ -301,7 +302,7 @@ func (db *DB) ListBookmarks(ctx context.Context, userID int64, limit, offset int SELECT id, url, title, description, public, user_id, created_at, updated_at FROM bookmarks WHERE user_id = ? - ORDER BY created_at DESC + ORDER BY updated_at DESC LIMIT ? OFFSET ? `, userID, limit, offset) if err != nil { @@ -674,6 +675,23 @@ func makeExcerpt(content, query string) string { if end > len(c) { end = len(c) } + // Align start/end to UTF-8 rune boundaries to avoid broken characters. + // Move start backward to the beginning of a rune. + for start > 0 { + b := c[start] + if (b & 0xC0) != 0x80 { // not a continuation byte + break + } + start-- + } + // Move end forward to the end of the current rune if we cut in the middle. + for end < len(c) { + b := c[end] + if (b & 0xC0) != 0x80 { // at a rune boundary already + break + } + end++ + } s := c[start:end] if start > 0 { s = "…" + s @@ -681,11 +699,13 @@ func makeExcerpt(content, query string) string { if end < len(c) { s = s + "…" } - // Highlight all tokens (basic, case-insensitive) using guillemets « » to avoid - // conflict with brackets used elsewhere. + // Highlight all tokens (basic, case-insensitive) using guillemets « ». + // Iterate by runes to avoid breaking multi-byte characters while matching + // tokens by byte-slices at rune boundaries. ls := strings.ToLower(s) var b strings.Builder for i := 0; i < len(s); { + // Try to match any token starting at this byte position matched := false for _, t := range fields { if t == "" { @@ -700,10 +720,18 @@ func makeExcerpt(content, query string) string { break } } - if !matched { + if matched { + continue + } + r, size := utf8.DecodeRuneInString(s[i:]) + if r == utf8.RuneError && size == 1 { + // Write raw byte on decode error to avoid infinite loop b.WriteByte(s[i]) i++ + continue } + b.WriteRune(r) + i += size } return b.String() } diff --git a/internal/user_db.go b/internal/user_db.go @@ -100,7 +100,7 @@ func (db *DB) ListPublicBookmarks(ctx context.Context, userID int64, limit, offs rows, err := db.sql.QueryContext(ctx, ` SELECT id, url, title, description, public, user_id, created_at, updated_at FROM bookmarks WHERE user_id = ? AND public = 1 - ORDER BY created_at DESC + ORDER BY updated_at DESC LIMIT ? OFFSET ? `, userID, limit, offset) if err != nil { diff --git a/internal/web/index.html b/internal/web/index.html @@ -133,8 +133,10 @@ </div> </div> <p v-else-if="b.excerpt" class="excerpt" v-html="formatExcerpt(b.excerpt)"></p> - <p class="meta">Updated: {{ formatDate(b.updated_at) }}</p> + <div v-if="!b.editing && !b.excerpt && b.description" class="desc" v-html="renderMarkdown(b.description)"></div> <p class="action-links"> + <span class="meta">{{ formatDate(b.updated_at) }}</span> + <span class="sep">|</span> <template v-if="b.has_archive"> <a :href="`/bookmarks/${b.id}/archive`" target="_blank" title="View archived text">Archive</a> <span class="sep">|</span> @@ -143,7 +145,7 @@ <span class="sep">|</span> <a href="#" @click.prevent="confirmDelete(b)">Delete</a> </p> - <div v-if="!b.editing && !b.excerpt && b.description" class="desc" v-html="renderMarkdown(b.description)"></div> + </article> </div> </section> @@ -161,11 +163,14 @@ <article v-for="b in results" :key="b.id" class="card"> <h3><a :href="b.url" target="_blank" rel="noopener noreferrer">{{ b.title || b.url }}</a></h3> <p class="url">{{ b.url }}</p> - <p class="meta">Updated: {{ formatDate(b.updated_at) }}</p> + <div v-if="b.description" class="desc" v-html="renderMarkdown(b.description)"></div> <p class="action-links"> - <a v-if="b.has_archive" :href="`/bookmarks/${b.id}/archive`" target="_blank">Archive</a> + <span class="meta">{{ formatDate(b.updated_at) }}</span> + <template v-if="b.has_archive"> + <span class="sep">|</span> + <a :href="`/bookmarks/${b.id}/archive`" target="_blank">Archive</a> + </template> </p> - <div v-if="b.description" class="desc" v-html="renderMarkdown(b.description)"></div> </article> </div> </section>