Files
b1tsblog/internal/article/load.go
jbergner b7edfdd544
Some checks failed
release-tag / release-image (push) Failing after 1m48s
v2-Neuerstellung
2026-05-18 11:25:31 +02:00

204 lines
4.7 KiB
Go

package article
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"time"
md "github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
)
type LoaderOptions struct {
// TrustedHTML erlaubt rohe .html-Inhalte aus deinem Content-Repository.
// Achtung: Nur aktivieren, wenn du dem Repository vertraust.
TrustedHTML bool
}
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 {
return walkErr
}
if d.IsDir() {
return nil
}
ext := strings.ToLower(filepath.Ext(path))
if ext != ".md" && ext != ".markdown" && ext != ".html" && ext != ".htm" {
return nil
}
meta, body, format, err := readContentFile(path, opts)
if err != nil {
return err
}
if meta.Slug == "" {
meta.Slug = strings.TrimSuffix(filepath.Base(path), ext)
}
if seen[meta.Slug] {
return fmt.Errorf("%s: duplicate slug %q", path, meta.Slug)
}
seen[meta.Slug] = true
date, err := time.Parse("2006-01-02", meta.Date)
if err != nil {
return fmt.Errorf("%s: parse date: %w", path, err)
}
out = append(out, Article{
Title: meta.Title,
Slug: meta.Slug,
Date: date,
Cover: meta.Cover,
Description: meta.Description,
Body: body,
Format: format,
SourcePath: path,
})
return nil
})
if err != nil {
return nil, err
}
sort.Slice(out, func(i, j int) bool { return out[i].Date.After(out[j].Date) })
return out, nil
}
func LoadStatic(dir string, opts LoaderOptions) (map[string]StaticPage, error) {
pages := make(map[string]StaticPage)
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
for _, e := range entries {
if e.IsDir() {
continue
}
ext := strings.ToLower(filepath.Ext(e.Name()))
if ext != ".md" && ext != ".markdown" && ext != ".html" && ext != ".htm" {
continue
}
path := filepath.Join(dir, e.Name())
meta, body, format, err := readContentFile(path, opts)
if err != nil {
return nil, err
}
if meta.Slug == "" {
meta.Slug = strings.TrimSuffix(e.Name(), ext)
}
if _, dup := pages[meta.Slug]; dup {
return nil, fmt.Errorf("%s: duplicate slug %q", path, meta.Slug)
}
pages[meta.Slug] = StaticPage{
Title: meta.Title,
Slug: meta.Slug,
Description: meta.Description,
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
}