package internal import ( "bytes" "fmt" "html/template" "io" "io/fs" "path/filepath" "strings" "sync" "github.com/Masterminds/sprig" humanize "github.com/dustin/go-humanize" log "github.com/sirupsen/logrus" "git.mills.io/prologic/spyda/internal/templates" ) const ( templatesPath = "templates" baseTemplate = "base.html" partialsTemplate = "_partials.html" baseName = "base" ) type TemplateManager struct { sync.RWMutex debug bool templates map[string]*template.Template funcMap template.FuncMap } func NewTemplateManager(conf *Config) (*TemplateManager, error) { templates := make(map[string]*template.Template) funcMap := sprig.FuncMap() funcMap["time"] = humanize.Time funcMap["hostnameFromURL"] = HostnameFromURL funcMap["prettyURL"] = PrettyURL funcMap["isLocalURL"] = IsLocalURLFactory(conf) funcMap["formatForDateTime"] = FormatForDateTime funcMap["isAdminUser"] = IsAdminUserFactory(conf) m := &TemplateManager{debug: conf.Debug, templates: templates, funcMap: funcMap} if err := m.LoadTemplates(); err != nil { log.WithError(err).Error("error loading templates") return nil, fmt.Errorf("error loading templates: %w", err) } return m, nil } func (m *TemplateManager) LoadTemplates() error { m.Lock() defer m.Unlock() err := fs.WalkDir(templates.FS(), ".", func(path string, d fs.DirEntry, err error) error { if err != nil { log.WithError(err).Error("error walking templates") return fmt.Errorf("error walking templates: %w", err) } fname := d.Name() if !d.IsDir() && fname != baseTemplate { // Skip _partials.html and also editor swap files, to improve the development // cycle. Editors often add suffixes to their swap files, e.g "~" or ".swp" // (Vim) and those files are not parsable as templates, causing panics. if fname == partialsTemplate || !strings.HasSuffix(fname, ".html") { return nil } name := strings.TrimSuffix(fname, filepath.Ext(fname)) t := template.New(name).Option("missingkey=zero") t.Funcs(m.funcMap) template.Must(t.Parse(templates.MustGetTemplate(fname))) template.Must(t.Parse(templates.MustGetTemplate(partialsTemplate))) template.Must(t.Parse(templates.MustGetTemplate(baseTemplate))) m.templates[name] = t } return nil }) if err != nil { log.WithError(err).Error("error loading templates") return fmt.Errorf("error loading templates: %w", err) } return nil } func (m *TemplateManager) Add(name string, template *template.Template) { m.Lock() defer m.Unlock() m.templates[name] = template } func (m *TemplateManager) Exec(name string, ctx *Context) (io.WriterTo, error) { if m.debug { log.Debug("reloading templates in debug mode...") if err := m.LoadTemplates(); err != nil { log.WithError(err).Error("error reloading templates") return nil, fmt.Errorf("error reloading templates: %w", err) } } m.RLock() template, ok := m.templates[name] m.RUnlock() if !ok { log.WithField("name", name).Errorf("template not found") return nil, fmt.Errorf("no such template: %s", name) } if ctx == nil { ctx = &Context{} } buf := bytes.NewBuffer([]byte{}) err := template.ExecuteTemplate(buf, baseName, ctx) if err != nil { log.WithError(err).WithField("name", name).Errorf("error executing template") return nil, fmt.Errorf("error executing template %s: %w", name, err) } return buf, nil } // RenderHTML ... func RenderHTML(tpl string, ctx *Context) (string, error) { t := template.Must(template.New("tpl").Parse(tpl)) buf := bytes.NewBuffer([]byte{}) err := t.Execute(buf, ctx) if err != nil { return "", err } return buf.String(), nil }