Created
May 4, 2015 23:39
-
-
Save jonathaningram/9e7a6e18863e04eea398 to your computer and use it in GitHub Desktop.
One way in Go to build a template engine/renderer.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{{template "layout" .}} | |
{{define `htmlAttrs`}} ng-app="myApp"{{end}} | |
{{define "headTitle"}}Contact us<!-- can't include "parent" like in Jinja/Twig so these "blocks" are not 100% flexible, but still usable -->{{end}} | |
{{define "headExtraScripts"}} | |
<!-- assuming this is not in an asset pipeline where it normally would be --> | |
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script> | |
{{end}} | |
{{define "bodyClass"}}contact{{end}} | |
{{define "bodyAttrs"}}ng-cloak ng-controller="ContactCtrl"{{end}} | |
{{define "content"}} | |
<main> | |
<h1>Contact us</h1> | |
</main> | |
{{end}} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package template | |
import ( | |
"fmt" | |
htmltemplate "html/template" | |
"log" | |
"path/filepath" | |
"strings" | |
texttemplate "text/template" | |
) | |
type Engine interface { | |
LoadHTMLTemplate(templatePath string, funcs []htmltemplate.FuncMap) (*htmltemplate.Template, error) | |
LoadTextTemplate(templatePath string, funcs []texttemplate.FuncMap) (*texttemplate.Template, error) | |
} | |
// funcs that the Engine uses. could be made more flexible | |
var templateFuncs = []htmltemplate.FuncMap{ | |
htmltemplate.FuncMap{ | |
"add": add, | |
"safeHTML": safeHTML, | |
"safeHTMLAttr": safeHTMLAttr, | |
"safeJS": safeJS, | |
"safeJSStr": safeJSStr, | |
"safeURL": safeURL, | |
"toLower": strings.ToLower, | |
"ng": ngFilter, | |
"requestScheme": requestScheme, | |
"nl2br": nl2br, | |
}, | |
} | |
func NewEngine( | |
sharedTemplates []string, | |
autoReload bool, | |
) Engine { | |
return &engine{ | |
templateFuncs: templateFuncs, | |
sharedTemplates: sharedTemplates, | |
autoReload: autoReload, | |
} | |
} | |
type engine struct { | |
htmlTemplates map[string]*htmltemplate.Template | |
textTemplates map[string]*texttemplate.Template | |
templateFuncs []htmltemplate.FuncMap | |
sharedTemplates []string | |
autoReload bool | |
} | |
func (e *engine) LoadHTMLTemplate(templatePath string, funcs []htmltemplate.FuncMap) (*htmltemplate.Template, error) { | |
if e.htmlTemplates == nil { | |
e.htmlTemplates = make(map[string]*htmltemplate.Template) | |
} | |
fullTemplatePath := templatePath | |
t, ok := e.htmlTemplates[fullTemplatePath] | |
if e.autoReload || !ok { | |
templateName := filepath.Base(fullTemplatePath) | |
t = htmltemplate.New(templateName) | |
for _, f := range e.templateFuncs { | |
t.Funcs(f) | |
} | |
for _, f := range funcs { | |
t.Funcs(f) | |
} | |
templates := []string{ | |
fullTemplatePath, | |
} | |
templates = append(templates, e.sharedTemplates...) | |
if _, err := t.ParseFiles(templates...); err != nil { | |
return nil, err | |
} | |
e.htmlTemplates[fullTemplatePath] = t | |
log.Printf("compiled template %q", fullTemplatePath) | |
} | |
return t, nil | |
} | |
func (e *engine) LoadTextTemplate(templatePath string, funcs []texttemplate.FuncMap) (*texttemplate.Template, error) { | |
if e.textTemplates == nil { | |
e.textTemplates = make(map[string]*texttemplate.Template) | |
} | |
fullTemplatePath := templatePath | |
t, ok := e.textTemplates[fullTemplatePath] | |
if e.autoReload || !ok { | |
templateName := filepath.Base(fullTemplatePath) | |
t = texttemplate.New(templateName) | |
for _, f := range funcs { | |
t.Funcs(f) | |
} | |
if _, err := t.ParseFiles(fullTemplatePath); err != nil { | |
return nil, err | |
} | |
e.textTemplates[fullTemplatePath] = t | |
log.Printf("compiled text template %q", fullTemplatePath) | |
} | |
return t, nil | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
func main() { | |
templateEngine := template.NewEngine( | |
[]string{ | |
"app/templates/layout.html", | |
}, | |
appConfig.Env == "dev", | |
) | |
// ... | |
router.NewRoute().Methods("HEAD", "GET").PathPrefix("/sitemap.xml").Handler(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { | |
sitemapHandler( | |
res, | |
req, | |
template.NewRenderer( | |
res, | |
req, | |
templateEngine, | |
appConfig.TemplatesDir, | |
templateGlobals(req, appConfig), | |
), | |
) | |
})) | |
router.NewRoute().Methods("HEAD", "GET").PathPrefix("/contact").Handler(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { | |
contactHandler( | |
res, | |
req, | |
template.NewRenderer( | |
res, | |
req, | |
templateEngine, | |
appConfig.TemplatesDir, | |
templateGlobals(req, appConfig), | |
), | |
) | |
})) | |
} | |
func templateGlobals(req *http.Request, appConfig AppConfig) map[string]interface{} { | |
data := map[string]interface{}{ | |
// maybe the renderer should pass request instead of doing it via globals, not sure | |
"request": req, | |
"env": appConfig.Env, | |
"debug": appConfig.Debug, | |
"now": time.Now(), | |
} | |
return data | |
} | |
func sitemapHandler( | |
res http.ResponseWriter, | |
req *http.Request, | |
render template.Renderer, | |
) { | |
entries := []map[string]interface{}{ | |
map[string]interface{}{ | |
"path": "/contact", | |
}, | |
} | |
res.Header().Set("Content-Type", "text/xml; charset=utf-8") | |
if err := render.Text("sitemap.xml", map[string]interface{}{ | |
"entries": entries, | |
}); err != nil { | |
res.WriteHeader(http.StatusInternalServerError) | |
return | |
} | |
} | |
func contactHandler( | |
res http.ResponseWriter, | |
req *http.Request, | |
render template.Renderer, | |
) { | |
if err := render.HTML("contact.html", map[string]interface{}{}); err != nil { | |
// ... | |
return | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package template | |
import ( | |
htmltemplate "html/template" | |
"net/http" | |
"strings" | |
) | |
// example funcs that the Engine uses. could be made more flexible | |
func add(a, b int) int { | |
return a + b | |
} | |
func safeHTML(arg interface{}) htmltemplate.HTML { | |
return htmltemplate.HTML(arg.(string)) | |
} | |
func safeHTMLAttr(arg interface{}) htmltemplate.HTMLAttr { | |
return htmltemplate.HTMLAttr(arg.(string)) | |
} | |
func safeJS(arg interface{}) htmltemplate.JS { | |
return htmltemplate.JS(arg.(string)) | |
} | |
func safeJSStr(arg interface{}) htmltemplate.JSStr { | |
return htmltemplate.JSStr(arg.(string)) | |
} | |
func safeURL(arg interface{}) htmltemplate.URL { | |
return htmltemplate.URL(arg.(string)) | |
} | |
func requestScheme(req *http.Request) string { | |
if req.TLS != nil { | |
return "https" | |
} | |
return "http" | |
} | |
func ngFilter(arg string) string { | |
return "{{" + arg + "}}" | |
} | |
func nl2br(arg string) htmltemplate.HTML { | |
return htmltemplate.HTML(strings.Replace(arg, "\n", "<br>", -1)) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{{define "layout"}}<!doctype html> | |
<html lang="en" {{template `htmlAttrs` .}}> | |
<head> | |
<meta charset="utf-8"> | |
<title>{{template "headTitle" .}}</title> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
{{template "metaRobots" .}} | |
{{template "headExtraScripts" .}} | |
</head> | |
<body class="{{template `bodyClass` .}}" {{template `bodyAttrs` .}}> | |
<header class="container"></header> | |
{{template "content" .}} | |
<footer></footer> | |
</body> | |
</html> | |
{{end}} | |
{{define `htmlAttrs`}}{{end}} | |
{{define "metaRobots"}}{{end}} | |
{{define "headExtraScripts"}}{{end}} | |
{{define "bodyClass"}}{{end}} | |
{{define "bodyAttrs"}}{{end}} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package template | |
import ( | |
"bytes" | |
"fmt" | |
htmltemplate "html/template" | |
"net/http" | |
"strings" | |
texttemplate "text/template" | |
"github.com/oxtoacart/bpool" | |
) | |
var bufpool *bpool.BufferPool | |
func init() { | |
bufpool = bpool.NewBufferPool(64) | |
} | |
type Renderer interface { | |
HTML(templatePath string, data map[string]interface{}) error | |
Text(templatePath string, data map[string]interface{}) error | |
} | |
func NewRenderer( | |
res http.ResponseWriter, | |
req *http.Request, | |
te Engine, | |
appTemplatesDir string, | |
globals map[string]interface{}, | |
) Renderer { | |
return &renderer{ | |
response: res, | |
request: req, | |
engine: te, | |
appTemplatesDir: appTemplatesDir, | |
globals: globals, | |
} | |
} | |
type renderer struct { | |
response http.ResponseWriter | |
request *http.Request | |
engine Engine | |
appTemplatesDir string | |
globals map[string]interface{} | |
} | |
func (r *renderer) HTML(templatePath string, data map[string]interface{}) (err error) { | |
defer func() { | |
r := recover() | |
if r == nil { | |
return | |
} | |
switch t := r.(type) { | |
case error: | |
err = t | |
default: | |
panic(r) | |
} | |
}() | |
templatePath = fmt.Sprintf("%s/%s", r.appTemplatesDir, templatePath) | |
t, err := r.engine.LoadHTMLTemplate(templatePath, r.funcs()) | |
if err != nil { | |
return err | |
} | |
for key, value := range r.globals { | |
if _, ok := data[key]; !ok { | |
data[key] = value | |
} | |
} | |
b := bufpool.Get() | |
defer bufpool.Put(b) | |
if err := t.Execute(b, data); err != nil { | |
return err | |
} | |
r.response.Header().Set("Content-Type", "text/html; charset=utf-8") | |
b.WriteTo(r.response) | |
return nil | |
} | |
func (r *renderer) Text(templatePath string, data map[string]interface{}) (err error) { | |
defer func() { | |
r := recover() | |
if r == nil { | |
return | |
} | |
switch t := r.(type) { | |
case error: | |
err = t | |
default: | |
panic(r) | |
} | |
}() | |
templatePath = fmt.Sprintf("%s/%s", r.appTemplatesDir, templatePath) | |
t, err := r.engine.LoadTextTemplate(templatePath, r.textFuncs()) | |
if err != nil { | |
panic(err) | |
} | |
for key, value := range r.globals { | |
if _, ok := data[key]; !ok { | |
data[key] = value | |
} | |
} | |
b := bufpool.Get() | |
defer bufpool.Put(b) | |
if err := t.Execute(b, data); err != nil { | |
return err | |
} | |
b.WriteTo(r.response) | |
return nil | |
} | |
func (r *renderer) funcs() []htmltemplate.FuncMap { | |
return []htmltemplate.FuncMap{ | |
htmltemplate.FuncMap{ | |
"trimSpace": func(arg string) string { | |
return strings.TrimSpace(arg) | |
}, | |
"truncate": func(length int, separator, arg string) string { | |
if length >= len(arg) { | |
return arg | |
} | |
b := &bytes.Buffer{} | |
b.Write([]byte(arg)) | |
b.Truncate(length) | |
return b.String() + separator | |
}, | |
}, | |
} | |
} | |
func (r *renderer) textFuncs() []texttemplate.FuncMap { | |
funcMap := texttemplate.FuncMap{} | |
for _, funcs := range r.funcs() { | |
for name, f := range funcs { | |
funcMap[name] = f | |
} | |
} | |
return []texttemplate.FuncMap{funcMap} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8"?> | |
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> | |
{{$request := .request}} | |
{{range $key, $entry := .entries}} | |
<url> | |
<loc>http://{{$request.Host}}{{$entry.path}}</loc> | |
</url> | |
{{end}} | |
</urlset> |
- I can't seem to find the source, but my understanding was that the
defer
call was the (unavoidable) and slow part. I could be wrong here. - What's the overhead of passing (say) 20 funcs vs. 5? I'd be curious to know if there's a noticeable response-time bottleneck. Template processing can be crazy slow so you might be right in the value of per-handler funcs (i.e. don't call what you don't need). If you can divorce that from the template loading (which you never want to do in a handler - hits disk!) it'd be ideal, but not sure there's a clear way to do that without a lot of copying (so that the funcmap doesn't mutate the original (clean)
template.Template
map) - I think it might be one of mine - I used that exact library. Works well!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks for the feedback, will look into it.
Text
andHTML
functions is so that I can return an error from the render call. The specific reason being that I want to be able to handle that error case in the handler. True a higher level or middleware could catch the panic and render a generic 500 page (plus a stack in dev), but I originally just felt like the flexibility of knowing that the template render failed was good. May be time to review though. BTW, what's the performance hit? Is it the actual call torecover
that must be done every time? From http://blog.golang.org/defer-panic-and-recoverDuring normal execution, a call to recover will return nil and have no other effect.
Although I admit that that quote may be out of context performance-wise.