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

@ -27,6 +27,12 @@ type TreeNode struct {
Children []*TreeNode `json:"children,omitempty"`
}
// BacklinkInfo représente une note qui référence la note courante
type BacklinkInfo struct {
Path string `json:"path"`
Title string `json:"title"`
}
// Handler gère toutes les routes de l'API.
type Handler struct {
notesDir string
@ -696,14 +702,20 @@ func (h *Handler) handleGetNote(w http.ResponseWriter, r *http.Request, filename
content = []byte(initialContent)
}
// Récupérer les backlinks pour cette note
backlinks := h.idx.GetBacklinks(filename)
backlinkData := h.buildBacklinkData(backlinks)
data := struct {
Filename string
Content string
IsHome bool
Filename string
Content string
IsHome bool
Backlinks []BacklinkInfo
}{
Filename: filename,
Content: string(content),
IsHome: false,
Filename: filename,
Content: string(content),
IsHome: false,
Backlinks: backlinkData,
}
err = h.templates.ExecuteTemplate(w, "editor.html", data)
@ -1135,3 +1147,35 @@ func (h *Handler) removeEmptyDirRecursive(relPath string) {
}
}
}
// buildBacklinkData transforme une liste de chemins de notes en BacklinkInfo avec titres
func (h *Handler) buildBacklinkData(paths []string) []BacklinkInfo {
if len(paths) == 0 {
return nil
}
result := make([]BacklinkInfo, 0, len(paths))
for _, path := range paths {
// Lire le fichier pour extraire le titre du front matter
fullPath := filepath.Join(h.notesDir, path)
fm, _, err := indexer.ExtractFrontMatterAndBody(fullPath)
title := ""
if err == nil && fm.Title != "" {
title = fm.Title
} else {
// Fallback: dériver le titre du nom de fichier
title = strings.TrimSuffix(filepath.Base(path), ".md")
title = strings.ReplaceAll(title, "-", " ")
title = strings.Title(title)
}
result = append(result, BacklinkInfo{
Path: path,
Title: title,
})
}
return result
}

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
}