tplx/tplx.go
2024-12-22 20:57:32 +01:00

115 lines
3.1 KiB
Go

// Package tplx wraps the standard html/template library to provide a little more
// structure and ease of use.
package tplx
import (
"errors"
"fmt"
"html/template"
"io"
"io/fs"
)
var (
// ErrUnknownTemplate is returned when a template is not found in the renderer.
ErrUnknownTemplate = errors.New("template not found in renderer")
// ErrInvalidSpec is returned when the template renderer specification is invalid.
ErrInvalidSpec = errors.New("template renderer spec is invalid")
)
// Renderer is an interface for rendering templates.
type Renderer interface {
Render(w io.Writer, name string, data any, funcs template.FuncMap) error
}
type renderer struct {
m map[string]*template.Template
}
// Spec describes the structure of all templates managed by the renderer.
//
// The keys of the Spec map represent top-level template names. Each key maps
// to a slice of Meta, where each Meta defines the name, path, and functions
// associated with a template fragment.
type Spec map[string][]Meta
// Meta represents metadata for a single template fragment.
//
// Name specifies the name of the template fragment. Path specifies the path to
// the template file in the file system. Funcs provides template-specific
// functions.
type Meta struct {
Name string
Path string
Funcs template.FuncMap
}
// NewRenderer creates a new Renderer instance from a file system, specification,
// and global function map.
//
// The fsys parameter specifies the file system from which template files are
// loaded. The spec parameter defines the structure of the templates, mapping
// top-level template names to their fragments. The funcs parameter provides
// global template functions.
//
// Returns a Renderer instance or an error if the templates cannot be initialized
// according to the specification.
func NewRenderer(fsys fs.FS, spec Spec, funcs template.FuncMap) (Renderer, error) {
r := renderer{
m: make(map[string]*template.Template, len(spec)),
}
for name, metas := range spec {
inc := false
t := template.New(name).Funcs(funcs)
for _, meta := range metas {
if meta.Name == name {
inc = true
}
text, err := fs.ReadFile(fsys, meta.Path)
if err != nil {
return nil, fmt.Errorf("unable to read template file: %w", err)
}
t = t.New(meta.Name).Funcs(meta.Funcs)
t, err = t.Parse(string(text))
if err != nil {
return nil, err
}
}
if !inc {
return nil, ErrInvalidSpec
}
r.m[name] = t
}
return r, nil
}
// Render writes the rendered output of a named template to the provided writer.
//
// The wr parameter specifies the writer where the rendered template output will
// be written. The name parameter specifies the name of the template to render
// The data parameter provides the context data for rendering, and the funcs
// parameter provides additional template functions.
//
// Returns an error if the template cannot be rendered or does not exist.
func (r renderer) Render(wr io.Writer, name string, data any, funcs template.FuncMap) error {
t, ok := r.m[name]
if !ok {
return ErrUnknownTemplate
}
err := t.ExecuteTemplate(wr, name, data)
if err != nil {
return fmt.Errorf("cannot render template: %w", err)
}
return nil
}