fix: verstak:// links in preview, case-insensitive search, keyboard layout swap

1. Fix verstak:// links rendered as blocked/strikethrough in markdown preview:
   - Changed href from 'javascript:void(0)' to hash-based '#verstak-type-id'
   - DOMPurify no longer strips the link; click handler uses data-verstak-href
   - CSS already handles .md-link--internal with cyan color, no strikethrough

2. Add markdown label escaping for internal link picker:
   - New escapeMarkdownLabel() in markdown.ts escapes [ ] ( )
   - Applied in InternalLinkPicker.selectResult() before inserting markdown

3. Fix case-insensitive search for RU/EN:
   - Add title_lower column (migration 019) populated by Go strings.ToLower
   - BackfillTitleLower() runs after migrations to populate existing rows
   - Search() now queries title_lower with Go-level lowercase (Unicode-aware)
   - insertNode() and UpdateTitle() populate title_lower automatically
   - New migration 019 + BackfillTitleLower in storage.go
   - Tests: TestSearchCaseInsensitive, TestSearchFindsCreatedNode

4. Add keyboard layout swap search support:
   - New keyboardLayout.ts utility with RU↔EN QWERTY mapping
   - expandKeyboardVariants() generates original + swapped + lowercased variants
   - InternalLinkPicker.search() queries all variants in parallel, deduplicates by ID
   - Examples: dthcnfr → верстак, руддщ → hello

Files changed:
- markdown.ts: hash href + escapeMarkdownLabel export
- InternalLinkPicker.svelte: label escaping + layout swap search
- keyboardLayout.ts: new RU/EN layout swap utility
- repository.go: title_lower in Search/insertNode/UpdateTitle
- storage.go: migration019 + BackfillTitleLower
- migrations_019.sql.go: new migration
- search_test.go, repository_test.go: new tests
This commit is contained in:
2026-06-15 10:39:44 +08:00
parent 7521eea109
commit 88eb99e9af
10 changed files with 264 additions and 41 deletions
@@ -0,0 +1,8 @@
package storage
// migration019 — add title_lower column for case-insensitive search.
const migration019 = `
ALTER TABLE nodes ADD COLUMN title_lower TEXT NOT NULL DEFAULT '';
CREATE INDEX IF NOT EXISTS idx_nodes_title_lower ON nodes(title_lower);
`
+38
View File
@@ -39,6 +39,10 @@ func Open(path string) (*DB, error) {
db.Close()
return nil, err
}
if err := w.BackfillTitleLower(); err != nil {
db.Close()
return nil, err
}
return w, nil
}
@@ -75,6 +79,7 @@ var migrationFiles = map[int]string{
16: migration016,
17: migration017,
18: migration018,
19: migration019,
}
func (db *DB) runInitialSchema() error {
@@ -134,3 +139,36 @@ func (db *DB) applyMigration(version int, raw string) error {
}
return tx.Commit()
}
// BackfillTitleLower populates title_lower for rows where it is still empty.
// Uses Go strings.ToLower for Unicode-aware case folding.
func (db *DB) BackfillTitleLower() error {
rows, err := db.Query("SELECT id, title FROM nodes WHERE title_lower = '' AND deleted_at IS NULL")
if err != nil {
return err
}
defer rows.Close()
type pair struct {
id string
lower string
}
var pairs []pair
for rows.Next() {
var id, title string
if err := rows.Scan(&id, &title); err != nil {
return err
}
pairs = append(pairs, pair{id: id, lower: strings.ToLower(title)})
}
if err := rows.Err(); err != nil {
return err
}
for _, p := range pairs {
if _, err := db.Exec("UPDATE nodes SET title_lower = ? WHERE id = ?", p.lower, p.id); err != nil {
return err
}
}
return nil
}