package templates import ( "fmt" "html/template" "io" "net/http" "os" "time" "code.nonshy.com/nonshy/website/pkg/config" "code.nonshy.com/nonshy/website/pkg/log" "code.nonshy.com/nonshy/website/pkg/session" ) // Template is a logical HTML template for the app with ability to wrap around an html/template // and provide middlewares, hooks or live reloading capability in debug mode. type Template struct { filename string // Filename on disk (index.html) filepath string // Full path on disk (./web/templates/index.html) modified time.Time // Modification date of the file at init time tmpl *template.Template } // LoadTemplate processes and returns a template. Filename is relative // to the template directory, e.g. "index.html". Call this at the initialization // of your endpoint controller; in debug mode the template HTML from disk may be // reloaded if modified after initial load. func LoadTemplate(filename string) (*Template, error) { filepath := config.TemplatePath + "/" + filename stat, err := os.Stat(filepath) if err != nil { return nil, fmt.Errorf("LoadTemplate(%s): %s", filename, err) } files := templates(config.TemplatePath + "/" + filename) tmpl := template.New("page") tmpl.Funcs(TemplateFuncs(nil)) tmpl.ParseFiles(files...) return &Template{ filename: filename, filepath: filepath, modified: stat.ModTime(), tmpl: tmpl, }, nil } // LoadCustom loads a bare template without the site theme and partial templates attached. // // The custom TempleFuncs and vars are still available (PrettyTitle, .CurrentUser, etc.) func LoadCustom(filename string) (*Template, error) { filepath := config.TemplatePath + "/" + filename stat, err := os.Stat(filepath) if err != nil { return nil, fmt.Errorf("LoadTemplate(%s): %s", filename, err) } tmpl := template.New("page") tmpl.Funcs(TemplateFuncs(nil)) tmpl.ParseFiles(filepath) return &Template{ filename: filename, filepath: filepath, modified: stat.ModTime(), tmpl: tmpl, }, nil } // Must LoadTemplate or panic. func Must(filename string) *Template { tmpl, err := LoadTemplate(filename) if err != nil { panic(err) } return tmpl } // Must LoadCustom or panic. func MustLoadCustom(filename string) *Template { tmpl, err := LoadCustom(filename) if err != nil { panic(err) } return tmpl } // Execute a loaded template. In debug mode, the template file may be reloaded // from disk if the file on disk has been modified. func (t *Template) Execute(w http.ResponseWriter, r *http.Request, vars map[string]interface{}) error { if vars == nil { vars = map[string]interface{}{} } // Merge in global variables. MergeVars(r, vars) MergeUserVars(r, vars) // Merge the flashed messsage variables in. if r != nil { sess := session.Get(r) flashes, errors := sess.ReadFlashes(w) vars["Flashes"] = flashes vars["Errors"] = errors } // Reload the template from disk? if t.IsModifiedLocally() { if err := t.Reload(); err != nil { log.Error("Reloading error: %s", err) } } // Install the function map. tmpl := t.tmpl if r != nil { tmpl = t.tmpl.Funcs(TemplateFuncs(r)) } if err := tmpl.ExecuteTemplate(w, "base", vars); err != nil { return err } return nil } // IsModifiedLocally checks if any of the template partials of your Template have // had their files locally on disk modified, so to know to reload them. func (t *Template) IsModifiedLocally() bool { // Check all the template files from base.html, to partials, to our filepath. var files = templates(t.filepath) for _, filename := range files { if stat, err := os.Stat(filename); err == nil { if stat.ModTime().After(t.modified) { log.Info("Template(%s).Execute: file %s updated on disk, reloading", t.filename, filename) return true } } } return false } // Reload the template from disk. func (t *Template) Reload() error { stat, err := os.Stat(t.filepath) if err != nil { return fmt.Errorf("Reload(%s): %s", t.filename, err) } files := templates(t.filepath) tmpl := template.New("page") tmpl.Funcs(TemplateFuncs(nil)) tmpl.ParseFiles(files...) t.tmpl = tmpl t.modified = stat.ModTime() return nil } // Base template layout. var baseTemplates = []string{ config.TemplatePath + "/base.html", config.TemplatePath + "/partials/user_avatar.html", config.TemplatePath + "/partials/like_modal.html", config.TemplatePath + "/partials/right_click.html", config.TemplatePath + "/partials/mark_explicit.html", config.TemplatePath + "/partials/themes.html", } // templates returns a template chain with the base templates preceding yours. // Files given are expected to be full paths (config.TemplatePath + file) func templates(files ...string) []string { return append(baseTemplates, files...) } // RenderTemplate executes a template. Filename is relative to the templates // root, e.g. "index.html" func RenderTemplate(w io.Writer, r *http.Request, filename string, vars map[string]interface{}) error { if vars == nil { vars = map[string]interface{}{} } // Merge in user vars. MergeVars(r, vars) MergeUserVars(r, vars) files := templates(config.TemplatePath + "/" + filename) tmpl := template.Must( template.New("index").ParseFiles(files...), ) err := tmpl.ExecuteTemplate(w, "base", vars) if err != nil { return err } return nil }