Files
hotdog-cms/internal/build/templates.go
T

187 lines
5.0 KiB
Go

package build
import (
"fmt"
"html/template"
"os"
"path/filepath"
"sort"
"strings"
"git.coffeylabs.org/coffey-labs/hotdog-cms/internal/site"
)
// Templates holds one parsed set per layout. Each set is the shared part
// (layouts/base.html, every partial, every section) plus that layout's own
// file, which fills the blocks base.html leaves open.
//
// Names, as a template refers to them:
//
// {{ template "head" . }} partials/head.html
// {{ template "section/hero" . }} sections/hero.html
// {{ template "sections" . }} every section in the page's front matter
type Templates struct {
layouts map[string]*template.Template
sections map[string]bool
}
// SectionContext is what a section template receives: its own fields, and
// the page and site around it.
type SectionContext struct {
Type string
Data map[string]any
Page *site.Page
Site *site.Site
}
func loadTemplates(dir string, funcs template.FuncMap) (*Templates, error) {
t := &Templates{layouts: map[string]*template.Template{}, sections: map[string]bool{}}
baseSrc, err := os.ReadFile(filepath.Join(dir, "layouts", "base.html"))
if err != nil {
return nil, fmt.Errorf("layouts/base.html: %w", err)
}
common := template.New("base").Funcs(funcs)
if _, err := common.Parse(string(baseSrc)); err != nil {
return nil, fmt.Errorf("layouts/base.html: %w", err)
}
partials, err := htmlFiles(filepath.Join(dir, "partials"))
if err != nil {
return nil, err
}
for _, name := range partials {
if err := parseInto(common, name, filepath.Join(dir, "partials", name+".html"), "partials/"); err != nil {
return nil, err
}
}
secs, err := htmlFiles(filepath.Join(dir, "sections"))
if err != nil {
return nil, err
}
// One dispatching template, written from the section files that exist, so
// a page can name its sections in front matter without anything being
// looked up by name at run time.
var dispatch strings.Builder
dispatch.WriteString(`{{define "hotdog:section"}}`)
for i, name := range secs {
if err := parseInto(common, "section/"+name, filepath.Join(dir, "sections", name+".html"), "sections/"); err != nil {
return nil, err
}
t.sections[name] = true
kw := "else if"
if i == 0 {
kw = "if"
}
fmt.Fprintf(&dispatch, `{{%s eq .Type %q}}{{template %q .}}`, kw, name, "section/"+name)
}
if len(secs) > 0 {
dispatch.WriteString(`{{end}}`)
}
dispatch.WriteString(`{{end}}`)
dispatch.WriteString(`{{define "sections"}}{{range .Sections}}{{template "hotdog:section" (sectionContext $ .)}}{{end}}{{end}}`)
if _, err := common.Parse(dispatch.String()); err != nil {
return nil, err
}
layouts, err := htmlFiles(filepath.Join(dir, "layouts"))
if err != nil {
return nil, err
}
for _, name := range layouts {
if name == "base" {
continue
}
set, err := common.Clone()
if err != nil {
return nil, err
}
src, err := os.ReadFile(filepath.Join(dir, "layouts", name+".html"))
if err != nil {
return nil, err
}
if _, err := set.New("layout/" + name).Parse(string(src)); err != nil {
return nil, fmt.Errorf("layouts/%s.html: %w", name, err)
}
t.layouts[name] = set
}
return t, nil
}
func parseInto(set *template.Template, name, file, label string) error {
src, err := os.ReadFile(file)
if err != nil {
return err
}
if _, err := set.New(name).Parse(string(src)); err != nil {
return fmt.Errorf("%s%s: %w", label, filepath.Base(file), err)
}
return nil
}
func htmlFiles(dir string) ([]string, error) {
entries, err := os.ReadDir(dir)
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, err
}
var out []string
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".html") {
out = append(out, strings.TrimSuffix(e.Name(), ".html"))
}
}
sort.Strings(out)
return out, nil
}
// layoutFor picks a page's layout: its own front matter, else its
// collection's setting, else the default for its kind.
func (t *Templates) layoutFor(p *site.Page, cfg *site.Config) (string, error) {
try := []string{}
if p.Layout != "" {
if _, ok := t.layouts[p.Layout]; !ok {
return "", fmt.Errorf("layout %q: no layouts/%s.html", p.Layout, p.Layout)
}
return p.Layout, nil
}
switch p.Kind {
case site.KindPage:
if p.IsHome() {
try = append(try, "home")
}
if p.Collection != "" {
if l := cfg.Collections[p.Collection].Layout; l != "" {
try = append(try, l)
}
}
try = append(try, "page")
case site.KindList:
if l := cfg.Collections[p.Collection].ListLayout; l != "" {
try = append(try, l)
}
try = append(try, "list")
case site.KindTerm:
try = append(try, "term", "list")
case site.KindTerms:
try = append(try, "terms", "list")
case site.KindError:
try = append(try, "404", "page")
}
for _, name := range try {
if _, ok := t.layouts[name]; ok {
return name, nil
}
}
return "", fmt.Errorf("no layout for this %s page (tried %s)", p.Kind, strings.Join(try, ", "))
}
func (t *Templates) checkSections(p *site.Page) error {
for _, s := range p.Sections {
if !t.sections[s.Type] {
return fmt.Errorf("section %q: no sections/%s.html", s.Type, s.Type)
}
}
return nil
}