The goal here is to see how:
- to load a first batch of templates from a
toolbox/
directory, - and how to use them while iterating on a second set of templates.
As a syntactical refinement, we’ll see how to call the first batch of templates from
the second one as functions. Meaning, if you have a template toolbox/tool.tmpl
,
we’ll see how to use it from the second set of templates as:
{{ tool arg0 arg1 }}
The first batch of templates provides generic functionality to be used throughout
the second batch. A typical use case would be a static site generator, where you
have some generic templating functions in toolbox/*.tmpl
, that you can use on all
your pages/*.tmpl
. I’ll keep referring to this use-case later on to help clear
things out.
Getting started
Let’s start by simply calling a template with some arguments, wrapped in a map[string]any
.
The pros of using such a generic map is that the data can be arbitrarily structured (random
JSON file for example): the Go code doesn’t have to know how to precisely unwrap it
into precisely-typed data structures. Of course, the cost of this extra-flexibility is a
loss of static checks.
In the present article, it’ll make the code slightly more succinct, so I’ll stick with it.
package main
import (
"log"
"os"
"text/template"
)
func main() {
err := template.Must(
template.ParseFiles("pages/00.tmpl"),
).Execute(os.Stdout, map[string]any{
"user" : map[string]any{
"name" : "earthling",
},
})
if err != nil {
log.Fatal(err)
}
}
{{/* pages/00.tmpl */}}
hello, {{ .user.name }}!
# stdout
hello, earthling!
This should be fairly self-explanatory; the details are best explained in the relevant documentation:
Adding and using some tools.
We now need to parse two sets of templates; mind the use of
template.ParseGlob()
to load the
set of tools:
package main
import (
"log"
"os"
"text/template"
)
func main() {
err := template.Must(
template.Must(
template.ParseFiles("pages/01.tmpl"),
).ParseGlob("toolbox/*.tmpl"),
).Execute(os.Stdout, map[string]any{
"user" : map[string]any{
"name" : "earthling",
},
})
if err != nil {
log.Fatal(err)
}
}
{{/* pages/01.tmpl */}}
{{- template "greet.tmpl" }}, {{ .user.name }}!
{{/* toolbox/greet.tmpl */}}
hello
{{/* toolbox/show.tmpl */}}
{{- range . -}}
{{- . -}}
{{- end -}}
# stdout
hello, earthling!
Note: toolbox/show.tmpl
is loaded but unused.
There’s one important subtlety in the previous code: a template.Template
is a wrapper that can actually refer to multiple templates. If you try
to template.Execute()
it, the execution entry
point will be the first loaded template, in our case, pages/01.tmpl
.
An easy mistake would have been to load first the toolbox/*
, then a
page, and to be left slightly befuddled, as one of the toolbox/
template
would have been executed instead.
package main
import (
"log"
"os"
"text/template"
)
func main() {
// XXX: This is usually NOT what you want
err := template.Must(
template.Must(
template.ParseGlob("toolbox/*.tmpl"),
).ParseFiles("pages/01.tmpl"),
).Execute(os.Stdout, map[string]any{
"user" : map[string]any{
"name" : "earthling",
},
})
if err != nil {
log.Fatal(err)
}
}
{{/* pages/01.tmpl */}}
{{- template "greet.tmpl" }}, {{ .user.name }}!
{{/* toolbox/greet.tmpl */}}
hello
{{/* toolbox/show.tmpl */}}
{{- range . -}}
{{- . -}}
{{- end -}}
# stdout
hello
One step further
Of course, considering the plan for this article, we want a workaround, as we’ll
want to first load the toolbox, then the “pages”: while
template.Execute()
by default
calls the first loaded template, there’s a
template.ExecuteTemplate()
executing
a specific template from the set.
package main
import (
"log"
"os"
"text/template"
)
func main() {
err := template.Must(
template.Must(
template.ParseGlob("toolbox/*.tmpl"),
).ParseFiles("pages/01.tmpl"),
).ExecuteTemplate(os.Stdout, "01.tmpl", map[string]any{
"user" : map[string]any{
"name" : "earthling",
},
})
if err != nil {
log.Fatal(err)
}
}
{{/* pages/01.tmpl */}}
{{- template "greet.tmpl" }}, {{ .user.name }}!
{{/* toolbox/greet.tmpl */}}
hello
{{/* toolbox/show.tmpl */}}
{{- range . -}}
{{- . -}}
{{- end -}}
# stdout
hello, earthling!
Iterating on the pages
Let’s add an init()
function to load the toolbox, and let’s iterate on some
pages. Note that in real life scenario, this list would have been given e.g. via
a filepath.Walk()
:
package main
import (
"log"
"os"
"path/filepath"
"text/template"
)
var tmpls *template.Template
func init() {
tmpls = template.Must(
template.ParseGlob("toolbox/*.tmpl"),
)
}
func main() {
for _, x := range []string{"00.tmpl", "01.tmpl"} {
err := template.Must(
template.Must(tmpls.Clone()).ParseFiles(filepath.Join("pages", x)),
).ExecuteTemplate(os.Stdout, x, map[string]any{
"user" : map[string]any{
"name" : "from "+x,
},
})
if err != nil {
log.Fatal(err)
}
}
}
{{/* pages/00.tmpl */}}
hello, {{ .user.name }}!
{{/* pages/01.tmpl */}}
{{- template "greet.tmpl" }}, {{ .user.name }}!
{{/* toolbox/greet.tmpl */}}
hello
{{/* toolbox/show.tmpl */}}
{{- range . -}}
{{- . -}}
{{- end -}}
# stdout
hello, from 00.tmpl!
hello, from 01.tmpl!
Calling Go code from a template
Here’s a small intermediate step showing how to register functions
to a template.Template
so as to call them from the templates:
package main
import (
"log"
"os"
"path/filepath"
"text/template"
)
var tmpls *template.Template
func init() {
tmpls = template.Must(
template.ParseGlob("toolbox/*.tmpl"),
).Funcs(template.FuncMap{
"doGreet" : func() string {
return "hi"
},
})
}
func main() {
for _, x := range []string{"00.tmpl", "01.tmpl", "05.tmpl"} {
err := template.Must(
template.Must(tmpls.Clone()).ParseFiles(filepath.Join("pages", x)),
).ExecuteTemplate(os.Stdout, x, map[string]any{
"user" : map[string]any{
"name" : "from "+x,
},
})
if err != nil {
log.Fatal(err)
}
}
}
{{/* pages/00.tmpl */}}
hello, {{ .user.name }}!
{{/* pages/01.tmpl */}}
{{- template "greet.tmpl" }}, {{ .user.name }}!
{{/* pages/05.tmpl */}}
{{- doGreet }}, {{ .user.name }}!
{{/* toolbox/greet.tmpl */}}
hello
{{/* toolbox/show.tmpl */}}
{{- range . -}}
{{- . -}}
{{- end -}}
# stdout
hello, from 00.tmpl!
hello, from 01.tmpl!
hi, from 05.tmpl!
There are of course some restrictions regarding what kind of functions you can register, especially concerning their return value. The documentation is rather clear on all those aspects.
Toolbox templates called as functions
Here’s the syntactic sugar: the idea is simply to iterate on all the templates
you’ve just loaded from the toolbox/
, and to register one function for them.
We need to trim away the .tmpl
prefix, and more generally, to make sure
we can build a proper function name from the (toolbox) template path. Of course,
choosing appropriate template names in the first place is the best way to Go.
Mind the use of a strings.Builder
to
catpure the output of a template execution to a string.
package main
import (
"log"
"os"
"path/filepath"
"strings"
"text/template"
)
var tmpls *template.Template
func init() {
tmpls = template.Must(
template.ParseGlob("toolbox/*.tmpl"),
).Funcs(template.FuncMap{
"doGreet" : func() string {
return "hi"
},
})
for _, x := range tmpls.Templates() {
n := strings.TrimSuffix(x.Name(), ".tmpl")
// beware of the race...
m := x.Name()
tmpls.Funcs(template.FuncMap{
n : func(ys ...any) (string, error) {
var s strings.Builder
err := tmpls.ExecuteTemplate(&s, m, ys)
return s.String(), err
},
})
}
}
func main() {
for _, x := range []string{"00.tmpl", "01.tmpl", "05.tmpl", "06.tmpl"} {
err := template.Must(
template.Must(tmpls.Clone()).ParseFiles(filepath.Join("pages", x)),
).ExecuteTemplate(os.Stdout, x, map[string]any{
"user" : map[string]any{
"name" : "from "+x,
},
})
if err != nil {
log.Fatal(err)
}
}
}
{{/* pages/00.tmpl */}}
hello, {{ .user.name }}!
{{/* pages/01.tmpl */}}
{{- template "greet.tmpl" }}, {{ .user.name }}!
{{/* pages/05.tmpl */}}
{{- doGreet }}, {{ .user.name }}!
{{/* pages/06.tmpl */}}
{{- greet }}, {{ show .user.name }}!
{{/* toolbox/greet.tmpl */}}
hello
{{/* toolbox/show.tmpl */}}
{{- range . -}}
{{- . -}}
{{- end -}}
# stdout
hello, from 00.tmpl!
hello, from 01.tmpl!
hi, from 05.tmpl!
hello, from 06.tmpl!
Syntactic sugar
Lucky for us, the templates from the toolbox/
don’t call each other. But they
could. However, if we tried to call them as functions, we’ll get a parsing error
when loading the toolbox/
, because the functions haven’t been registered just
yet. But we can still call them using the predefined template
function. However,
by default, this template
function only allows to send one “argument” to the
template (the usual “pipeline”), but in practice, you may have want to send
more than one.
We can tweak things by adding an arr()
function to the template.Template
that will collapse multiple argument to a single array, hence to a single
argument/pipeline.
Note also that so far, doGreet()
was only used in the pages, and so it was
OK to load it after the toolbox, but we now need arr()
within the toolbox,
so we have to define at least arr()
before loading the toolbox. We achieve
this by creating a dummy template
first. This yields something like:
package main
import (
"log"
"os"
"path/filepath"
"strings"
"text/template"
)
var tmpls *template.Template
func init() {
tmpls = template.Must(template.New("").Funcs(template.FuncMap{
"doGreet" : func() string {
return "hi"
},
"arr" : func(xs ...any) []any {
return xs
},
}).ParseGlob("toolbox/*.tmpl"))
for _, x := range tmpls.Templates() {
n := strings.TrimSuffix(x.Name(), ".tmpl")
// beware of the race...
m := x.Name()
tmpls.Funcs(template.FuncMap{
n : func(ys ...any) (string, error) {
var s strings.Builder
err := tmpls.ExecuteTemplate(&s, m, ys)
return s.String(), err
},
})
}
}
func main() {
for _, x := range []string{"07.tmpl"} {
err := template.Must(
template.Must(tmpls.Clone()).ParseFiles(filepath.Join("pages", x)),
).ExecuteTemplate(os.Stdout, x, map[string]any{
"user" : map[string]any{
"name" : "from "+x,
},
})
if err != nil {
log.Fatal(err)
}
}
}
{{/* pages/07.tmpl */}}
{{ callshow }}
{{/* toolbox/callshow.tmpl */}}
{{ template "show.tmpl" (arr "hello" " " "from" " " "callshow") }}
{{/* toolbox/show.tmpl */}}
{{- range . -}}
{{- . -}}
{{- end -}}
# stdout
hello from callshow
We should actually be able to call the toolbox/
templates from
each others as functions. Indeed, consider the following example,
where we first define a function test
, load the toolbox, and redefine
this function test
. When calling it from he toolbox later on, it’s
using this last definition:
package main
import (
"log"
"os"
"path/filepath"
"strings"
"text/template"
)
var tmpls *template.Template
func init() {
tmpls = template.Must(template.New("").Funcs(template.FuncMap{
"doGreet" : func() string {
return "hi"
},
"arr" : func(xs ...any) []any {
return xs
},
"test" : func() string {
return "before"
},
}).ParseGlob("toolbox/*.tmpl"))
for _, x := range tmpls.Templates() {
n := strings.TrimSuffix(x.Name(), ".tmpl")
// beware of the race...
m := x.Name()
tmpls.Funcs(template.FuncMap{
n : func(ys ...any) (string, error) {
var s strings.Builder
err := tmpls.ExecuteTemplate(&s, m, ys)
return s.String(), err
},
})
}
tmpls.Funcs(template.FuncMap{
"test" : func() string {
return "after"
},
})
}
func main() {
for _, x := range []string{"08.tmpl"} {
err := template.Must(
template.Must(tmpls.Clone()).ParseFiles(filepath.Join("pages", x)),
).ExecuteTemplate(os.Stdout, x, map[string]any{
"user" : map[string]any{
"name" : "from "+x,
},
})
if err != nil {
log.Fatal(err)
}
}
}
{{/* pages/08.tmpl */}}
{{ calltest }}
{{/* toolbox/calltest.tmpl */}}
{{ test }}
# stdout
after
Exercise: Registering dummy functions for each toolbox/*.tmpl
before updating them with the actual functions, and testing it all, is left
as an exercise to the reader1.
At this point, you may want to consider defining some regular interface
between your templates/toolbox, depending on what kind of data you originally
intend to provide your pages/
with: an array is nice, but a hash
with some predefined entries might be a better option.
Note: In case you’re interested, I’ve got another article building on this kind of template structures, allowing to pipe the content of a template from/to an arbitrary command.
-
To be honest, I’m being lazy; I hope it works as advertised. ↩︎
Comments
By email, at mathieu.bivert chez: