diff --git a/xtemplate/hot.go b/xtemplate/hot.go new file mode 100644 index 0000000..d1cd4f3 --- /dev/null +++ b/xtemplate/hot.go @@ -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[0].([]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") +)