Compile text/template / html/template to regular go code.
still a wip!
You will need both library and binary.
go get github.com/mh-cbon/template-compiler
cd $GOPATH/src/github.com/mh-cbon/template-compiler
glide install
go installtemplate-compiler - 0.0.0
-help | -h Show this help.
-version Show program version.
-keep Keep bootstrap program compiler.
-print Print bootstrap program compiler.
-var The variable name of the configuration in your program
default: compiledTemplates
-wdir The working directory where the bootstrap program is written
default: $GOPATH/src/template-compilerxx/
Examples
template-compiler -h
template-compiler -version
template-compiler -keep -var theVarName
template-compiler -keep -var theVarName -wdir /tmpLet s take this example package
package mypackage
import(
"net/http"
"html/template"
)
var tplFuncs = map[string]interface{}{
"up": strings.ToUpper,
}
type TplData struct {
Email string
Name string
}
func handler(w http.ResponseWriter, r *http.Request) {
t := template.New("").Funcs(tplFuncs).ParseFiles("tmpl/welcome.html")
t.Execute(w, TplData{})
}With this template
{{.Email}} {{.Name}}
To generate compiled version of your template, change it to
package mypackage
import (
"net/http"
"github.com/mh-cbon/template-compiler/compiled"
"github.com/mh-cbon/template-compiler/std/text/template"
)
//go:generate template-compiler
var compiledTemplates = compiled.New(
"gen.go",
[]compiled.TemplateConfiguration{
compiled.TemplateConfiguration{
HTML: true,
TemplatesPath: "tmpl/*.tpl",
TemplatesData: map[string]interface{}{
"*": TplData{},
},
FuncsMap: []string{
"somewhere/mypackage:tplFuncs",
},
},
compiled.TemplateConfiguration{
TemplateName: "notafile",
TemplateContent: `hello!{{define "embed"}}{{.Email}} {{.Name}}{{end}}`,
TemplatesData: map[string]interface{}{
"*": nil,
"embed": TplData{},
},
},
},
)
var tplFuncs = map[string]interface{}{
"up": strings.ToUpper,
}
type TplData struct {
Email string
Name string
}
func handler(w http.ResponseWriter, r *http.Request) {
compiledTemplates.MustGet("welcome.tpl").Execute(w, TplData{})
compiledTemplates.MustGet("notafile").Execute(w, nil)
compiledTemplates.MustGet("embed").Execute(w, TplData{})
}Then run,
go generateIt will produce a file gen.go containing the code to declare and run the compiled templates,
package main
//golint:ignore
import (
"io"
"github.com/mh-cbon/template-compiler/compiled"
"github.com/mh-cbon/template-compiler/std/text/template/parse"
"path/to/mypackage"
)
func init () {
compiledTemplates = compiled.NewRegistry()
compiledTemplates.Add("welcome.tpl", fnaTplaTpl0)
}
// only demonstration purpose, not the actual real generated code for the example template.
func fnaTplaTpl0(t parse.Templater, w io.Writer, indata interface {
}) error {
var bw bytes.Buffer
var data aliasdata.MyTemplateData
if d, ok := indata.(aliasdata.MyTemplateData); ok {
data = d
}
if _, werr := w.Write(builtin5); werr != nil {
return werr
}
var var2 []string = data.MethodItems()
var var1 int = len(var2)
var var0 bool = 0 != var1
if var0 {
if _, werr := w.Write(builtin6); werr != nil {
return werr
}
var var3 []string = data.MethodItems()
for _, iterable := range var3 {
if _, werr := w.Write(builtin7); werr != nil {
return werr
}
bw.WriteString(iterable)
template.HTMLEscape(w, bw.Bytes())
bw.Reset()
if _, werr := w.Write(builtin8); werr != nil {
return werr
}
}
if _, werr := w.Write(builtin9); werr != nil {
return werr
}
} else {
if _, werr := w.Write(builtin10); werr != nil {
return werr
}
}
if _, werr := w.Write(builtin2); werr != nil {
return werr
}
return nil
}
var builtin0 = []byte(" ")
// more like thisGiven the templates compiled as HTML available here
$ go test -bench=. -benchmem
BenchmarkRenderWithCompiledTemplateA-4 20000000 78.9 ns/op 48 B/op 1 allocs/op
BenchmarkRenderWithJitTemplateA-4 3000000 668 ns/op 96 B/op 2 allocs/op
BenchmarkRenderWithCompiledTemplateB-4 20000000 82.4 ns/op 48 B/op 1 allocs/op
BenchmarkRenderWithJitTemplateB-4 3000000 603 ns/op 96 B/op 2 allocs/op
BenchmarkRenderWithCompiledTemplateC-4 500000 2530 ns/op 192 B/op 6 allocs/op
BenchmarkRenderWithJitTemplateC-4 50000 38245 ns/op 3641 B/op 82 allocs/op
BenchmarkRenderWithCompiledTemplateD-4 20000000 114 ns/op 48 B/op 1 allocs/op
BenchmarkRenderWithJitTemplateD-4 3000000 809 ns/op 144 B/op 3 allocs/op
// the next 2 benchmarks are particularly encouraging
// as they involve 2k html string escaping
BenchmarkRenderWithCompiledTemplateE-4 10000 103929 ns/op 160 B/op 2 allocs/op
BenchmarkRenderWithJitTemplateE-4 300 6207912 ns/op 656598 B/op 18012 allocs/op
BenchmarkRenderWithCompiledTemplateF-4 10000 111047 ns/op 160 B/op 2 allocs/op
BenchmarkRenderWithJitTemplateF-4 200 5836766 ns/op 657000 B/op 18024 allocs/op
Depending on the kind of template expect 5 to 30 times faster and much much less allocations.
This paragraph will describe and explain the various steps from the go:generate command,
to the write of the compiled go code.
- When
go:generateis invoked, the go tool will parse and invoke your calls totemplate-compiler.template-compileris invoked in the directory containing the file with thego:generatecomment,go generatealso declares an environment variableGOFILE. With those hintstemplate-compilercan locate and consume the variable declared with-varparameter. We are here template-compilerwill generate a bootstrap program. We are here- The generation of the bootstrap program is about parsing, browsing, and re exporting
an updated version of your configuration variable.
It specifically looks for each
compiled.TemplateConfiguration{}:
- If the configuration is set to generate html content with the key
HTML:true, it ensure that stdfunc are appropriately declared into the configuration. - It read and evaluates the data field
Data: your.struct{}, generates aDataConfiguration{}of it, and adds it to the template configuration. - It checks for
FuncsMapkey, and export those variable targets (with the help of this package) toFuncsExportandPublicIdentskeys. We are here
template-compilerwrites and compiles a go program into$GOPATH/src/template-compilerxxx. This program is made to compile the templates with the updated configuration. We are herebootstrap-programis now invoked. We are herebootstrap-programbrowses the configuration value, for each template path, it compiles it astext/templateorhtml/template. This steps creates the standard template AST Tree. Each template tree is then transformed and simplified with the help of this package. We are heretemplate-tree-simplifiertakes in input the template tree and apply transformations:
- It unshadows all variables declaration within the template.
- It renames all template variables to prefix them with
tpl - It simplifies structure such as
{{"son" | split "wat"}}to{{$var0 := split "wat" "son"}}{{$var0}} - It produces a small type checker structure which registers variable and their type for each scope of the template. We are here
bootstrap-programbrowses each simplified template tree, generates a go function corresponding to it. We are herebootstrap-programgenerates aninitfunction to register to your configuration variable the new functions as their template name. We are herebootstrap-programwrites the fully generated program.
template-compiler needs to be able to evaluate the funcmap consumed by the templates.
In that matter template-compiler can take in input a path to a variable declaring this functions.
pkgPath:variableName where pkgPath is the go pakage path such as text/template,
the variable name is the name of the variable declaring the funcmap such as builtins.
See this.
It can read map[string]interface{} or template.FuncMap.
It can extract exported or unexported variables.
Functions declared into the funcmap can be exported, unexported, or inlined.
Note that unexported functions needs some runtime type checking.
examples
If you like sprig, you d be able to consume those functions with the path,
github.com/Masterminds/sprig:genericMap
If you prefer gtf, you d be able to consume those functions with the path,
github.com/leekchan/gtf:GtfFuncMap
beware
it can t evaluate a function call! It must be a variable declaration into the top level context such as
package yy
var funcs = map[string]interface{}{
"funcname": func(){},
"funcname2": pkg.Func,
}The data consumed by your template must follow few rules:
- It must be an exported type.
- It must not be declared into a
mainpackage.
As the resulting compilation is pure go code, the type system must be respected,
thus unexported types may not work.
Unfortunately this package contains some ugly copy pastes :x :x :x
It duplicates both text/template and html/template.
It would be great to backport those changes into core go code to get ride of those duplications.
- Added a new
text/template.Compiledtype. Much like atext/templateor anhtml/template,Compiledhas a*parse.Tree. This tree is a bultin tree to hold only one node to execute the compiled function. Doing so allow to mix compiled and non-compiled templates. see here - Added a new method
text/template.GetFuncs()to get the funcs related to the template. This is usefull to the compiled template functions to get access to those unexported functions. see here - Added
text/template.Compiled()to attach a compiled template to a regulartext/templateinstance. see here - Added a new tree node
text/template/parse.CompiledNode, which knows the function to execute for a compiled template. see here - Added a new interface
text/template/parse.Templater, to use in the compiled function to receive the current template executed. This instance can be one oftext/template.Template,html/template.Templateortext/template.Compiled. see here - Added a new type
CompiledTemplateFuncfor the signature of a compiled template function. see here - Added a new funcmap variable
html/template.publicFuncMapto map all html template idents to a function. It also delcares all escapers to a public function to improve performance of compiled templates. see here - Added support of CompiledNode to the state walker see here
Here are some optimizations/todos to implement later:
When compiling templates, funcs like_html_template_htmlescaperwill translate totemplate.HTMLEscaper. It worth to note that many cases are probablytemplate.HTMLEscaper(string), buttemplate.HTMLEscaperis doing some extra job to type check thisstringvalue. An optimization is to detect those callstemplate.HTMLEscaper(string)and transformedform them totemplate.HTMLEscapeString(string)- Same as previous for most escaper functions of
html/template - Detect template calls such
, oreq(bool, bool)neq(int, int)and transform them to an appropriate go binary testbool == bool, ect. Detect templates calls suchlen(some)and transforms it to the builtinlenfunction.- Detect prints of
structor*struct, check if they implementsStringer, or something likeByter, and make use of that to get ride of somefmt.Sprintfcalls. - review the install procedure, i suspect it is not yet correct. Make use of glide.
- consolidate additions to std
text/template/html/templatepackages. - version releases.
- implement cache for functions export.
- add template.Options support (some stuff there)
- add channel support (is it really used ? :x)
- add a method to easily switch from compiled function to original templates without modifying the configuration, imports ect.