blog/core/templates.go

201 lines
5.5 KiB
Go

package core
import (
"html/template"
"io"
"net/http"
"strings"
"time"
"github.com/kirsle/blog/core/forms"
"github.com/kirsle/blog/core/models/settings"
"github.com/kirsle/blog/core/models/users"
)
// Vars is an interface to implement by the templates to pass their own custom
// variables in. It auto-loads global template variables (site name, etc.)
// when the template is rendered.
type Vars struct {
// Global, "constant" template variables.
SetupNeeded bool
Title string
Path string
LoggedIn bool
CurrentUser *users.User
CSRF string
Editable bool // page is editable
Request *http.Request
RequestTime time.Time
RequestDuration time.Duration
// Configuration variables
NoLayout bool // don't wrap in .layout.html, just render the template
// Common template variables.
Message string
Flashes []string
Error error
Data map[interface{}]interface{}
Form forms.Form
}
// NewVars initializes a Vars struct with the custom Data map initialized.
// You may pass in an initial value for this map if you want.
func NewVars(data ...map[interface{}]interface{}) *Vars {
var value map[interface{}]interface{}
if len(data) > 0 {
value = data[0]
} else {
value = make(map[interface{}]interface{})
}
return &Vars{
Data: value,
}
}
// LoadDefaults combines template variables with default, globally available vars.
func (v *Vars) LoadDefaults(b *Blog, r *http.Request) {
// Get the site settings.
s, err := settings.Load()
if err != nil {
s = settings.Defaults()
}
if s.Initialized == false && !strings.HasPrefix(r.URL.Path, "/initial-setup") {
v.SetupNeeded = true
}
v.Request = r
v.RequestTime = r.Context().Value(requestTimeKey).(time.Time)
v.Title = s.Site.Title
v.Path = r.URL.Path
user, err := b.CurrentUser(r)
v.CurrentUser = user
v.LoggedIn = err == nil
}
// // TemplateVars is an interface that describes the template variable struct.
// type TemplateVars interface {
// LoadDefaults(*Blog, *http.Request)
// }
// RenderPartialTemplate handles rendering a Go template to a writer, without
// doing anything extra to the vars or dealing with net/http. This is ideal for
// rendering partials, such as comment partials.
//
// This will wrap the template in `.layout.gohtml` by default. To render just
// a bare template on its own, i.e. for partial templates, create a Vars struct
// with `Vars{NoIndex: true}`
func (b *Blog) RenderPartialTemplate(w io.Writer, path string, v interface{}, withLayout bool, functions map[string]interface{}) error {
var (
layout Filepath
templateName string
err error
)
// Find the file path to the template.
filepath, err := b.ResolvePath(path)
if err != nil {
log.Error("RenderTemplate(%s): file not found", path)
return err
}
// Get the layout template.
if withLayout {
templateName = "layout"
layout, err = b.ResolvePath(".layout")
if err != nil {
log.Error("RenderTemplate(%s): layout template not found", path)
return err
}
} else {
templateName = filepath.Basename
}
// The comment entry partial.
commentEntry, err := b.ResolvePath("comments/entry.partial")
if err != nil {
log.Error("RenderTemplate(%s): comments/entry.partial not found")
return err
}
// Template functions.
funcmap := template.FuncMap{
"StringsJoin": strings.Join,
"Now": time.Now,
"RenderIndex": b.RenderIndex,
"RenderPost": b.RenderPost,
"RenderTags": b.RenderTags,
"TemplateName": func() string {
return filepath.URI
},
}
if functions != nil {
for name, fn := range functions {
funcmap[name] = fn
}
}
// Useful template functions.
t := template.New(filepath.Absolute).Funcs(funcmap)
// Parse the template files. The layout comes first because it's the wrapper
// and allows the filepath template to set the page title.
var templates []string
if withLayout {
templates = append(templates, layout.Absolute)
}
t, err = t.ParseFiles(append(templates, commentEntry.Absolute, filepath.Absolute)...)
if err != nil {
log.Error(err.Error())
return err
}
err = t.ExecuteTemplate(w, templateName, v)
if err != nil {
log.Error("Template parsing error: %s", err)
return err
}
return nil
}
// RenderTemplate responds with an HTML template.
//
// The vars will be massaged a bit to load the global defaults (such as the
// website title and user login status), the user's session may be updated with
// new CSRF token, and other such things. If you just want to render a template
// without all that nonsense, use RenderPartialTemplate.
func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path string, vars *Vars) error {
// Inject globally available variables.
if vars == nil {
vars = &Vars{}
}
vars.LoadDefaults(b, r)
// Add any flashed messages from the endpoint controllers.
session := b.Session(r)
if flashes := session.Flashes(); len(flashes) > 0 {
for _, flash := range flashes {
_ = flash
vars.Flashes = append(vars.Flashes, flash.(string))
}
session.Save(r, w)
}
vars.RequestDuration = time.Now().Sub(vars.RequestTime)
vars.CSRF = b.GenerateCSRFToken(w, r, session)
vars.Editable = !strings.HasPrefix(path, "admin/")
w.Header().Set("Content-Type", "text/html; encoding=UTF-8")
b.RenderPartialTemplate(w, path, vars, true, template.FuncMap{
"RenderComments": func(subject string, ids ...string) template.HTML {
session := b.Session(r)
csrf := b.GenerateCSRFToken(w, r, session)
return b.RenderComments(session, csrf, r.URL.Path, subject, ids...)
},
})
return nil
}