feat(db): add wiki-link extraction, resolution, and backlinks
CI / test (pull_request) Successful in 2m27s
CI / test (pull_request) Successful in 2m27s
[[wiki-links]] in entry bodies are extracted at save time, resolved to entity IDs (title match first, body substring fallback), and stored in entity_links junction table. Backlinks surface in TUI detail view showing entries that link to the current entry. Schema migration v5 adds entity_links with CASCADE/SET NULL semantics. Links sync on Create, Update, and Absorb.
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
package link
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var linkRe = regexp.MustCompile(`\[\[(.+?)\]\]`)
|
||||
|
||||
func ExtractLinks(body string) []string {
|
||||
matches := linkRe.FindAllStringSubmatch(body, -1)
|
||||
if len(matches) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := map[string]bool{}
|
||||
var result []string
|
||||
for _, m := range matches {
|
||||
text := strings.TrimSpace(m[1])
|
||||
if text == "" || seen[text] {
|
||||
continue
|
||||
}
|
||||
seen[text] = true
|
||||
result = append(result, text)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package link
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestExtractLinks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
want []string
|
||||
}{
|
||||
{"no links", "plain text with no links", nil},
|
||||
{"single link", "see [[nginx config]] for details", []string{"nginx config"}},
|
||||
{"multiple links", "see [[nginx config]] and [[deploy steps]]", []string{"nginx config", "deploy steps"}},
|
||||
{"duplicate deduped", "[[foo]] then [[foo]] again", []string{"foo"}},
|
||||
{"empty brackets", "empty [[ ]] ignored", nil},
|
||||
{"just brackets no content", "[[]] empty", nil},
|
||||
{"link with special chars", "see [[deploy: staging (v2)]]", []string{"deploy: staging (v2)"}},
|
||||
{"link in markdown", "# heading\n\nsee [[my note]] for info", []string{"my note"}},
|
||||
{"adjacent links", "[[one]][[two]]", []string{"one", "two"}},
|
||||
{"partial brackets ignored", "not a [link] or [[incomplete", nil},
|
||||
{"link with hash", "see [[#ops channel]]", []string{"#ops channel"}},
|
||||
{"multiline body", "line one [[link one]]\nline two [[link two]]", []string{"link one", "link two"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := ExtractLinks(tt.body)
|
||||
if len(got) != len(tt.want) {
|
||||
t.Fatalf("got %v, want %v", got, tt.want)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.want[i] {
|
||||
t.Fatalf("got %v, want %v", got, tt.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user