feat(xtemplate,hot): HotTemplate, providing hot reloading for text/ and html/template

This commit is contained in:
Guz
2025-10-21 21:46:24 -03:00
parent 0281e7ac20
commit 1e7f79bfbf

283
xtemplate/hot.go Normal file
View File

@@ -0,0 +1,283 @@
package xtemplate
import (
"errors"
"fmt"
htmltemplate "html/template"
"io"
"io/fs"
"slices"
"text/template"
"text/template/parse"
)
func NewHot[T template.Template | htmltemplate.Template](name string) *HotTemplate {
return Hot(New[T](name))
}
func Hot(t Template) *HotTemplate {
return &HotTemplate{template: t}
}
type HotTemplate struct {
template Template
err error
methodQueue []struct {
name methodName
args []any
}
}
var _ Template = (*HotTemplate)(nil)
func (t *HotTemplate) AddParseTree(name string, tree *parse.Tree) (Template, error) {
return t.queue(methodAddParseTree, []any{name, tree})
}
func (t *HotTemplate) Clone() (Template, error) {
return t.clone()
}
func (t *HotTemplate) DefinedTemplates() string {
temp, err := t.run()
if err != nil {
return ""
}
return temp.DefinedTemplates()
}
func (t *HotTemplate) Delims(left, right string) Template {
t, _ = t.queue(methodDelims, []any{left, right})
return t
}
func (t *HotTemplate) Execute(wr io.Writer, data any) error {
temp, err := t.run()
if err != nil {
return errors.Join(ErrHotRun, err)
}
return temp.Execute(wr, data)
}
func (t *HotTemplate) ExecuteTemplate(wr io.Writer, name string, data any) error {
temp, err := t.run()
if err != nil {
return errors.Join(ErrHotRun, err)
}
return temp.ExecuteTemplate(wr, name, data)
}
func (t *HotTemplate) Funcs(funcMap template.FuncMap) Template {
t, _ = t.queue(methodFuncs, funcMap)
return t
}
func (t *HotTemplate) Lookup(name string) Template {
temp, err := t.run()
if err != nil {
return nil
}
return temp.Lookup(name)
}
func (t *HotTemplate) Name() string {
temp, err := t.run()
if err != nil {
return ""
}
return temp.Name()
}
func (t *HotTemplate) New(name string) Template {
temp, err := t.run()
if err != nil {
return nil
}
return Hot(temp.New(name))
}
func (t *HotTemplate) Option(opt ...string) Template {
t, _ = t.queue(methodOption, opt)
return t
}
func (t *HotTemplate) Parse(text string) (Template, error) {
return t.queue(methodParse, text)
}
func (t *HotTemplate) ParseFS(fs fs.FS, patterns ...string) (Template, error) {
return t.queue(methodParseFS, fs, patterns)
}
func (t *HotTemplate) ParseFiles(filenames ...string) (Template, error) {
return t.queue(methodParseFiles, filenames)
}
func (t *HotTemplate) ParseGlob(pattern string) (Template, error) {
return t.queue(methodParseGlob, pattern)
}
func (t *HotTemplate) Templates() []Template {
temp, err := t.run()
if err != nil {
return nil
}
ts := temp.Templates()
hts := make([]Template, len(ts))
for i := range ts {
hts[i] = Hot(ts[i])
}
return hts
}
func (t *HotTemplate) queue(method methodName, args ...any) (*HotTemplate, error) {
tc, err := t.clone()
if err != nil {
// HACK: Storing the error so it can be returned on methods that can
t.err = errors.Join(ErrHotQueue, err)
return t, err
}
tc.methodQueue = append(t.methodQueue, struct {
name methodName
args []any
}{
name: method,
args: args,
})
return tc, nil
}
func (t *HotTemplate) clone() (*HotTemplate, error) {
temp, err := t.template.Clone()
if err != nil {
return nil, fmt.Errorf("HotTemplate: unable to clone template: %w", err)
}
return &HotTemplate{
template: temp,
methodQueue: slices.Clone(t.methodQueue),
}, nil
}
func (t *HotTemplate) run() (Template, error) {
if t.err != nil {
return t, fmt.Errorf("HotTemplate: template has a past error: %w", t.err)
}
temp, err := t.template.Clone()
if err != nil {
return temp, fmt.Errorf("HotTemplate: unable to clone template: %w", err)
}
for _, method := range t.methodQueue {
switch method.name {
case methodAddParseTree:
name, ok := method.args[0].(string)
if !ok {
err = fmt.Errorf("HotTemplate: first argument is not of type string")
break
}
tree, ok := method.args[1].(*parse.Tree)
if !ok {
err = fmt.Errorf("HotTemplate: second argument is not of type *parse.Tree")
break
}
temp, err = temp.AddParseTree(name, tree)
case methodDelims:
left, ok := method.args[0].(string)
if !ok {
err = fmt.Errorf("HotTemplate: first argument is not of type string")
break
}
right, ok := method.args[1].(string)
if !ok {
err = fmt.Errorf("HotTemplate: second argument is not of type string")
break
}
temp = temp.Delims(left, right)
case methodFuncs:
funcMap, ok := method.args[0].(template.FuncMap)
if !ok {
err = fmt.Errorf("HotTemplate: first argument is not of type template.FuncMap")
break
}
temp = temp.Funcs(funcMap)
case methodOption:
opt, ok := method.args[0].([]string)
if !ok {
err = fmt.Errorf("HotTemplate: first argument is not of type []string")
break
}
temp = temp.Option(opt...)
case methodParse:
text, ok := method.args[0].(string)
if !ok {
err = fmt.Errorf("HotTemplate: first argument is not of type string")
break
}
temp, err = temp.Parse(text)
case methodParseFS:
fs, ok := method.args[0].(fs.FS)
if !ok {
err = fmt.Errorf("HotTemplate: first argument is not of type fs.FS")
break
}
patterns, ok := method.args[1].([]string)
if !ok {
err = fmt.Errorf("HotTemplate: second argument is not of type []string")
break
}
temp, err = temp.ParseFS(fs, patterns...)
case methodParseFiles:
filenames, ok := method.args[0].([]string)
if !ok {
err = fmt.Errorf("HotTemplate: first argument is not of type []string")
break
}
temp, err = temp.ParseFiles(filenames...)
case methodParseGlob:
pattern, ok := method.args[0].(string)
if !ok {
err = fmt.Errorf("HotTemplate: first argument is not of type string")
break
}
temp, err = temp.ParseGlob(pattern)
default:
err = fmt.Errorf("HotTemplate: method %q does not exist", method.name)
}
if err != nil {
return temp, fmt.Errorf("HotTemplate: failed to run method %q with arguments %v: %w",
method.name, method.args, err)
}
}
return temp, nil
}
const (
methodAddParseTree methodName = "AddParseTree"
methodDelims methodName = "Delims"
methodFuncs methodName = "Funcs"
methodOption methodName = "Option"
methodParse methodName = "Parse"
methodParseFS methodName = "ParseFS"
methodParseFiles methodName = "ParseFiles"
methodParseGlob methodName = "ParseGlob"
)
type methodName string
var (
ErrHotRun = errors.New("HotTemplate: unable to run queued methods")
ErrHotQueue = errors.New("HotTemplate: unable to queue method")
)