summaryrefslogtreecommitdiffstats
path: root/internal/builder/builder.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/builder/builder.go')
-rw-r--r--internal/builder/builder.go288
1 files changed, 0 insertions, 288 deletions
diff --git a/internal/builder/builder.go b/internal/builder/builder.go
deleted file mode 100644
index 59bb71b..0000000
--- a/internal/builder/builder.go
+++ /dev/null
@@ -1,288 +0,0 @@
-package builder
-
-import (
- "fmt"
- "html/template"
- "os"
- "path/filepath"
- "strings"
- "time"
-
- "github.com/fsnotify/fsnotify"
- "nebbet.no/internal/db"
-)
-
-// Builder orchestrates the markdown → HTML build pipeline.
-type Builder struct {
- ContentDir string
- OutputDir string
- TemplateDir string
- ComponentDir string
- LibDir string
- MetaDB *db.MetaDB
- SearchDB *db.SearchDB
- tmpl *template.Template
-}
-
-func New(contentDir, outputDir string, meta *db.MetaDB, search *db.SearchDB) *Builder {
- return &Builder{
- ContentDir: contentDir,
- OutputDir: outputDir,
- TemplateDir: "templates",
- ComponentDir: "components",
- LibDir: "lib",
- MetaDB: meta,
- SearchDB: search,
- }
-}
-
-// PageData is passed to HTML templates.
-type PageData struct {
- Title string
- Content template.HTML
- // ImportMapTag is the full <script type="importmap">…</script> block,
- // pre-rendered as safe HTML so the JSON inside is never entity-escaped.
- ImportMapTag template.HTML
- ComponentScripts []string
- Date string
- Tags []string
- Path string
-}
-
-// BuildAll performs a full site build.
-func (b *Builder) BuildAll() error {
- if err := b.loadTemplates(); err != nil {
- return fmt.Errorf("load templates: %w", err)
- }
- importMap, err := GenerateImportMap(b.LibDir)
- if err != nil {
- return fmt.Errorf("importmap: %w", err)
- }
- return filepath.WalkDir(b.ContentDir, func(path string, d os.DirEntry, err error) error {
- if err != nil || d.IsDir() || !strings.HasSuffix(path, ".md") {
- return err
- }
- // Skip content/admin/ — served dynamically by the admin HTTP server.
- rel, _ := filepath.Rel(b.ContentDir, path)
- if strings.HasPrefix(filepath.ToSlash(rel), "admin/") || filepath.ToSlash(rel) == "admin" {
- return nil
- }
- return b.BuildFile(path, importMap)
- })
-}
-
-// BuildFile converts a single markdown file and updates both databases.
-func (b *Builder) BuildFile(mdPath, importMap string) error {
- data, err := os.ReadFile(mdPath)
- if err != nil {
- return err
- }
-
- fm, body := ParseFrontmatter(string(data))
- if fm.Draft {
- fmt.Printf("skip draft: %s\n", mdPath)
- return nil
- }
-
- htmlBody, err := MarkdownToHTML(body)
- if err != nil {
- return fmt.Errorf("markdown: %w", err)
- }
- htmlBody = ProcessComponents(htmlBody)
- scripts := FindComponentScripts(htmlBody, b.ComponentDir)
-
- // Derive URL path and output file path from content-relative path.
- rel, _ := filepath.Rel(b.ContentDir, mdPath)
- urlPath := "/" + filepath.ToSlash(strings.TrimSuffix(rel, ".md"))
- // /index → / and /section/index → /section
- switch {
- case urlPath == "/index":
- urlPath = "/"
- case strings.HasSuffix(urlPath, "/index"):
- urlPath = strings.TrimSuffix(urlPath, "/index")
- }
- outPath := filepath.Join(b.OutputDir, filepath.FromSlash(
- strings.TrimSuffix(filepath.ToSlash(rel), ".md")+".html"))
-
- if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
- return err
- }
-
- var importMapTag template.HTML
- if importMap != "" {
- importMapTag = template.HTML(
- "<script type=\"importmap\">" + importMap + "</script>")
- }
- page := PageData{
- Title: fm.Title,
- Content: template.HTML(htmlBody),
- ImportMapTag: importMapTag,
- ComponentScripts: scripts,
- Date: fm.Date,
- Tags: fm.Tags,
- Path: urlPath,
- }
-
- tmplName := fm.Layout + ".html"
- f, err := os.Create(outPath)
- if err != nil {
- return err
- }
- defer f.Close()
- if err := b.tmpl.ExecuteTemplate(f, tmplName, page); err != nil {
- return fmt.Errorf("template %s: %w", tmplName, err)
- }
-
- if err := b.MetaDB.UpsertPage(db.PageMeta{
- Path: urlPath,
- HTMLPath: outPath,
- Title: fm.Title,
- Date: fm.Date,
- Tags: fm.Tags,
- UpdatedAt: time.Now(),
- }); err != nil {
- return fmt.Errorf("meta db: %w", err)
- }
- if err := b.SearchDB.IndexPage(db.SearchPage{
- Path: urlPath,
- Title: fm.Title,
- Content: StripHTML(htmlBody),
- }); err != nil {
- return fmt.Errorf("search db: %w", err)
- }
-
- fmt.Printf("built %s → %s\n", mdPath, outPath)
- return nil
-}
-
-// RemovePage deletes the built HTML and removes the page from both databases.
-func (b *Builder) RemovePage(mdPath string) error {
- rel, _ := filepath.Rel(b.ContentDir, mdPath)
- urlPath := "/" + filepath.ToSlash(strings.TrimSuffix(rel, ".md"))
- switch {
- case urlPath == "/index":
- urlPath = "/"
- case strings.HasSuffix(urlPath, "/index"):
- urlPath = strings.TrimSuffix(urlPath, "/index")
- }
- outPath := filepath.Join(b.OutputDir, filepath.FromSlash(
- strings.TrimSuffix(filepath.ToSlash(rel), ".md")+".html"))
-
- _ = os.Remove(outPath)
- _ = b.MetaDB.DeletePage(urlPath)
- _ = b.SearchDB.DeletePage(urlPath)
- fmt.Printf("removed %s\n", outPath)
- return nil
-}
-
-func (b *Builder) loadTemplates() error {
- tmpl, err := template.ParseGlob(filepath.Join(b.TemplateDir, "*.html"))
- if err != nil {
- return err
- }
- b.tmpl = tmpl
- return nil
-}
-
-// Watch monitors source directories and rebuilds on changes.
-// A 150 ms debounce prevents redundant rebuilds when many files change at once.
-func (b *Builder) Watch() error {
- watcher, err := fsnotify.NewWatcher()
- if err != nil {
- return err
- }
- defer watcher.Close()
-
- // Add all dirs (including nested content subdirs) to watcher.
- watchDirs := []string{b.ContentDir, b.TemplateDir, b.ComponentDir, b.LibDir, "styles"}
- for _, dir := range watchDirs {
- if err := addDirRecursive(watcher, dir); err != nil && !os.IsNotExist(err) {
- return err
- }
- }
-
- fmt.Println("watching for changes — Ctrl+C to stop")
-
- var (
- debounce = time.NewTimer(0)
- pendingMD = "" // non-empty → rebuild only this file
- fullBuild = false
- )
- <-debounce.C // drain initial tick
-
- for {
- select {
- case event, ok := <-watcher.Events:
- if !ok {
- return nil
- }
- if !event.Has(fsnotify.Write) && !event.Has(fsnotify.Create) && !event.Has(fsnotify.Remove) {
- continue
- }
-
- // If a new directory appears, start watching it.
- if event.Has(fsnotify.Create) {
- if info, err := os.Stat(event.Name); err == nil && info.IsDir() {
- _ = watcher.Add(event.Name)
- }
- }
-
- isMD := strings.HasSuffix(event.Name, ".md")
- isContentMD := isMD && strings.HasPrefix(
- filepath.ToSlash(event.Name),
- filepath.ToSlash(b.ContentDir),
- )
-
- if isContentMD && !fullBuild {
- if event.Has(fsnotify.Remove) {
- b.RemovePage(event.Name)
- pendingMD = ""
- } else if pendingMD == "" {
- pendingMD = event.Name
- } else if pendingMD != event.Name {
- // Multiple different md files → full rebuild.
- fullBuild = true
- pendingMD = ""
- }
- } else {
- // Templates, styles, components, lib, or multiple md changed.
- fullBuild = true
- pendingMD = ""
- }
-
- debounce.Reset(150 * time.Millisecond)
-
- case <-debounce.C:
- importMap, _ := GenerateImportMap(b.LibDir)
- if fullBuild {
- if err := b.loadTemplates(); err == nil {
- _ = b.BuildAll()
- }
- fullBuild = false
- } else if pendingMD != "" {
- if err := b.loadTemplates(); err == nil {
- _ = b.BuildFile(pendingMD, importMap)
- }
- pendingMD = ""
- }
-
- case err, ok := <-watcher.Errors:
- if !ok {
- return nil
- }
- fmt.Fprintf(os.Stderr, "watch error: %v\n", err)
- }
- }
-}
-
-func addDirRecursive(w *fsnotify.Watcher, root string) error {
- return filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
- if err != nil {
- return nil // skip unreadable entries
- }
- if d.IsDir() {
- return w.Add(path)
- }
- return nil
- })
-}