website/pkg/templates/templates.go
Noah dd1e6c2918 Initial commit
* Initial codebase (lot of work!)
* Uses vanilla Go net/http and implements by hand: session cookies
  backed by Redis; log in/out; CSRF protection; email verification flow;
  initial database models (User table)
2022-08-09 22:32:19 -07:00

152 lines
3.8 KiB
Go

package templates
import (
"fmt"
"html/template"
"io"
"net/http"
"os"
"time"
"git.kirsle.net/apps/gosocial/pkg/config"
"git.kirsle.net/apps/gosocial/pkg/log"
"git.kirsle.net/apps/gosocial/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", 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
}
// Must LoadTemplate or panic.
func Must(filename string) *Template {
tmpl, err := LoadTemplate(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 stat, err := os.Stat(t.filepath); err == nil {
if stat.ModTime().After(t.modified) {
log.Info("Template(%s).Execute: file updated on disk, reloading", t.filename)
err = t.Reload()
if 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
}
// 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",
}
// 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
}