187 lines
5.0 KiB
Go
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
|
|
}
|