Skip to content

Instantly share code, notes, and snippets.

@jonathaningram
Created May 4, 2015 23:39
Show Gist options
  • Save jonathaningram/9e7a6e18863e04eea398 to your computer and use it in GitHub Desktop.
Save jonathaningram/9e7a6e18863e04eea398 to your computer and use it in GitHub Desktop.
One way in Go to build a template engine/renderer.
{{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}}
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
}
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
}
}
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))
}
{{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}}
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}
}
<?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>
@elithrar
Copy link

elithrar commented May 6, 2015

  • 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