Add backlink

This commit is contained in:
2025-11-12 09:31:09 +01:00
parent 5e30a5cf5d
commit 584a4a0acd
25 changed files with 1769 additions and 79 deletions

View File

@ -7,6 +7,7 @@ import (
"io"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"sync"
@ -17,9 +18,10 @@ import (
// Indexer maintient un index en memoire des tags associes aux fichiers Markdown.
type Indexer struct {
mu sync.RWMutex
tags map[string][]string
docs map[string]*Document
mu sync.RWMutex
tags map[string][]string
docs map[string]*Document
backlinks map[string][]string // note path -> list of notes that reference it
}
// Document représente une note indexée pour la recherche.
@ -51,8 +53,9 @@ type SearchResult struct {
// New cree une nouvelle instance d Indexer.
func New() *Indexer {
return &Indexer{
tags: make(map[string][]string),
docs: make(map[string]*Document),
tags: make(map[string][]string),
docs: make(map[string]*Document),
backlinks: make(map[string][]string),
}
}
@ -112,9 +115,31 @@ func (i *Indexer) Load(root string) error {
indexed[tag] = list
}
// Build backlinks index
backlinksMap := make(map[string][]string)
for sourcePath, doc := range documents {
links := extractInternalLinks(doc.Body)
for _, targetPath := range links {
// Add sourcePath to the backlinks of targetPath
if _, ok := backlinksMap[targetPath]; !ok {
backlinksMap[targetPath] = make([]string, 0)
}
// Avoid duplicates
if !containsString(backlinksMap[targetPath], sourcePath) {
backlinksMap[targetPath] = append(backlinksMap[targetPath], sourcePath)
}
}
}
// Sort backlinks for consistency
for _, links := range backlinksMap {
sort.Strings(links)
}
i.mu.Lock()
i.tags = indexed
i.docs = documents
i.backlinks = backlinksMap
i.mu.Unlock()
return nil
@ -668,3 +693,56 @@ func (i *Indexer) GetAllTagsWithCount() []TagCount {
return result
}
// GetBacklinks retourne la liste des notes qui référencent la note spécifiée
func (i *Indexer) GetBacklinks(path string) []string {
i.mu.RLock()
defer i.mu.RUnlock()
links, ok := i.backlinks[path]
if !ok || len(links) == 0 {
return nil
}
// Retourner une copie pour éviter les modifications externes
result := make([]string, len(links))
copy(result, links)
return result
}
// extractInternalLinks extrait tous les liens internes d'un texte Markdown/HTML
// Format: <a ... hx-get="/api/notes/path/to/note.md" ...>
func extractInternalLinks(body string) []string {
// Pattern pour capturer le chemin dans hx-get="/api/notes/..."
// On cherche: hx-get="/api/notes/ suivi de n'importe quoi jusqu'au prochain guillemet
pattern := `hx-get="/api/notes/([^"]+)"`
// Compiler la regex
re, err := regexp.Compile(pattern)
if err != nil {
return nil
}
// Trouver tous les matches
matches := re.FindAllStringSubmatch(body, -1)
if len(matches) == 0 {
return nil
}
// Extraire les chemins (groupe de capture 1)
links := make([]string, 0, len(matches))
seen := make(map[string]struct{})
for _, match := range matches {
if len(match) > 1 {
path := match[1]
// Éviter les doublons
if _, ok := seen[path]; !ok {
seen[path] = struct{}{}
links = append(links, path)
}
}
}
return links
}