Bat
A mustache like ({{foo.bar}}
) templating engine for Go. This is still very
much WIP, but contributions and issues are welcome.
Usage
Given a file, index.batml
:
<h1>Hello {{Team.Name}}</h1>
Create a new template and execute it:
content, _ := ioutil.ReadFile("index.bat")
bat.NewTemplate(content)
t := team{
Name: "Foo",
}
bat.Execute(map[string]any{"Team": team})
Engine
Bat provides an engine that allows you to register templates and provides
default, as well as user provided helper functions to those templates.
engine := bat.NewEngine(bat.HTMLEscape)
engine.Register("index.bat", "<h1>Hello {{Team.Name}}</h1>")
or, you can use AutoRegister
to automatically register all templates in a
directory. This is useful with the Go embed package:
//go:embed templates
var templates embed.FS
engine := bat.NewEngine(bat.HTMLEscape)
engine.AutoRegister(templates, ".html")
engine.Render("templates/users/signup.html", map[string]any{"Team": team})
Built-in helpers
safe
- marks a value as safe to be rendered. This is useful for rendering
HTML. For example, {{safe("<h1>Foo</h1>")}}
will render <h1>Foo</h1>
.
len
- returns the length of a slice or map. For example, {{len(Users)}}
will
return the length of the Users
slice.
partial
- renders a partial template. For example, {{partial("header", {foo: "bar"})}}
will render the header
template with the provided map as locals.
layout
- Wraps the current template with the provided layout. For example,
{{ layout("layouts/application") }}
will render the current template wrapped with template registered as "layouts/application". All data available to the current template will be available to the layout.
Here's an overview of more advanced usage:
Primitives
Bat supports the following primitives that can be used within {{}}
expressions:
- booleans -
true
and false
- nil -
nil
- strings -
"string value"
and "string with \"escaped\" values"
- integers -
1000
and -1000
- maps -
{ foo: 1, bar: "two" }
Data Access
Templates accept data in the form of map[string]any
. The strings must be
valid identifiers in order to be access, which start with an alphabetical
character following by any number of alphanumerical characters.
The template {{userName}}
would attempt to access the userName
key from the
provided data map.
e.g.
t := bat.NewTemplate(`{{userName}}!`)
out := new(bytes.Buffer)
// outputs "gogopher!"
t.Execute(out, map[string]{"Username": "gogopher"}
Chaining and method calls are also supported:
type Name struct {
First string
Last string
}
type User struct {
Name Name
}
func (n Name) Initials() string {
return n.First[0:1] + n.Last[0:1]
}
t := bat.NewTemplate(`{{user.Name.Initials()}}!`)
out := new(bytes.Buffer)
user := User{
Name: Name{
First: "Fox",
Last: "Mulder",
}
}
// outputs "FM!"
t.Execute(out, map[string]{"user": user}
Finally, map/slice/array access is supported via []
:
<h1>{{user[0].Name.First}}</h1>
Conditionals
Bat supports if
statements, and the !=
and ==
operators.
{{if user != nil}}
<a href="/login">Login</a>
{{else}}
<a href="/profile">View your profile</a>
{{end}}
Not
The !
operator can be used to negate an expression and return a boolean
{{!true}}
The above will render false
.
Iterators
Iteration is supported via the range
keyword. Supported types are slices, maps, arrays, and channels.
{{range $index, $name in data}}
<h1>Hello {{$name}}, number {{$index}}</h1>
{{end}}
Given data
being defined as: []string{"Fox Mulder", "Dana Scully"}
, the resulting output would look like:
<h1>Hello Fox Mulder, number 0</h1>
<h1>Hello Dana Scully, number 1</h1>
In the example above, range defines two variables which must begin with a $
so they don't conflict with data
passed into the template.
The range
keyword can also be used with a single variable, providing only the
key or index to the iterator:
{{range $index in data}}
<h1>Hello person {{$index}}</h1>
{{end}}
Given data
being defined as: []string{"Fox Mulder", "Dana Scully"}
, the resulting output would look like:
<h1>Hello person 0</h1>
<h1>Hello person 1</h1>
If a map is passed to range
, it will attempt to sort it before iteration if
the key is able to be compared and is implemented in the internal/mapsort
package.
Helper functions
Helper functions can be provided directly to templates using the WithHelpers
function when instantiating a template.
e.g.
helloHelper := func(name string) string {
return fmt.Sprintf("Hello %s!", name)
}
t := bat.NewTemplate(`{{hello "there"}}`, WithHelpers(map[string]any{"hello": helloHelper}))
// output "Hello there!"
out := new(bytes.Buffer)
t.Execute(out, map[string]any{})
Escaping
Templates can be provided a custom escape function with the signature
func(string) string
that will be called on the resulting output from {{}}
blocks.
There are two escape functions that can be utilized, NoEscape
which does no
escaping, and HTMLEscape
which delegates to html.EscapeString
, which
escapes HTML.
The default escape function is HTMLEscape
for safety reasons.
e.g.
// This template will escape HTML from the output of `{{}}` blocks
t := NewTemplate("{{foo}}", WithEscapeFunc(HTMLEscape))
Escaping can be avoided by returning the bat.Safe
type from the result of a
{{}}
block.
e.g.
t := bat.NewTemplate(`{{output}}`, WithEscapeFunc(HTMLEscape))
// output "Hello there!"
out := new(bytes.Buffer)
// outputs <h1>Hello!</h1^gt;
t.Execute(out, map[string]any{"output": "<h1>Hello!</h1>"})
// outputs <h1>Hello!</h1>
t.Execute(out, map[string]any{"output": bat.Safe("<h1>Hello!</h1>")})
Math
Basic math is supported, with some caveats. When performing math operations,
the left most type is converted into the right most type, when possible:
// int32 - int64
100 - 200 // returns int64
The following operations are supported:
-
Subtraction
+
Addition
*
Multiplication
/
Division
%
Modulus
More comprehensive casting logic would be welcome in the form of a PR.
Comments are supported as complete statements or at the end of a statement.
{{ // This is a comment }}
{{ foo // This is also a comment }}
TODO
- Add
each
functionality (see the section on range
)
- Add
if
and else
functionality
- Emit better error messages and validate them with tests (template execution)
- Emit better error messages from lexer and parser
- Create an engine struct that will enable partials, helper functions, and
custom escaping functions.
- Add escaping support to templates
- Support strings in templates
- Support integer numbers
- Add basic math operations
- Simple map class
{ "foo": bar }
for use with partials
- Improve stringify logic in the executor (
bat.go
)
- Support channels in
range
- Trim whitespace by default, add control characters to avoid trimming.
- Support method calls
- Support helpers
- Support map/slice array access
[]
- Validate helper methods have 0 or 1 return values
Maybe
- Add &&, and || operators for more complex conditionals
Replace {{end}}
with named end blocks, like {{/if}}
rejected
- Add support for
{{else if <expression>}}
Support the not operator, e.g. if !foo
done
- Track and error on undefined variable usage in the parsing stage
Don't
- Add parens for complex options
- Variable declarations that look like provided data access (use $ for template locals, plain identifiers for everything else)
Add string concatenation