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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
defer
call was the (unavoidable) and slow part. I could be wrong here.template.Template
map)