kiln/templates.go
Adnan Maolood 299cb5e1d1 templates: Fix base template block customization
Previously, kiln would error out if any blocks defined in the base
template were not defined in page or index templates. Instead, make
customizing base template blocks optional. This also allows base
template blocks to have a default value.
2022-09-27 08:43:24 -04:00

203 lines
4.6 KiB
Go

package main
import (
"fmt"
htemplate "html/template"
"io"
"io/fs"
"os"
pathpkg "path"
"strings"
"text/template"
)
// Template represents a template.
type Template interface {
Clone() (Template, error)
AddTemplates(Template) error
Execute(io.Writer, interface{}) error
}
type textTemplate struct {
tmpl *template.Template
}
func (t textTemplate) Clone() (Template, error) {
clone, err := t.tmpl.Clone()
return textTemplate{clone}, err
}
func (t textTemplate) AddTemplates(other Template) error {
otherTmpl := other.(textTemplate).tmpl
for _, def := range otherTmpl.Templates() {
if def.Name() == otherTmpl.Name() {
continue
}
_, err := t.tmpl.AddParseTree(def.Name(), def.Tree)
if err != nil {
return err
}
}
return nil
}
func (t textTemplate) Execute(w io.Writer, data interface{}) error {
return t.tmpl.Execute(w, data)
}
type htmlTemplate struct {
tmpl *htemplate.Template
}
func (t htmlTemplate) Clone() (Template, error) {
clone, err := t.tmpl.Clone()
return htmlTemplate{clone}, err
}
func (t htmlTemplate) AddTemplates(other Template) error {
otherTmpl := other.(htmlTemplate).tmpl
for _, def := range otherTmpl.Templates() {
if def.Name() == otherTmpl.Name() {
continue
}
_, err := t.tmpl.AddParseTree(def.Name(), def.Tree)
if err != nil {
return err
}
}
return nil
}
func (t htmlTemplate) Execute(w io.Writer, data interface{}) error {
return t.tmpl.Execute(w, data)
}
// Templates contains site templates.
type Templates struct {
tmpls map[string]Template
funcs map[string]interface{}
}
// Funcs sets the functions available to newly created templates.
func (t *Templates) Funcs(funcs map[string]interface{}) {
t.funcs = funcs
}
// LoadTemplate loads a template from the provided filenames.
func (t *Templates) LoadTemplate(fsys fs.FS, path string) error {
if t.tmpls == nil {
t.tmpls = map[string]Template{}
}
if ext := pathpkg.Ext(path); ext == ".html" || ext == ".xml" {
return t.loadHTMLTemplate(fsys, path)
}
return t.loadTextTemplate(fsys, path)
}
func (t *Templates) loadTextTemplate(fsys fs.FS, path string) error {
tmpl := template.New(path).Funcs(t.funcs)
b, err := fs.ReadFile(fsys, path)
if err != nil {
return err
}
if _, err := tmpl.Parse(string(b)); err != nil {
return err
}
t.tmpls[path] = textTemplate{tmpl}
return nil
}
func (t *Templates) loadHTMLTemplate(fsys fs.FS, path string) error {
tmpl := htemplate.New(path).Funcs(t.funcs)
b, err := fs.ReadFile(fsys, path)
if err != nil {
return err
}
if _, err := tmpl.Parse(string(b)); err != nil {
return err
}
t.tmpls[path] = htmlTemplate{tmpl}
return nil
}
// Load loads templates from the provided directory.
func (t *Templates) Load(dir string, exts []string) error {
fsys := os.DirFS(dir)
err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.Type().IsRegular() || d.Type()&fs.ModeSymlink != 0 {
if err := t.LoadTemplate(fsys, path); err != nil {
return err
}
}
return nil
})
if err != nil && !os.IsNotExist(err) {
return err
}
// Add base templates
var extsMap = map[string]struct{}{}
for _, ext := range exts {
extsMap[ext] = struct{}{}
}
for path := range t.tmpls {
ext := pathpkg.Ext(path)
if _, ok := extsMap[ext]; !ok {
continue
}
basePath := pathpkg.Join(pathpkg.Dir(path), "base"+ext)
if path == basePath {
continue
}
if base, ok := t.tmpls[basePath]; ok {
tmpl, err := base.Clone()
if err != nil {
return err
}
// Load customized template definitions
if err := tmpl.AddTemplates(t.tmpls[path]); err != nil {
return err
}
t.tmpls[path] = tmpl
}
}
return nil
}
// FindTemplate returns the template for the given path.
func (t *Templates) FindTemplate(path string, tmpl string) (Template, bool) {
tmplPath := pathpkg.Join(path, tmpl)
if t, ok := t.tmpls[tmplPath]; ok {
return t, true
}
if t, ok := t.tmpls[pathpkg.Join("_default", tmpl)]; ok {
return t, true
}
// Failed to find template
return nil, false
}
// FindPartial returns the partial template of the given name.
func (t *Templates) FindPartial(name string) (Template, bool) {
if t, ok := t.tmpls[pathpkg.Join("_partials", name)]; ok {
return t, true
}
return nil, false
}
// ExecutePartial executes the partial with the given name.
func (t *Templates) ExecutePartial(name string, data interface{}) (string, error) {
tmpl, ok := t.FindPartial(name)
if !ok {
return "", fmt.Errorf("Error: partial %q not found", name)
}
var b strings.Builder
if err := tmpl.Execute(&b, data); err != nil {
return "", err
}
return b.String(), nil
}