This commit is contained in:
@@ -1,8 +1,5 @@
|
||||
// internal/article/load.go
|
||||
package article
|
||||
|
||||
// internal/article/load.go (gekürzt)
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
@@ -19,15 +16,24 @@ import (
|
||||
"github.com/gomarkdown/markdown/html"
|
||||
)
|
||||
|
||||
// LoadDir liest alle *.html und *.md unter root/content,
|
||||
// parst Front‑Matter (JSON‑Kommentar in der 1. Zeile) und liefert []Article.
|
||||
func LoadDir(root string) ([]Article, error) {
|
||||
var out []Article
|
||||
seen := make(map[string]bool) // Duplikats‑Check
|
||||
type LoaderOptions struct {
|
||||
// TrustedHTML erlaubt rohe .html-Inhalte aus deinem Content-Repository.
|
||||
// Achtung: Nur aktivieren, wenn du dem Repository vertraust.
|
||||
TrustedHTML bool
|
||||
}
|
||||
|
||||
mdRenderer := html.NewRenderer(html.RendererOptions{
|
||||
Flags: html.CommonFlags | html.HrefTargetBlank,
|
||||
})
|
||||
type frontMatter struct {
|
||||
Title string `json:"title"`
|
||||
Date string `json:"date"`
|
||||
Slug string `json:"slug"`
|
||||
Cover string `json:"cover"`
|
||||
Description string `json:"description"`
|
||||
Format string `json:"format"`
|
||||
}
|
||||
|
||||
func LoadDir(root string, opts LoaderOptions) ([]Article, error) {
|
||||
var out []Article
|
||||
seen := make(map[string]bool)
|
||||
|
||||
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
@@ -38,37 +44,15 @@ func LoadDir(root string) ([]Article, error) {
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
if ext != ".md" && ext != ".html" {
|
||||
return nil // uninteressante Datei
|
||||
if ext != ".md" && ext != ".markdown" && ext != ".html" && ext != ".htm" {
|
||||
return nil
|
||||
}
|
||||
|
||||
raw, err := os.ReadFile(path)
|
||||
meta, body, format, err := readContentFile(path, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read %s: %w", path, err)
|
||||
return err
|
||||
}
|
||||
|
||||
parts := bytes.SplitN(raw, []byte("\n"), 2)
|
||||
if len(parts) < 2 {
|
||||
return fmt.Errorf("%s: missing front‑matter", path)
|
||||
}
|
||||
|
||||
headerJSON, err := extractJSONHeader(parts[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
|
||||
var meta struct {
|
||||
Title string `json:"title"`
|
||||
Date string `json:"date"`
|
||||
Slug string `json:"slug"`
|
||||
Cover string `json:"cover"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := json.Unmarshal(headerJSON, &meta); err != nil {
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
|
||||
// Fallback‑Slug aus Dateinamen
|
||||
if meta.Slug == "" {
|
||||
meta.Slug = strings.TrimSuffix(filepath.Base(path), ext)
|
||||
}
|
||||
@@ -77,11 +61,6 @@ func LoadDir(root string) ([]Article, error) {
|
||||
}
|
||||
seen[meta.Slug] = true
|
||||
|
||||
body := parts[1]
|
||||
if ext == ".md" {
|
||||
body = md.ToHTML(body, nil, mdRenderer) // frischer Parser pro Call
|
||||
}
|
||||
|
||||
date, err := time.Parse("2006-01-02", meta.Date)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: parse date: %w", path, err)
|
||||
@@ -93,7 +72,9 @@ func LoadDir(root string) ([]Article, error) {
|
||||
Date: date,
|
||||
Cover: meta.Cover,
|
||||
Description: meta.Description,
|
||||
Body: template.HTML(body),
|
||||
Body: body,
|
||||
Format: format,
|
||||
SourcePath: path,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
@@ -105,34 +86,7 @@ func LoadDir(root string) ([]Article, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ---------- NEU ----------
|
||||
type StaticPage struct {
|
||||
Title string
|
||||
Slug string
|
||||
Description string
|
||||
Body template.HTML
|
||||
}
|
||||
|
||||
// helper.go – einmal zentral verwenden
|
||||
func extractJSONHeader(line []byte) ([]byte, error) {
|
||||
s := strings.TrimSpace(string(line))
|
||||
s = strings.TrimPrefix(s, "<!--")
|
||||
|
||||
// alles bis zum ersten "-->" abschneiden
|
||||
if idx := strings.Index(s, "-->"); idx != -1 {
|
||||
s = s[:idx]
|
||||
}
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
// optional: falls doch kein JSON, besserer Fehler
|
||||
if !strings.HasPrefix(s, "{") {
|
||||
return nil, fmt.Errorf("no JSON front‑matter found")
|
||||
}
|
||||
return []byte(s), nil
|
||||
}
|
||||
|
||||
// LoadStatic liest alle .md/.html‑Dateien unter dir und liefert sie als Map[slug]StaticPage.
|
||||
func LoadStatic(dir string) (map[string]StaticPage, error) {
|
||||
func LoadStatic(dir string, opts LoaderOptions) (map[string]StaticPage, error) {
|
||||
pages := make(map[string]StaticPage)
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
@@ -144,38 +98,17 @@ func LoadStatic(dir string) (map[string]StaticPage, error) {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
ext := filepath.Ext(e.Name())
|
||||
if ext != ".md" && ext != ".html" {
|
||||
continue // unbekanntes Format
|
||||
ext := strings.ToLower(filepath.Ext(e.Name()))
|
||||
if ext != ".md" && ext != ".markdown" && ext != ".html" && ext != ".htm" {
|
||||
continue
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, e.Name())
|
||||
raw, err := os.ReadFile(path)
|
||||
meta, body, format, err := readContentFile(path, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %s: %w", path, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Front‑Matter (erste Zeile) herauslösen
|
||||
parts := bytes.SplitN(raw, []byte("\n"), 2)
|
||||
if len(parts) < 2 {
|
||||
return nil, fmt.Errorf("%s: missing front‑matter", path)
|
||||
}
|
||||
|
||||
headerJSON, err := extractJSONHeader(parts[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
|
||||
var meta struct {
|
||||
Title string `json:"title"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := json.Unmarshal(headerJSON, &meta); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
|
||||
// Fallback: Slug aus Dateinamen ableiten, falls im Header leer
|
||||
if meta.Slug == "" {
|
||||
meta.Slug = strings.TrimSuffix(e.Name(), ext)
|
||||
}
|
||||
@@ -183,17 +116,88 @@ func LoadStatic(dir string) (map[string]StaticPage, error) {
|
||||
return nil, fmt.Errorf("%s: duplicate slug %q", path, meta.Slug)
|
||||
}
|
||||
|
||||
body := parts[1]
|
||||
if ext == ".md" {
|
||||
body = md.ToHTML(body, nil, nil) // eigener Parser je Aufruf
|
||||
}
|
||||
|
||||
pages[meta.Slug] = StaticPage{
|
||||
Title: meta.Title,
|
||||
Slug: meta.Slug,
|
||||
Description: meta.Description,
|
||||
Body: template.HTML(body),
|
||||
Body: body,
|
||||
Format: format,
|
||||
SourcePath: path,
|
||||
}
|
||||
}
|
||||
|
||||
return pages, nil
|
||||
}
|
||||
|
||||
func readContentFile(path string, opts LoaderOptions) (frontMatter, template.HTML, Format, error) {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return frontMatter{}, "", "", fmt.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
|
||||
parts := bytes.SplitN(raw, []byte("\n"), 2)
|
||||
if len(parts) < 2 {
|
||||
return frontMatter{}, "", "", fmt.Errorf("%s: missing front-matter", path)
|
||||
}
|
||||
|
||||
headerJSON, err := extractJSONHeader(parts[0])
|
||||
if err != nil {
|
||||
return frontMatter{}, "", "", fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
|
||||
var meta frontMatter
|
||||
if err := json.Unmarshal(headerJSON, &meta); err != nil {
|
||||
return frontMatter{}, "", "", fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
format := detectFormat(ext, meta.Format)
|
||||
body := renderBody(parts[1], format, opts)
|
||||
|
||||
return meta, body, format, nil
|
||||
}
|
||||
|
||||
func detectFormat(ext, explicit string) Format {
|
||||
switch strings.ToLower(strings.TrimSpace(explicit)) {
|
||||
case "html":
|
||||
return FormatHTML
|
||||
case "md", "markdown":
|
||||
return FormatMarkdown
|
||||
}
|
||||
|
||||
switch ext {
|
||||
case ".html", ".htm":
|
||||
return FormatHTML
|
||||
default:
|
||||
return FormatMarkdown
|
||||
}
|
||||
}
|
||||
|
||||
func renderBody(body []byte, format Format, opts LoaderOptions) template.HTML {
|
||||
if format == FormatMarkdown {
|
||||
renderer := html.NewRenderer(html.RendererOptions{
|
||||
Flags: html.CommonFlags | html.HrefTargetBlank,
|
||||
})
|
||||
return template.HTML(md.ToHTML(body, nil, renderer))
|
||||
}
|
||||
|
||||
if opts.TrustedHTML {
|
||||
return template.HTML(body)
|
||||
}
|
||||
|
||||
// Sicherheits-Fallback: HTML-Dateien werden escaped, solange BLOG_TRUSTED_HTML=false ist.
|
||||
return template.HTML(template.HTMLEscapeString(string(body)))
|
||||
}
|
||||
|
||||
func extractJSONHeader(line []byte) ([]byte, error) {
|
||||
s := strings.TrimSpace(string(line))
|
||||
s = strings.TrimPrefix(s, "<!--")
|
||||
if idx := strings.Index(s, "-->"); idx != -1 {
|
||||
s = s[:idx]
|
||||
}
|
||||
s = strings.TrimSpace(s)
|
||||
if !strings.HasPrefix(s, "{") {
|
||||
return nil, fmt.Errorf("no JSON front-matter found")
|
||||
}
|
||||
return []byte(s), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user