Created
December 25, 2024 02:23
-
-
Save zeroidentidad/7b170cb90965399d507b96dfd724e9a6 to your computer and use it in GitHub Desktop.
Set templates with stlib
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package tset | |
import ( | |
"context" | |
"fmt" | |
"html/template" | |
"io" | |
"io/fs" | |
"net/http" | |
"path/filepath" | |
"reflect" | |
"strings" | |
"github.com/Masterminds/sprig/v3" | |
) | |
type TemplateSet struct { | |
templates map[string]*template.Template | |
} | |
// Because the fs.WalkDir call is very similar between loading partials/layouts | |
// and loading page templates, this function has a few flags which allow it to | |
// react in different ways, the most important of which is updateBase, which | |
// updates the baseTemplate rather than cloning it. | |
func loadTemplates(logger *Logger, rootFS fs.FS, dir string, baseTemplate *template.Template, updateBase bool) (map[string]*template.Template, error) { | |
ret := make(map[string]*template.Template) | |
err := fs.WalkDir(rootFS, dir, func(target string, d fs.DirEntry, err error) error { | |
if err != nil { | |
return err | |
} | |
// Ensure we only operate on files ending in .html | |
if d.IsDir() || !strings.HasSuffix(d.Name(), ".html") { | |
return nil | |
} | |
t := baseTemplate | |
if !updateBase { | |
// Clone the baseTemplate so the page template can have access to all | |
// layouts and includes. | |
t, err = baseTemplate.Clone() | |
if err != nil { | |
return err | |
} | |
} | |
// Because this could technically be run on Windows, we need to sanitize | |
// the path name so it's consistent between platforms. | |
templateName := filepath.Base(target) | |
// Skip any templates which have already been loaded. This makes it | |
// possible to have a template directory layout where we preload | |
// includes and layouts, but aren't required to put everything else in a | |
// separate folder. | |
if baseTemplate.Lookup(templateName) != nil { | |
return nil | |
} | |
logger.Debugf("Loading template %s from %q", templateName, target) | |
// Here's another footgun - when using ParseFS, the template package | |
// lops off the directory name and just uses the basename of the file. | |
// Because of that, in order to get template names with the directory in | |
// the name, we have to read the data and call parse ourselves. | |
data, err := fs.ReadFile(rootFS, target) | |
if err != nil { | |
return err | |
} | |
t, err = t.New(templateName).Parse(string(data)) | |
if err != nil { | |
return err | |
} | |
ret[templateName] = t | |
return nil | |
}) | |
if err != nil { | |
return nil, err | |
} | |
return ret, nil | |
} | |
func NewTemplateSet(logger *Logger, rootFS fs.FS, funcs ...template.FuncMap) (*TemplateSet, error) { | |
ts := &TemplateSet{ | |
templates: make(map[string]*template.Template), | |
} | |
baseTemplate := template.New("") | |
// Add any common template funcs we care about - we currently add in all the | |
// sprig helpers . Note that functions need to be set up before loading | |
// templates, or loading the templates will error. | |
baseTemplate.Funcs(sprig.FuncMap()) | |
// Set up any additional built-in functions. | |
baseTemplate.Funcs(template.FuncMap{ | |
"hasField": templateHasField, | |
}) | |
for _, fMap := range funcs { | |
baseTemplate.Funcs(fMap) | |
} | |
// Because the stdlib html/template package has a number of issues, we need | |
// to do the parsing in multiple passes, walking the tree every time. First, | |
// we need to build a base template which has all the layouts, includes, and | |
// functions. Next, we need to load each page template as a separate | |
// template so inheritance works how we'd expect. | |
_, err := loadTemplates(logger, rootFS, "includes", baseTemplate, true) | |
if err != nil { | |
return nil, err | |
} | |
_, err = loadTemplates(logger, rootFS, "layouts", baseTemplate, true) | |
if err != nil { | |
return nil, err | |
} | |
// Walk the pages directory and attempt to parse any templates as top-level | |
// templates. | |
ts.templates, err = loadTemplates(logger, rootFS, ".", baseTemplate, false) | |
if err != nil { | |
return nil, err | |
} | |
return ts, nil | |
} | |
func (ts *TemplateSet) Execute(w io.Writer, name string, data interface{}) error { | |
t, ok := ts.templates[name] | |
if !ok { | |
return fmt.Errorf("unknown page template %q", name) | |
} | |
return t.ExecuteTemplate(w, name, data) | |
} | |
func TemplateMiddleware(ts *TemplateSet) func(http.Handler) http.Handler { | |
return contextValueMiddleware(TemplateSetContextKey, ts) | |
} | |
func Render(ctx context.Context, w io.Writer, name string, data interface{}) { | |
logger := ExtractLogger(ctx).With("template_name", name) | |
logger.Debug("rendering template") | |
templates := ExtractTemplates(ctx) | |
err := templates.Execute(w, name, data) | |
if err != nil { | |
logger.WithError(err).Error("failed to render template") | |
} | |
} | |
func templateHasField(v interface{}, name string) bool { | |
rv := reflect.ValueOf(v) | |
if rv.Kind() == reflect.Ptr { | |
rv = rv.Elem() | |
} | |
if rv.Kind() != reflect.Struct { | |
return false | |
} | |
return rv.FieldByName(name).IsValid() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment