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") )