Changement des ilink vers markdown pur

This commit is contained in:
2025-11-12 20:17:43 +01:00
parent 6585b1765a
commit a09b73e4f1
25 changed files with 803 additions and 315 deletions

View File

@ -114,6 +114,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handleFavorites(w, r)
return
}
if strings.HasPrefix(path, "/api/folder/") {
h.handleFolderView(w, r)
return
}
http.NotFound(w, r)
}
@ -278,15 +282,17 @@ func (h *Handler) handleHome(w http.ResponseWriter, r *http.Request) {
// Utiliser le template editor.html pour afficher la page d'accueil
data := struct {
Filename string
Content string
IsHome bool
Backlinks []BacklinkInfo
Filename string
Content string
IsHome bool
Backlinks []BacklinkInfo
Breadcrumb template.HTML
}{
Filename: "🏠 Accueil - Index",
Content: content,
IsHome: true,
Backlinks: nil, // Pas de backlinks pour la page d'accueil
Filename: "🏠 Accueil - Index",
Content: content,
IsHome: true,
Backlinks: nil, // Pas de backlinks pour la page d'accueil
Breadcrumb: h.generateBreadcrumb(""),
}
err := h.templates.ExecuteTemplate(w, "editor.html", data)
@ -338,12 +344,19 @@ func (h *Handler) generateHomeMarkdown() string {
// Section des notes récemment modifiées (après les favoris)
h.generateRecentNotesSection(&sb)
// Titre de l'arborescence avec le nombre de notes
sb.WriteString(fmt.Sprintf("## 📂 Toutes les notes (%d)\n\n", noteCount))
// Section de toutes les notes avec accordéon
sb.WriteString("<div class=\"home-section\">\n")
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('all-notes')\">\n")
sb.WriteString(fmt.Sprintf(" <h2 class=\"home-section-title\">📂 Toutes les notes (%d)</h2>\n", noteCount))
sb.WriteString(" </div>\n")
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-all-notes\">\n")
// Générer l'arborescence en Markdown
h.generateMarkdownTree(&sb, tree, 0)
sb.WriteString(" </div>\n")
sb.WriteString("</div>\n")
return sb.String()
}
@ -355,18 +368,24 @@ func (h *Handler) generateTagsSection(sb *strings.Builder) {
return
}
sb.WriteString("## 🏷️ Tags\n\n")
sb.WriteString("<div class=\"tags-cloud\">\n")
sb.WriteString("<div class=\"home-section\">\n")
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('tags')\">\n")
sb.WriteString(" <h2 class=\"home-section-title\">🏷️ Tags</h2>\n")
sb.WriteString(" </div>\n")
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-tags\">\n")
sb.WriteString(" <div class=\"tags-cloud\">\n")
for _, tc := range tags {
// Créer un lien HTML discret et fonctionnel
sb.WriteString(fmt.Sprintf(
`<a href="#" class="tag-item" hx-get="/api/search?query=tag:%s" hx-target="#search-results" hx-swap="innerHTML"><kbd class="tag-badge">#%s</kbd> <mark class="tag-count">%d</mark></a>`,
` <a href="#" class="tag-item" hx-get="/api/search?query=tag:%s" hx-target="#search-results" hx-swap="innerHTML"><kbd class="tag-badge">#%s</kbd> <mark class="tag-count">%d</mark></a>`,
tc.Tag, tc.Tag, tc.Count,
))
sb.WriteString("\n")
}
sb.WriteString(" </div>\n")
sb.WriteString(" </div>\n")
sb.WriteString("</div>\n\n")
}
@ -377,36 +396,42 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder) {
return
}
sb.WriteString("## ⭐ Favoris\n\n")
sb.WriteString("<div class=\"note-tree favorites-tree\">\n")
sb.WriteString("<div class=\"home-section\">\n")
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('favorites')\">\n")
sb.WriteString(" <h2 class=\"home-section-title\">⭐ Favoris</h2>\n")
sb.WriteString(" </div>\n")
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-favorites\">\n")
sb.WriteString(" <div class=\"note-tree favorites-tree\">\n")
for _, fav := range favorites.Items {
safeID := "fav-" + strings.ReplaceAll(strings.ReplaceAll(fav.Path, "/", "-"), "\\", "-")
if fav.IsDir {
// Dossier - avec accordéon
sb.WriteString(fmt.Sprintf(" <div class=\"folder indent-level-1\">\n"))
sb.WriteString(fmt.Sprintf(" <div class=\"folder-header\" onclick=\"toggleFolder('%s')\">\n", safeID))
sb.WriteString(fmt.Sprintf(" <span class=\"folder-icon\" id=\"icon-%s\">📁</span>\n", safeID))
sb.WriteString(fmt.Sprintf(" <strong>%s</strong>\n", fav.Title))
sb.WriteString(fmt.Sprintf(" </div>\n"))
sb.WriteString(fmt.Sprintf(" <div class=\"folder-content\" id=\"folder-%s\">\n", safeID))
sb.WriteString(fmt.Sprintf(" <div class=\"folder indent-level-1\">\n"))
sb.WriteString(fmt.Sprintf(" <div class=\"folder-header\" onclick=\"toggleFolder('%s')\">\n", safeID))
sb.WriteString(fmt.Sprintf(" <span class=\"folder-icon\" id=\"icon-%s\">📁</span>\n", safeID))
sb.WriteString(fmt.Sprintf(" <strong>%s</strong>\n", fav.Title))
sb.WriteString(fmt.Sprintf(" </div>\n"))
sb.WriteString(fmt.Sprintf(" <div class=\"folder-content\" id=\"folder-%s\">\n", safeID))
// Lister le contenu du dossier
h.generateFavoriteFolderContent(sb, fav.Path, 2)
sb.WriteString(fmt.Sprintf(" </div>\n"))
sb.WriteString(fmt.Sprintf(" </div>\n"))
h.generateFavoriteFolderContent(sb, fav.Path, 3)
sb.WriteString(fmt.Sprintf(" </div>\n"))
sb.WriteString(fmt.Sprintf(" </div>\n"))
} else {
// Fichier
sb.WriteString(fmt.Sprintf(" <div class=\"file indent-level-1\">\n"))
sb.WriteString(fmt.Sprintf(" <a href=\"#\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\">", fav.Path))
sb.WriteString(fmt.Sprintf(" <div class=\"file indent-level-1\">\n"))
sb.WriteString(fmt.Sprintf(" <a href=\"#\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\">", fav.Path))
sb.WriteString(fmt.Sprintf("📄 %s", fav.Title))
sb.WriteString("</a>\n")
sb.WriteString(fmt.Sprintf(" </div>\n"))
sb.WriteString(fmt.Sprintf(" </div>\n"))
}
}
sb.WriteString(" </div>\n")
sb.WriteString(" </div>\n")
sb.WriteString("</div>\n\n")
}
@ -418,8 +443,12 @@ func (h *Handler) generateRecentNotesSection(sb *strings.Builder) {
return
}
sb.WriteString("## 🕒 Récemment modifiés\n\n")
sb.WriteString("<div class=\"recent-notes-container\">\n")
sb.WriteString("<div class=\"home-section\">\n")
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('recent')\">\n")
sb.WriteString(" <h2 class=\"home-section-title\">🕒 Récemment modifiés</h2>\n")
sb.WriteString(" </div>\n")
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-recent\">\n")
sb.WriteString(" <div class=\"recent-notes-container\">\n")
for _, doc := range recentDocs {
// Extraire les premières lignes du corps (max 150 caractères)
@ -434,13 +463,13 @@ func (h *Handler) generateRecentNotesSection(sb *strings.Builder) {
dateStr = doc.Date
}
sb.WriteString(" <div class=\"recent-note-card\">\n")
sb.WriteString(fmt.Sprintf(" <a href=\"#\" class=\"recent-note-link\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\">\n", doc.Path))
sb.WriteString(fmt.Sprintf(" <div class=\"recent-note-title\">%s</div>\n", doc.Title))
sb.WriteString(fmt.Sprintf(" <div class=\"recent-note-meta\">\n"))
sb.WriteString(fmt.Sprintf(" <span class=\"recent-note-date\">📅 %s</span>\n", dateStr))
sb.WriteString(" <div class=\"recent-note-card\">\n")
sb.WriteString(fmt.Sprintf(" <a href=\"#\" class=\"recent-note-link\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\">\n", doc.Path))
sb.WriteString(fmt.Sprintf(" <div class=\"recent-note-title\">%s</div>\n", doc.Title))
sb.WriteString(fmt.Sprintf(" <div class=\"recent-note-meta\">\n"))
sb.WriteString(fmt.Sprintf(" <span class=\"recent-note-date\">📅 %s</span>\n", dateStr))
if len(doc.Tags) > 0 {
sb.WriteString(fmt.Sprintf(" <span class=\"recent-note-tags\">"))
sb.WriteString(fmt.Sprintf(" <span class=\"recent-note-tags\">"))
for i, tag := range doc.Tags {
if i > 0 {
sb.WriteString(" ")
@ -449,14 +478,16 @@ func (h *Handler) generateRecentNotesSection(sb *strings.Builder) {
}
sb.WriteString("</span>\n")
}
sb.WriteString(" </div>\n")
sb.WriteString(" </div>\n")
if preview != "" {
sb.WriteString(fmt.Sprintf(" <div class=\"recent-note-preview\">%s</div>\n", preview))
sb.WriteString(fmt.Sprintf(" <div class=\"recent-note-preview\">%s</div>\n", preview))
}
sb.WriteString(" </a>\n")
sb.WriteString(" </div>\n")
sb.WriteString(" </a>\n")
sb.WriteString(" </div>\n")
}
sb.WriteString(" </div>\n")
sb.WriteString(" </div>\n")
sb.WriteString("</div>\n\n")
}
@ -788,15 +819,17 @@ func (h *Handler) handleGetNote(w http.ResponseWriter, r *http.Request, filename
backlinkData := h.buildBacklinkData(backlinks)
data := struct {
Filename string
Content string
IsHome bool
Backlinks []BacklinkInfo
Filename string
Content string
IsHome bool
Backlinks []BacklinkInfo
Breadcrumb template.HTML
}{
Filename: filename,
Content: string(content),
IsHome: false,
Backlinks: backlinkData,
Filename: filename,
Content: string(content),
IsHome: false,
Backlinks: backlinkData,
Breadcrumb: h.generateBreadcrumb(filename),
}
err = h.templates.ExecuteTemplate(w, "editor.html", data)
@ -1264,3 +1297,190 @@ func (h *Handler) buildBacklinkData(paths []string) []BacklinkInfo {
return result
}
// handleFolderView affiche le contenu d'un dossier
func (h *Handler) handleFolderView(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed)
return
}
// Si ce n'est pas une requête HTMX, rediriger vers la page principale
if r.Header.Get("HX-Request") == "" {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
// Extraire le chemin du dossier depuis l'URL
folderPath := strings.TrimPrefix(r.URL.Path, "/api/folder/")
folderPath = strings.TrimPrefix(folderPath, "/")
// Sécurité : vérifier le chemin
cleanPath := filepath.Clean(folderPath)
if strings.HasPrefix(cleanPath, "..") || filepath.IsAbs(cleanPath) {
http.Error(w, "Chemin invalide", http.StatusBadRequest)
return
}
// Construire le chemin absolu
absPath := filepath.Join(h.notesDir, cleanPath)
// Vérifier que c'est bien un dossier
info, err := os.Stat(absPath)
if err != nil || !info.IsDir() {
http.Error(w, "Dossier non trouvé", http.StatusNotFound)
return
}
// Générer le contenu de la page
content := h.generateFolderViewMarkdown(cleanPath)
// Utiliser le template editor.html
data := struct {
Filename string
Content string
IsHome bool
Backlinks []BacklinkInfo
Breadcrumb template.HTML
}{
Filename: cleanPath,
Content: content,
IsHome: true, // Pas d'édition pour une vue de dossier
Backlinks: nil,
Breadcrumb: h.generateBreadcrumb(cleanPath),
}
err = h.templates.ExecuteTemplate(w, "editor.html", data)
if err != nil {
h.logger.Printf("Erreur d'exécution du template folder view: %v", err)
http.Error(w, "Erreur interne", http.StatusInternalServerError)
}
}
// generateBreadcrumb génère un fil d'Ariane HTML cliquable
func (h *Handler) generateBreadcrumb(path string) template.HTML {
if path == "" {
return template.HTML(`<strong>📁 Racine</strong>`)
}
parts := strings.Split(filepath.ToSlash(path), "/")
var sb strings.Builder
sb.WriteString(`<span class="breadcrumb">`)
// Lien racine
sb.WriteString(`<a href="#" hx-get="/api/home" hx-target="#editor-container" hx-swap="innerHTML" hx-push-url="true" class="breadcrumb-link">📁 Racine</a>`)
// Construire les liens pour chaque partie
currentPath := ""
for i, part := range parts {
sb.WriteString(` <span class="breadcrumb-separator"></span> `)
if currentPath == "" {
currentPath = part
} else {
currentPath = currentPath + "/" + part
}
// Le dernier élément (fichier) n'est pas cliquable
if i == len(parts)-1 && strings.HasSuffix(part, ".md") {
// C'est un fichier, pas cliquable
displayName := strings.TrimSuffix(part, ".md")
sb.WriteString(fmt.Sprintf(`<strong>%s</strong>`, displayName))
} else {
// C'est un dossier, cliquable
sb.WriteString(fmt.Sprintf(
`<a href="#" hx-get="/api/folder/%s" hx-target="#editor-container" hx-swap="innerHTML" hx-push-url="true" class="breadcrumb-link">📂 %s</a>`,
currentPath, part,
))
}
}
sb.WriteString(`</span>`)
return template.HTML(sb.String())
}
// generateFolderViewMarkdown génère le contenu Markdown pour l'affichage d'un dossier
func (h *Handler) generateFolderViewMarkdown(folderPath string) string {
var sb strings.Builder
// En-tête
if folderPath == "" {
sb.WriteString("# 📁 Racine\n\n")
} else {
folderName := filepath.Base(folderPath)
sb.WriteString(fmt.Sprintf("# 📂 %s\n\n", folderName))
}
sb.WriteString("_Contenu du dossier_\n\n")
// Lister le contenu
absPath := filepath.Join(h.notesDir, folderPath)
entries, err := os.ReadDir(absPath)
if err != nil {
sb.WriteString("❌ Erreur lors de la lecture du dossier\n")
return sb.String()
}
// Séparer dossiers et fichiers
var folders []os.DirEntry
var files []os.DirEntry
for _, entry := range entries {
// Ignorer les fichiers cachés
if strings.HasPrefix(entry.Name(), ".") {
continue
}
if entry.IsDir() {
folders = append(folders, entry)
} else if strings.HasSuffix(entry.Name(), ".md") {
files = append(files, entry)
}
}
// Afficher les dossiers
if len(folders) > 0 {
sb.WriteString("## 📁 Dossiers\n\n")
sb.WriteString("<div class=\"folder-list\">\n")
for _, folder := range folders {
subPath := filepath.Join(folderPath, folder.Name())
sb.WriteString(fmt.Sprintf(
`<div class="folder-item"><a href="#" hx-get="/api/folder/%s" hx-target="#editor-container" hx-swap="innerHTML" hx-push-url="true">📂 %s</a></div>`,
filepath.ToSlash(subPath), folder.Name(),
))
sb.WriteString("\n")
}
sb.WriteString("</div>\n\n")
}
// Afficher les fichiers
if len(files) > 0 {
sb.WriteString(fmt.Sprintf("## 📄 Notes (%d)\n\n", len(files)))
sb.WriteString("<div class=\"file-list\">\n")
for _, file := range files {
filePath := filepath.Join(folderPath, file.Name())
displayName := strings.TrimSuffix(file.Name(), ".md")
// Lire le titre du front matter si possible
fullPath := filepath.Join(h.notesDir, filePath)
fm, _, err := indexer.ExtractFrontMatterAndBody(fullPath)
if err == nil && fm.Title != "" {
displayName = fm.Title
}
sb.WriteString(fmt.Sprintf(
`<div class="file-item"><a href="#" hx-get="/api/notes/%s" hx-target="#editor-container" hx-swap="innerHTML" hx-push-url="true">📄 %s</a></div>`,
filepath.ToSlash(filePath), displayName,
))
sb.WriteString("\n")
}
sb.WriteString("</div>\n\n")
}
if len(folders) == 0 && len(files) == 0 {
sb.WriteString("_Ce dossier est vide_\n")
}
return sb.String()
}