Further simplify template rendering

This commit is contained in:
Noah 2018-02-10 13:16:20 -08:00
parent f0045ae2cf
commit eab7dae75b
12 changed files with 105 additions and 220 deletions

View File

@ -33,7 +33,7 @@ func (b *Blog) AdminRoutes(r *mux.Router) {
// AdminHandler is the admin landing page. // AdminHandler is the admin landing page.
func (b *Blog) AdminHandler(w http.ResponseWriter, r *http.Request) { func (b *Blog) AdminHandler(w http.ResponseWriter, r *http.Request) {
b.RenderTemplate(w, r, "admin/index", render.Vars{}) render.Template(w, r, "admin/index", NewVars())
} }
// FileTree holds information about files in the document roots. // FileTree holds information about files in the document roots.

View File

@ -23,7 +23,6 @@ func (b *Blog) AuthRoutes(r *mux.Router) {
// MustLogin handles errors from the LoginRequired middleware by redirecting // MustLogin handles errors from the LoginRequired middleware by redirecting
// the user to the login page. // the user to the login page.
func (b *Blog) MustLogin(w http.ResponseWriter, r *http.Request) { func (b *Blog) MustLogin(w http.ResponseWriter, r *http.Request) {
log.Info("MustLogin for %s", r.URL.Path)
responses.Redirect(w, "/login?next="+r.URL.Path) responses.Redirect(w, "/login?next="+r.URL.Path)
} }

View File

@ -44,6 +44,10 @@ type Archive struct {
// BlogRoutes attaches the blog routes to the app. // BlogRoutes attaches the blog routes to the app.
func (b *Blog) BlogRoutes(r *mux.Router) { func (b *Blog) BlogRoutes(r *mux.Router) {
render.Funcs["RenderIndex"] = b.RenderIndex
render.Funcs["RenderPost"] = b.RenderPost
render.Funcs["RenderTags"] = b.RenderTags
// Public routes // Public routes
r.HandleFunc("/blog", b.IndexHandler) r.HandleFunc("/blog", b.IndexHandler)
r.HandleFunc("/blog.rss", b.RSSHandler) r.HandleFunc("/blog.rss", b.RSSHandler)
@ -302,19 +306,14 @@ func (b *Blog) RenderIndex(r *http.Request, tag, privacy string) template.HTML {
// Render the blog index partial. // Render the blog index partial.
var output bytes.Buffer var output bytes.Buffer
v := render.Vars{ v := render.Vars{
NoLayout: true,
Data: map[interface{}]interface{}{ Data: map[interface{}]interface{}{
"PreviousPage": previousPage, "PreviousPage": previousPage,
"NextPage": nextPage, "NextPage": nextPage,
"View": view, "View": view,
}, },
} }
v = b.LoadDefaults(v, r) b.RenderTemplate(&output, r, "blog/index.partial", v)
render.PartialTemplate(&output, "blog/index.partial", render.Config{
Request: r,
Vars: &v,
WithLayout: false,
Functions: b.TemplateFuncs(nil, r, nil),
})
return template.HTML(output.String()) return template.HTML(output.String())
} }
@ -333,19 +332,13 @@ func (b *Blog) RenderTags(r *http.Request, indexView bool) template.HTML {
var output bytes.Buffer var output bytes.Buffer
v := render.Vars{ v := render.Vars{
NoLayout: true,
Data: map[interface{}]interface{}{ Data: map[interface{}]interface{}{
"IndexView": indexView, "IndexView": indexView,
"Tags": tags, "Tags": tags,
}, },
} }
v = b.LoadDefaults(v, r) b.RenderTemplate(&output, r, "blog/tags.partial", v)
render.PartialTemplate(&output, "blog/tags.partial", render.Config{
Request: r,
Vars: &v,
WithLayout: false,
Functions: b.TemplateFuncs(nil, nil, nil),
})
// b.RenderPartialTemplate(&output, r, "blog/tags.partial", v, false, nil)
return template.HTML(output.String()) return template.HTML(output.String())
} }
@ -455,6 +448,7 @@ func (b *Blog) RenderPost(r *http.Request, p *posts.Post, indexView bool, numCom
} }
meta := render.Vars{ meta := render.Vars{
NoLayout: true,
Data: map[interface{}]interface{}{ Data: map[interface{}]interface{}{
"Post": p, "Post": p,
"Rendered": rendered, "Rendered": rendered,
@ -465,7 +459,7 @@ func (b *Blog) RenderPost(r *http.Request, p *posts.Post, indexView bool, numCom
}, },
} }
output := bytes.Buffer{} output := bytes.Buffer{}
err = b.RenderPartialTemplate(&output, r, "blog/entry.partial", meta, false, nil) err = b.RenderTemplate(&output, r, "blog/entry.partial", meta)
if err != nil { if err != nil {
return template.HTML(fmt.Sprintf("[template error in blog/entry.partial: %s]", err.Error())) return template.HTML(fmt.Sprintf("[template error in blog/entry.partial: %s]", err.Error()))
} }

View File

@ -10,7 +10,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/gorilla/mux" "github.com/gorilla/mux"
gorilla "github.com/gorilla/sessions"
"github.com/kirsle/blog/core/internal/log" "github.com/kirsle/blog/core/internal/log"
"github.com/kirsle/blog/core/internal/markdown" "github.com/kirsle/blog/core/internal/markdown"
"github.com/kirsle/blog/core/internal/middleware/auth" "github.com/kirsle/blog/core/internal/middleware/auth"
@ -23,6 +22,8 @@ import (
// CommentRoutes attaches the comment routes to the app. // CommentRoutes attaches the comment routes to the app.
func (b *Blog) CommentRoutes(r *mux.Router) { func (b *Blog) CommentRoutes(r *mux.Router) {
render.Funcs["RenderComments"] = b.RenderComments
r.HandleFunc("/comments", b.CommentHandler) r.HandleFunc("/comments", b.CommentHandler)
r.HandleFunc("/comments/subscription", b.SubscriptionHandler) r.HandleFunc("/comments/subscription", b.SubscriptionHandler)
r.HandleFunc("/comments/quick-delete", b.QuickDeleteHandler) r.HandleFunc("/comments/quick-delete", b.QuickDeleteHandler)
@ -40,13 +41,16 @@ type CommentMeta struct {
} }
// RenderComments renders a comment form partial and returns the HTML. // RenderComments renders a comment form partial and returns the HTML.
func (b *Blog) RenderComments(session *gorilla.Session, csrfToken, url, subject string, ids ...string) template.HTML { func (b *Blog) RenderComments(r *http.Request, subject string, ids ...string) template.HTML {
id := strings.Join(ids, "-") id := strings.Join(ids, "-")
session := sessions.Get(r)
url := r.URL.Path
// Load their cached name and email if they posted a comment before. // Load their cached name and email if they posted a comment before.
name, _ := session.Values["c.name"].(string) name, _ := session.Values["c.name"].(string)
email, _ := session.Values["c.email"].(string) email, _ := session.Values["c.email"].(string)
editToken, _ := session.Values["c.token"].(string) editToken, _ := session.Values["c.token"].(string)
csrf, _ := session.Values["csrf"].(string)
// Check if the user is a logged-in admin, to make all comments editable. // Check if the user is a logged-in admin, to make all comments editable.
var isAdmin bool var isAdmin bool
@ -71,7 +75,7 @@ func (b *Blog) RenderComments(session *gorilla.Session, csrfToken, url, subject
c.HTML = template.HTML(markdown.RenderMarkdown(c.Body)) c.HTML = template.HTML(markdown.RenderMarkdown(c.Body))
c.ThreadID = thread.ID c.ThreadID = thread.ID
c.OriginURL = url c.OriginURL = url
c.CSRF = csrfToken c.CSRF = csrf
// Look up the author username. // Look up the author username.
if c.UserID > 0 { if c.UserID > 0 {
@ -120,7 +124,7 @@ func (b *Blog) RenderComments(session *gorilla.Session, csrfToken, url, subject
ID: thread.ID, ID: thread.ID,
OriginURL: url, OriginURL: url,
Subject: subject, Subject: subject,
CSRF: csrfToken, CSRF: csrf,
Thread: &thread, Thread: &thread,
NewComment: comments.Comment{ NewComment: comments.Comment{
Name: name, Name: name,

View File

@ -0,0 +1 @@
package setup

View File

@ -5,7 +5,6 @@ import (
"errors" "errors"
"net/http" "net/http"
"github.com/kirsle/blog/core/internal/log"
"github.com/kirsle/blog/core/internal/models/users" "github.com/kirsle/blog/core/internal/models/users"
"github.com/kirsle/blog/core/internal/sessions" "github.com/kirsle/blog/core/internal/sessions"
"github.com/kirsle/blog/core/internal/types" "github.com/kirsle/blog/core/internal/types"
@ -46,8 +45,6 @@ func LoginRequired(onError http.HandlerFunc) negroni.HandlerFunc {
return return
} }
} }
log.Info("Redirect away!")
onError(w, r) onError(w, r)
} }

View File

@ -0,0 +1,14 @@
package render
import (
"html/template"
"strings"
"time"
)
// Funcs is a global funcmap that the blog can hook its internal
// methods onto.
var Funcs = template.FuncMap{
"StringsJoin": strings.Join,
"Now": time.Now,
}

View File

@ -9,23 +9,14 @@ import (
"github.com/kirsle/blog/core/internal/forms" "github.com/kirsle/blog/core/internal/forms"
"github.com/kirsle/blog/core/internal/log" "github.com/kirsle/blog/core/internal/log"
"github.com/kirsle/blog/core/internal/middleware"
"github.com/kirsle/blog/core/internal/middleware/auth"
"github.com/kirsle/blog/core/internal/models/settings"
"github.com/kirsle/blog/core/internal/models/users" "github.com/kirsle/blog/core/internal/models/users"
"github.com/kirsle/blog/core/internal/sessions"
"github.com/kirsle/blog/core/internal/types"
) )
// Config provides the settings and injectables for rendering templates.
type Config struct {
// Refined and raw variables for the templates.
Vars *Vars // Normal RenderTemplate's
// Wrap the template with the `.layout.gohtml`
WithLayout bool
// Inject your own functions for the Go templates.
Functions map[string]interface{}
Request *http.Request
}
// Vars is an interface to implement by the templates to pass their own custom // 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.) // variables in. It auto-loads global template variables (site name, etc.)
// when the template is rendered. // when the template is rendered.
@ -34,6 +25,7 @@ type Vars struct {
SetupNeeded bool SetupNeeded bool
Title string Title string
Path string Path string
TemplatePath string
LoggedIn bool LoggedIn bool
CurrentUser *users.User CurrentUser *users.User
CSRF string CSRF string
@ -53,18 +45,58 @@ type Vars struct {
Form forms.Form Form forms.Form
} }
// PartialTemplate handles rendering a Go template to a writer, without // loadDefaults combines template variables with default, globally available vars.
// doing anything extra to the vars or dealing with net/http. This is ideal for func (v *Vars) loadDefaults(r *http.Request) {
// rendering partials, such as comment partials. // Get the site settings.
// s, err := settings.Load()
// This will wrap the template in `.layout.gohtml` by default. To render just if err != nil {
// a bare template on its own, i.e. for partial templates, create a Vars struct s = settings.Defaults()
// with `Vars{NoIndex: true}`
func PartialTemplate(w io.Writer, path string, C Config) error {
if C.Request == nil {
panic("render.RenderPartialTemplate(): The *http.Request is nil!?")
} }
if s.Initialized == false && !strings.HasPrefix(r.URL.Path, "/initial-setup") {
v.SetupNeeded = true
}
v.Request = r
v.RequestTime = r.Context().Value(types.StartTimeKey).(time.Time)
v.Title = s.Site.Title
v.Path = r.URL.Path
user, err := auth.CurrentUser(r)
v.CurrentUser = user
v.LoggedIn = err == nil
}
// Template 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 Template(w io.Writer, r *http.Request, path string, v Vars) error {
// Inject globally available variables.
v.loadDefaults(r)
// If this is the HTTP response, handle session-related things.
if rw, ok := w.(http.ResponseWriter); ok {
rw.Header().Set("Content-Type", "text/html; encoding=UTF-8")
session := sessions.Get(r)
// Flashed messages.
if flashes := session.Flashes(); len(flashes) > 0 {
for _, flash := range flashes {
_ = flash
v.Flashes = append(v.Flashes, flash.(string))
}
session.Save(r, rw)
}
// CSRF token for forms.
v.CSRF = middleware.GenerateCSRFToken(rw, r, session)
}
v.RequestDuration = time.Now().Sub(v.RequestTime)
v.Editable = !strings.HasPrefix(path, "admin/")
// v interface{}, withLayout bool, functions map[string]interface{}) error { // v interface{}, withLayout bool, functions map[string]interface{}) error {
var ( var (
layout Filepath layout Filepath
@ -80,7 +112,7 @@ func PartialTemplate(w io.Writer, path string, C Config) error {
} }
// Get the layout template. // Get the layout template.
if C.WithLayout { if !v.NoLayout {
templateName = "layout" templateName = "layout"
layout, err = ResolvePath(".layout") layout, err = ResolvePath(".layout")
if err != nil { if err != nil {
@ -98,27 +130,12 @@ func PartialTemplate(w io.Writer, path string, C Config) error {
return err return err
} }
// Template functions. t := template.New(filepath.Absolute).Funcs(Funcs)
funcmap := template.FuncMap{
"StringsJoin": strings.Join,
"Now": time.Now,
"TemplateName": func() string {
return filepath.URI
},
}
if C.Functions != nil {
for name, fn := range C.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 // Parse the template files. The layout comes first because it's the wrapper
// and allows the filepath template to set the page title. // and allows the filepath template to set the page title.
var templates []string var templates []string
if C.WithLayout { if !v.NoLayout {
templates = append(templates, layout.Absolute) templates = append(templates, layout.Absolute)
} }
t, err = t.ParseFiles(append(templates, commentEntry.Absolute, filepath.Absolute)...) t, err = t.ParseFiles(append(templates, commentEntry.Absolute, filepath.Absolute)...)
@ -127,7 +144,7 @@ func PartialTemplate(w io.Writer, path string, C Config) error {
return err return err
} }
err = t.ExecuteTemplate(w, templateName, C.Vars) err = t.ExecuteTemplate(w, templateName, v)
if err != nil { if err != nil {
log.Error("Template parsing error: %s", err) log.Error("Template parsing error: %s", err)
return err return err
@ -135,25 +152,3 @@ func PartialTemplate(w io.Writer, path string, C Config) error {
return nil return nil
} }
// Template 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 Template(w http.ResponseWriter, path string, C Config) error {
if C.Request == nil {
panic("render.RenderTemplate(): The *http.Request is nil!?")
}
w.Header().Set("Content-Type", "text/html; encoding=UTF-8")
PartialTemplate(w, path, Config{
Request: C.Request,
Vars: C.Vars,
WithLayout: true,
Functions: C.Functions,
})
return nil
}

View File

@ -41,7 +41,7 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
// Is it a template file? // Is it a template file?
if strings.HasSuffix(filepath.URI, ".gohtml") { if strings.HasSuffix(filepath.URI, ".gohtml") {
b.RenderTemplate(w, r, filepath.URI, render.Vars{}) b.RenderTemplate(w, r, filepath.URI, NewVars())
return return
} }

View File

@ -1,49 +1,12 @@
package core package core
import ( import (
"html/template"
"io" "io"
"net/http" "net/http"
"strings"
"time"
"github.com/kirsle/blog/core/internal/forms"
"github.com/kirsle/blog/core/internal/middleware"
"github.com/kirsle/blog/core/internal/middleware/auth"
"github.com/kirsle/blog/core/internal/models/settings"
"github.com/kirsle/blog/core/internal/models/users"
"github.com/kirsle/blog/core/internal/render" "github.com/kirsle/blog/core/internal/render"
"github.com/kirsle/blog/core/internal/sessions"
"github.com/kirsle/blog/core/internal/types"
) )
// 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. // NewVars initializes a Vars struct with the custom Data map initialized.
// You may pass in an initial value for this map if you want. // You may pass in an initial value for this map if you want.
func NewVars(data ...map[interface{}]interface{}) render.Vars { func NewVars(data ...map[interface{}]interface{}) render.Vars {
@ -58,102 +21,20 @@ func NewVars(data ...map[interface{}]interface{}) render.Vars {
} }
} }
// LoadDefaults combines template variables with default, globally available vars.
func (b *Blog) LoadDefaults(v render.Vars, r *http.Request) render.Vars {
// 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(types.StartTimeKey).(time.Time)
v.Title = s.Site.Title
v.Path = r.URL.Path
user, err := auth.CurrentUser(r)
v.CurrentUser = user
v.LoggedIn = err == nil
return v
}
// 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, r *http.Request, path string, v render.Vars, withLayout bool, functions map[string]interface{}) error {
v = b.LoadDefaults(v, r)
return render.PartialTemplate(w, path, render.Config{
Request: r,
Vars: &v,
WithLayout: withLayout,
Functions: b.TemplateFuncs(nil, nil, functions),
})
}
// RenderTemplate responds with an HTML template. // RenderTemplate responds with an HTML template.
// //
// The vars will be massaged a bit to load the global defaults (such as the // 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 // 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 // new CSRF token, and other such things.
// without all that nonsense, use RenderPartialTemplate. //
func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path string, vars render.Vars) error { // For server-rendered templates given directly to the user (i.e., in controllers),
// give it the http.ResponseWriter; for partial templates you can give it a
// bytes.Buffer to write to instead. The subtle difference is whether or not the
// template will have access to the request's session.
func (b *Blog) RenderTemplate(w io.Writer, r *http.Request, path string, vars render.Vars) error {
if r == nil { if r == nil {
panic("core.RenderTemplate(): the *http.Request is nil!?") panic("core.RenderTemplate(): the *http.Request is nil!?")
} }
// Inject globally available variables. return render.Template(w, r, path, vars)
vars = b.LoadDefaults(vars, r)
// Add any flashed messages from the endpoint controllers.
session := sessions.Get(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 = middleware.GenerateCSRFToken(w, r, session)
vars.Editable = !strings.HasPrefix(path, "admin/")
return render.Template(w, path, render.Config{
Request: r,
Vars: &vars,
Functions: b.TemplateFuncs(w, r, nil),
})
}
// TemplateFuncs returns the common template function map.
func (b *Blog) TemplateFuncs(w http.ResponseWriter, r *http.Request, inject map[string]interface{}) map[string]interface{} {
fn := map[string]interface{}{
"RenderIndex": b.RenderIndex,
"RenderPost": b.RenderPost,
"RenderTags": b.RenderTags,
"RenderComments": func(subject string, ids ...string) template.HTML {
if w == nil || r == nil {
return template.HTML("[RenderComments Error: need both http.ResponseWriter and http.Request]")
}
session := sessions.Get(r)
csrf := middleware.GenerateCSRFToken(w, r, session)
return b.RenderComments(session, csrf, r.URL.Path, subject, ids...)
},
}
if inject != nil {
for k, v := range inject {
fn[k] = v
}
}
return fn
} }

View File

@ -81,7 +81,7 @@
{{ if and .CurrentUser.Admin .Editable }} {{ if and .CurrentUser.Admin .Editable }}
<p class="mt-4"> <p class="mt-4">
<strong>Admin:</strong> [<a href="/admin/editor?file={{ or .Data.MarkdownFile TemplateName }}">edit this page</a>] <strong>Admin:</strong> [<a href="/admin/editor?file={{ or .Data.MarkdownFile .Path }}">edit this page</a>]
</p> </p>
{{ end }} {{ end }}
</div> </div>

View File

@ -18,7 +18,7 @@
<h2 id="comments" class="mt-4">Comments</h2> <h2 id="comments" class="mt-4">Comments</h2>
{{ $idStr := printf "%d" $p.ID}} {{ $idStr := printf "%d" $p.ID}}
{{ RenderComments $p.Title "post" $idStr }} {{ RenderComments .Request $p.Title "post" $idStr }}
{{ else }} {{ else }}
<hr> <hr>
<em>Comments are disabled on this post.</em> <em>Comments are disabled on this post.</em>