feat(xtemplate,hot): HotTemplate, providing hot reloading for text/ and html/template
This commit is contained in:
283
xtemplate/hot.go
Normal file
283
xtemplate/hot.go
Normal 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")
|
||||
)
|
||||
Reference in New Issue
Block a user