go-wiki/render/markdown.go
2026-07-14 20:46:19 +02:00

137 lines
4.4 KiB
Go

// Package render ist zuständig für alles rund ums Lesen und Umwandeln von Markdown-Dateien.
package render
import (
"bytes"
"os"
"strings"
"github.com/yuin/goldmark"
"gopkg.in/yaml.v3"
)
// Page repräsentiert eine einzelne Wiki-Seite.
// Ein Struct in Go ist wie eine Datenklasse: es bündelt zusammengehörige Felder.
type Page struct {
Title string // Titel aus dem Frontmatter
Password string // Passwort aus dem Frontmatter (leer = öffentlich)
Tags []string // Tags aus dem Frontmatter (optional)
Hidden bool // Wenn true: Seite erscheint nicht in der Navigation (aber URL bleibt erreichbar)
Body string // Der fertig gerenderte HTML-Inhalt der Seite
RawBody string // Der originale Markdown-Text ohne Frontmatter — wird für die Volltextsuche genutzt
}
// frontmatter ist ein internes Struct nur zum Parsen des YAML-Blocks.
// Wir brauchen es nur kurz, deshalb ist es kleingeschrieben (= nicht exportiert,
// also nur innerhalb dieses Packages sichtbar).
type frontmatter struct {
Title string `yaml:"title"`
Password string `yaml:"password"`
Tags []string `yaml:"tags"`
Hidden bool `yaml:"hidden"`
}
// LoadPage liest eine Markdown-Datei vom Dateisystem, parst das Frontmatter
// und wandelt den Markdown-Inhalt in HTML um.
//
// In Go geben Funktionen oft zwei Werte zurück: das Ergebnis und einen Fehler.
// Der Aufrufer muss den Fehler prüfen — Go hat keine Exceptions.
func LoadPage(filePath string) (*Page, error) {
// os.ReadFile liest die komplette Datei in einen Byte-Slice ([]byte).
// Ein Byte-Slice ist einfach eine Liste von Bytes — rohe Dateidaten.
rawContent, err := os.ReadFile(filePath)
if err != nil {
// Wir geben nil (kein Ergebnis) und den Fehler zurück.
return nil, err
}
// Wir wandeln den Byte-Slice in einen String um, damit wir damit arbeiten können.
content := string(rawContent)
// Frontmatter und Markdown-Body trennen.
fm, body, err := splitFrontmatter(content)
if err != nil {
return nil, err
}
// Den Markdown-Body in HTML umwandeln.
htmlBody, err := markdownToHTML(body)
if err != nil {
return nil, err
}
// Das fertige Page-Struct befüllen und zurückgeben.
// Das * vor Page bedeutet: wir geben einen Zeiger zurück (eine Referenz auf
// das Struct im Speicher), nicht eine Kopie davon.
return &Page{
Title: fm.Title,
Password: fm.Password,
Tags: fm.Tags,
Hidden: fm.Hidden,
Body: htmlBody,
RawBody: body, // Rohtext für die BM25-Volltextsuche — HTML-Tags verfälschen die Tokenisierung
}, nil
}
// splitFrontmatter trennt den YAML-Frontmatter-Block vom Markdown-Inhalt.
//
// Eine typische Datei sieht so aus:
//
// ---
// title: Meine Seite
// password: geheim
// ---
//
// # Überschrift
// Inhalt hier...
func splitFrontmatter(content string) (frontmatter, string, error) {
var fm frontmatter
// Frontmatter beginnt und endet mit "---".
// Wir prüfen ob die Datei überhaupt Frontmatter hat.
if !strings.HasPrefix(content, "---") {
// Kein Frontmatter — leeres Struct zurückgeben, ganzer Inhalt ist Body.
return fm, content, nil
}
// Den ersten "---" abschneiden und nach dem zweiten suchen.
rest := strings.TrimPrefix(content, "---\n")
closingIndex := strings.Index(rest, "---")
if closingIndex == -1 {
// Öffnendes "---" gefunden, aber kein schließendes — Datei ist kaputt.
return fm, content, nil
}
// Den YAML-Block und den Body-Teil extrahieren.
yamlBlock := rest[:closingIndex]
body := rest[closingIndex+4:] // +4 um "---\n" zu überspringen
// Den YAML-Block parsen und in unser frontmatter-Struct füllen.
// yaml.Unmarshal erwartet einen Byte-Slice, deshalb []byte(yamlBlock).
err := yaml.Unmarshal([]byte(yamlBlock), &fm)
if err != nil {
return fm, body, err
}
return fm, body, nil
}
// markdownToHTML wandelt einen Markdown-String in einen HTML-String um.
// Dafür nutzen wir die goldmark-Bibliothek.
func markdownToHTML(markdownContent string) (string, error) {
// bytes.Buffer ist ein beschreibbarer Puffer im Speicher — wie eine Datei,
// aber im RAM. Goldmark schreibt das HTML-Ergebnis hinein.
var htmlBuffer bytes.Buffer
// goldmark.Convert macht die eigentliche Umwandlung.
// []byte(markdownContent) wandelt den String zurück in Bytes,
// weil die Bibliothek Bytes erwartet.
err := goldmark.Convert([]byte(markdownContent), &htmlBuffer)
if err != nil {
return "", err
}
// htmlBuffer.String() holt den fertigen HTML-String aus dem Puffer.
return htmlBuffer.String(), nil
}