bat

package module
v0.0.0-...-0e995ac Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 2, 2024 License: MIT Imports: 13 Imported by: 2

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:

<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 &lt;h1&gt;Hello!&lt;/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

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

Documentation

Index

Constants

This section is empty.

Variables

View Source
var HTMLEscape func(s string) string = html.EscapeString

An escapeFunc that returns text as escaped HTML

Functions

func NoEscape

func NoEscape(s string) string

An escapeFunc that returns text as-is

func WithEscapeFunc

func WithEscapeFunc(fn func(string) string) func(*Template)

An option function that provides a custom escape function that is used to escape unsafe dynamic template values.

Types

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

An Engine represents a collection of templates and helper functions. This allows templates to utilize partials and custom escape functions. For most applications, there should be 1 engine per-filetype.

func NewEngine

func NewEngine(escapeFunc func(text string) string) *Engine

Returns a new engine. NewEngine accepts an escape function that accepts un-escpaed text and returns escaped text safe for output.

func (*Engine) AutoRegister

func (e *Engine) AutoRegister(dir fs.FS, pathPrefix string, extension string) error

AutoRegister recursivly finds all files with the given extension and registers them as a template on the engine. If removePathPrefix is provided, it will register templates without the given prefix.

e.g. e.AutoRegister("./templates", ".html") and a file ./templates/users/hello.html will register the template with a name of "./templates/users/hello.html"

This is designed to be used with the embed package, allowing templates to be compiled into the resulting binary.

func (*Engine) Helper

func (e *Engine) Helper(name string, fn any)

Helper declares a new helper function available to templates by using the provided name.

If the provided value is not a function this method will panic.

func (*Engine) Register

func (e *Engine) Register(name string, input string) error

Registers a new template using the given name. Typically name's will be relative file paths. e.g. users/new.batml

func (*Engine) RegisterFile

func (e *Engine) RegisterFile(name string, input string) error

Registers a new template using the given name. Typically name's will be relative file paths. e.g. users/new.batml

func (*Engine) Render

func (e *Engine) Render(w io.Writer, name string, data map[string]any) error

Renders the template with the given name and data to the provider writer.

func (*Engine) RenderWithHelpers

func (e *Engine) RenderWithHelpers(w io.Writer, name string, helpers map[string]any, data map[string]any) error

type Safe

type Safe string

Safe values are not escaped. These should be used carefully as they expose risk to your templates outputting unsafe values, especially if the values are derived from user input.

type Template

type Template struct {
	// contains filtered or unexported fields
}

Represents a single template that can be rendered.

func NewTemplate

func NewTemplate(name string, input string, opts ...TemplateOption) (Template, error)

Creates a new template using the provided input. Options can be provided to customize the template, such as setting the function used to escape unsafe input.

func (*Template) Execute

func (t *Template) Execute(out io.Writer, extraHelpers map[string]any, data map[string]any) (err error)

Executes the template, streaming output to out. The data parameter is made available to the template.

func (*Template) Name

func (t *Template) Name() string

Name returns the name of the template.

type TemplateOption

type TemplateOption = func(*Template)

A function that allows the template to be customized when using NewTemplate.

func WithHelpers

func WithHelpers(fns map[string]any) TemplateOption

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL