https://github.com/blakewilliams/bat
Intuitive templating engine for Go
https://github.com/blakewilliams/bat
go golang template template-engine templates
Last synced: 5 months ago
JSON representation
Intuitive templating engine for Go
- Host: GitHub
- URL: https://github.com/blakewilliams/bat
- Owner: BlakeWilliams
- License: mit
- Created: 2022-07-31T15:47:14.000Z (almost 4 years ago)
- Default Branch: main
- Last Pushed: 2024-01-02T15:34:45.000Z (over 2 years ago)
- Last Synced: 2025-04-03T19:52:52.997Z (over 1 year ago)
- Topics: go, golang, template, template-engine, templates
- Language: Go
- Homepage:
- Size: 105 KB
- Stars: 8
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# 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`:
```
Hello {{Team.Name}}
```
Create a new template and execute it:
```go
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.
```go
engine := bat.NewEngine(bat.HTMLEscape)
engine.Register("index.bat", "
Hello {{Team.Name}}
")
```
or, you can use `AutoRegister` to automatically register all templates in a
directory. This is useful with the Go embed package:
```go
//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("
Foo
")}}` will render `Foo
`.
- `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.
```go
t := bat.NewTemplate(`{{userName}}!`)
out := new(bytes.Buffer)
// outputs "gogopher!"
t.Execute(out, map[string]{"Username": "gogopher"}
```
Chaining and method calls are also supported:
```go
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 `[]`:
```html
{{user[0].Name.First}}
```
### Conditionals
Bat supports `if` statements, and the `!=` and `==` operators.
```html
{{if user != nil}}
Login
{{else}}
View your profile
{{end}}
```
### Not
The `!` operator can be used to negate an expression and return a boolean
```html
{{!true}}
```
The above will render `false`.
### Iterators
Iteration is supported via the `range` keyword. Supported types are slices, maps, arrays, and channels.
```html
{{range $index, $name in data}}
Hello {{$name}}, number {{$index}}
{{end}}
```
Given `data` being defined as: `[]string{"Fox Mulder", "Dana Scully"}`, the resulting output would look like:
```html
Hello Fox Mulder, number 0
Hello Dana Scully, number 1
```
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:
```html
{{range $index in data}}
Hello person {{$index}}
{{end}}
```
Given `data` being defined as: `[]string{"Fox Mulder", "Dana Scully"}`, the resulting output would look like:
```html
Hello person 0
Hello person 1
```
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.
```go
// 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.
```go
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": "
Hello!
"})
// outputs
Hello!
t.Execute(out, map[string]any{"output": bat.Safe("Hello!
")})
```
### 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:
```go
// 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
Comments are supported as complete statements or at the end of a statement.
```html
{{ // This is a comment }}
```
```html
{{ foo // This is also a comment }}
```
## TODO
- [x] Add `each` functionality (see the section on `range`)
- [x] Add `if` and `else` functionality
- [x] Emit better error messages and validate them with tests (template execution)
- [ ] Emit better error messages from lexer and parser
- [x] Create an engine struct that will enable partials, helper functions, and
custom escaping functions.
- [x] Add escaping support to templates
- [x] Support strings in templates
- [x] Support integer numbers
- [x] Add basic math operations
- [x] Simple map class `{ "foo": bar }` for use with partials
- [ ] Improve stringify logic in the executor (`bat.go`)
- [x] Support channels in `range`
- [ ] Trim whitespace by default, add control characters to avoid trimming.
- [x] Support method calls
- [x] Support helpers
- [x] 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 }}`
- ~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~