Handlebars for golang with the same features as handlebars.js 3.0
.
The full API documentation is available here: http://godoc.org/github.com/aymerick/raymond.
- Quick Start
- Correct Usage
- Context
- HTML Escaping
- Helpers
- Context Functions
- Partials
- Utility Functions
- Mustache
- Limitations
- Handlebars Lexer
- Handlebars Parser
- Test
- References
- Others Implementations
$ go get github.com/aymerick/raymond
The quick and dirty way of rendering a handlebars template:
package main
import (
"fmt"
"github.com/aymerick/raymond"
)
func main() {
tpl := `<div class="entry">
<h1>{{title}}</h1>
<div class="body">
{{body}}
</div>
</div>
`
ctx := map[string]string{
"title": "My New Post",
"body": "This is my first post!",
}
result, err := raymond.Render(tpl, ctx)
if err != nil {
panic("Please report a bug :)")
}
fmt.Print(result)
}
Displays:
<div class="entry">
<h1>My New Post</h1>
<div class="body">
This is my first post!
</div>
</div>
Please note that the template will be parsed everytime you call Render()
function. So you probably want to read the next section.
To avoid parsing a template several times, use the Parse()
and Exec()
functions:
package main
import (
"fmt"
"github.com/aymerick/raymond"
)
func main() {
source := `<div class="entry">
<h1>{{title}}</h1>
<div class="body">
{{body}}
</div>
</div>
`
ctxList := []map[string]string{
{
"title": "My New Post",
"body": "This is my first post!",
},
{
"title": "Here is another post",
"body": "This is my second post!",
},
}
// parse template
tpl, err := raymond.Parse(source)
if err != nil {
panic(err)
}
for _, ctx := range ctxList {
// render template
result, err := tpl.Exec(ctx)
if err != nil {
panic(err)
}
fmt.Print(result)
}
}
Displays:
<div class="entry">
<h1>My New Post</h1>
<div class="body">
This is my first post!
</div>
</div>
<div class="entry">
<h1>Here is another post</h1>
<div class="body">
This is my second post!
</div>
</div>
You can use MustParse()
and MustExec()
functions if you don't want to deal with errors:
// parse template
tpl := raymond.MustParse(source)
// render template
result := tpl.MustExec(ctx)
The rendering context can contain any type of values, including array
, slice
, map
, struct
and func
.
When using structs, be warned that only exported fields are accessible. However you can access exported fields in template with their lowercase names. For example, both {{author.firstName}}
and {{Author.FirstName}}
references give the same result, as long as Author
and FirstName
are exported struct fields.
More, you can use the handlebars
struct tag to specify a template variable name different from the struct field name.
package main
import (
"fmt"
"github.com/aymerick/raymond"
)
func main() {
source := `<div class="post">
<h1>By {{author.firstName}} {{author.lastName}}</h1>
<div class="body">{{body}}</div>
<h1>Comments</h1>
{{#each comments}}
<h2>By {{author.firstName}} {{author.lastName}}</h2>
<div class="body">{{content}}</div>
{{/each}}
</div>`
type Person struct {
FirstName string
LastName string
}
type Comment struct {
Author Person
Body string `handlebars:"content"`
}
type Post struct {
Author Person
Body string
Comments []Comment
}
ctx := Post{
Person{"Jean", "Valjean"},
"Life is difficult",
[]Comment{
Comment{
Person{"Marcel", "Beliveau"},
"LOL!",
},
},
}
output := raymond.MustRender(source, ctx)
fmt.Print(output)
}
Output:
<div class="post">
<h1>By Jean Valjean</h1>
<div class="body">Life is difficult</div>
<h1>Comments</h1>
<h2>By Marcel Beliveau</h2>
<div class="body">LOL!</div>
</div>
By default, the result of a mustache expression is HTML escaped. Use the triple mustache {{{
to output unescaped values.
source := `<div class="entry">
<h1>{{title}}</h1>
<div class="body">
{{{body}}}
</div>
</div>
`
ctx := map[string]string{
"title": "All about <p> Tags",
"body": "<p>This is a post about <p> tags</p>",
}
tpl := raymond.MustParse(source)
result := tpl.MustExec(ctx)
fmt.Print(result)
Output:
<div class="entry">
<h1>All about <p> Tags</h1>
<div class="body">
<p>This is a post about <p> tags</p>
</div>
</div>
When returning HTML from a helper, you should return a SafeString
if you don't want it to be escaped by default. When using SafeString
all unknown or unsafe data should be manually escaped with the Escape
method.
raymond.RegisterHelper("link", func(url, text string) raymond.SafeString {
return raymond.SafeString("<a href='" + raymond.Escape(url) + "'>" + raymond.Escape(text) + "</a>")
})
tpl := raymond.MustParse("{{link url text}}")
ctx := map[string]string{
"url": "http://www.aymerick.com/",
"text": "This is a <em>cool</em> website",
}
result := tpl.MustExec(ctx)
fmt.Print(result)
Output:
<a href='http://www.aymerick.com/'>This is a <em>cool</em> website</a>
Helpers can be accessed from any context in a template. You can register a helper with the RegisterHelper
function.
For example:
<div class="post">
<h1>By {{fullName author}}</h1>
<div class="body">{{body}}</div>
<h1>Comments</h1>
{{#each comments}}
<h2>By {{fullName author}}</h2>
<div class="body">{{body}}</div>
{{/each}}
</div>
With this context and helper:
ctx := map[string]interface{}{
"author": map[string]string{"firstName": "Jean", "lastName": "Valjean"},
"body": "Life is difficult",
"comments": []map[string]interface{}{{
"author": map[string]string{"firstName": "Marcel", "lastName": "Beliveau"},
"body": "LOL!",
}},
}
raymond.RegisterHelper("fullName", func(person map[string]string) string {
return person["firstName"] + " " + person["lastName"]
})
Outputs:
<div class="post">
<h1>By Jean Valjean</h1>
<div class="body">Life is difficult</div>
<h1>Comments</h1>
<h2>By Marcel Beliveau</h2>
<div class="body">LOL!</div>
</div>
Helper arguments can be any type.
The following example uses structs instead of maps and produces the same output as the previous one:
<div class="post">
<h1>By {{fullName author}}</h1>
<div class="body">{{body}}</div>
<h1>Comments</h1>
{{#each comments}}
<h2>By {{fullName author}}</h2>
<div class="body">{{body}}</div>
{{/each}}
</div>
With this context and helper:
type Post struct {
Author Person
Body string
Comments []Comment
}
type Person struct {
FirstName string
LastName string
}
type Comment struct {
Author Person
Body string
}
ctx := Post{
Person{"Jean", "Valjean"},
"Life is difficult",
[]Comment{
Comment{
Person{"Marcel", "Beliveau"},
"LOL!",
},
},
}
raymond.RegisterHelper("fullName", func(person Person) string {
return person.FirstName + " " + person.LastName
})
You can unregister global helpers with RemoveHelper
and RemoveAllHelpers
functions:
raymond.RemoveHelper("fullname")
raymond.RemoveAllHelpers()
You can register a helper on a specific template, and in that case that helper will be available to that template only:
tpl := raymond.MustParse("User: {{fullName user.firstName user.lastName}}")
tpl.RegisterHelper("fullName", func(firstName, lastName string) string {
return firstName + " " + lastName
})
Those built-in helpers are available to all templates.
You can use the if
helper to conditionally render a block. If its argument returns false
, nil
, 0
, ""
, an empty array, an empty slice or an empty map, then raymond will not render the block.
<div class="entry">
{{#if author}}
<h1>{{firstName}} {{lastName}}</h1>
{{/if}}
</div>
When using a block expression, you can specify a template section to run if the expression returns a falsy value. That section, marked by {{else}}
is called an "else section".
<div class="entry">
{{#if author}}
<h1>{{firstName}} {{lastName}}</h1>
{{else}}
<h1>Unknown Author</h1>
{{/if}}
</div>
You can chain several blocks. For example that template:
{{#if isActive}}
<img src="star.gif" alt="Active">
{{else if isInactive}}
<img src="cry.gif" alt="Inactive">
{{else}}
<img src="wat.gif" alt="Unknown">
{{/if}}
With that context:
ctx := map[string]interface{}{
"isActive": false,
"isInactive": false,
}
Outputs:
<img src="wat.gif" alt="Unknown">
You can use the unless
helper as the inverse of the if
helper. Its block will be rendered if the expression returns a falsy value.
<div class="entry">
{{#unless license}}
<h3 class="warning">WARNING: This entry does not have a license!</h3>
{{/unless}}
</div>
You can iterate over an array, a slice, a map or a struct instance using this built-in each
helper. Inside the block, you can use this
to reference the element being iterated over.
For example:
<ul class="people">
{{#each people}}
<li>{{this}}</li>
{{/each}}
</ul>
With this context:
map[string]interface{}{
"people": []string{
"Marcel", "Jean-Claude", "Yvette",
},
}
Outputs:
<ul class="people">
<li>Marcel</li>
<li>Jean-Claude</li>
<li>Yvette</li>
</ul>
You can optionally provide an {{else}}
section which will display only when the passed argument is an empty array, an empty slice or an empty map (a struct
instance is never considered empty).
{{#each paragraphs}}
<p>{{this}}</p>
{{else}}
<p class="empty">No content</p>
{{/each}}
When looping through items in each
, you can optionally reference the current loop index via {{@index}}
.
{{#each array}}
{{@index}}: {{this}}
{{/each}}
Additionally for map and struct instance iteration, {{@key}}
references the current map key or struct field name:
{{#each map}}
{{@key}}: {{this}}
{{/each}}
The first and last steps of iteration are noted via the @first
and @last
variables.
You can shift the context for a section of a template by using the built-in with
block helper.
<div class="entry">
<h1>{{title}}</h1>
{{#with author}}
<h2>By {{firstName}} {{lastName}}</h2>
{{/with}}
</div>
With this context:
map[string]interface{}{
"title": "My first post!",
"author": map[string]string{
"firstName": "Jean",
"lastName": "Valjean",
},
}
Outputs:
<div class="entry">
<h1>My first post!</h1>
<h2>By Jean Valjean</h2>
</div>
You can optionally provide an {{else}}
section which will display only when the passed argument is falsy.
{{#with author}}
<p>{{name}}</p>
{{else}}
<p class="empty">No content</p>
{{/with}}
The lookup
helper allows for dynamic parameter resolution using handlebars variables.
{{#each bar}}
{{lookup ../foo @index}}
{{/each}}
The log
helper allows for logging while rendering a template.
{{log "Look at me!"}}
Note that the handlebars.js @level
variable is not supported.
The equal
helper renders a block if the string version of both arguments are equals.
For example that template:
{{#equal foo "bar"}}foo is bar{{/equal}}
{{#equal foo baz}}foo is the same as baz{{/equal}}
{{#equal nb 0}}nothing{{/equal}}
{{#equal nb 1}}there is one{{/equal}}
{{#equal nb "1"}}everything is stringified before comparison{{/equal}}
With that context:
ctx := map[string]interface{}{
"foo": "bar",
"baz": "bar",
"nb": 1,
}
Outputs:
foo is bar
foo is the same as baz
there is one
everything is stringified before comparison
Block helpers make it possible to define custom iterators and other functionality that can invoke the passed block with a new context.
As an example, let's define a block helper that adds some markup to the wrapped text.
<div class="entry">
<h1>{{title}}</h1>
<div class="body">
{{#bold}}{{body}}{{/bold}}
</div>
</div>
The bold
helper will add markup to make its text bold.
raymond.RegisterHelper("bold", func(options *raymond.Options) raymond.SafeString {
return raymond.SafeString(`<div class="mybold">` + options.Fn() + "</div>")
})
A helper evaluates the block content with current context by calling options.Fn()
.
If you want to evaluate the block with another context, then use options.FnWith(ctx)
, like this french version of built-in with
block helper:
raymond.RegisterHelper("avec", func(context interface{}, options *raymond.Options) string {
return options.FnWith(context)
})
With that template:
{{#avec obj.text}}{{this}}{{/avec}}
Let's write a french version of if
block helper:
source := `{{#si yep}}YEP !{{/si}}`
ctx := map[string]interface{}{"yep": true}
raymond.RegisterHelper("si", func(conditional bool, options *raymond.Options) string {
if conditional {
return options.Fn()
}
return ""
})
Note that as the first parameter of the helper is typed as bool
an automatic conversion is made if corresponding context value is not a boolean. So this helper works with that context too:
ctx := map[string]interface{}{"yep": "message"}
Here, "message"
is converted to true
because it is an non-empty string. See IsTrue()
function for more informations on boolean conversion.
We can enhance the si
block helper to evaluate the else block
by calling options.Inverse()
if conditional is false:
source := `{{#si yep}}YEP !{{else}}NOP !{{/si}}`
ctx := map[string]interface{}{"yep": false}
raymond.RegisterHelper("si", func(conditional bool, options *raymond.Options) string {
if conditional {
return options.Fn()
}
return options.Inverse()
})
Outputs:
NOP !
It's possible to receive named parameters from supporting helpers.
{{#each users as |user userId|}}
Id: {{userId}} Name: {{user.name}}
{{/each}}
In this particular example, user
will have the same value as the current context and userId
will have the index/key value for the iteration.
This allows for nested helpers to avoid name conflicts.
For example:
{{#each users as |user userId|}}
{{#each user.books as |book bookId|}}
User: {{userId}} Book: {{bookId}}
{{/each}}
{{/each}}
With this context:
ctx := map[string]interface{}{
"users": map[string]interface{}{
"marcel": map[string]interface{}{
"books": map[string]interface{}{
"book1": "My first book",
"book2": "My second book",
},
},
"didier": map[string]interface{}{
"books": map[string]interface{}{
"bookA": "Good book",
"bookB": "Bad book",
},
},
},
}
Outputs:
User: marcel Book: book1
User: marcel Book: book2
User: didier Book: bookA
User: didier Book: bookB
As you can see, the second block parameter is the map key. When using structs, it is the struct field name.
When using arrays and slices, the second parameter is element index:
ctx := map[string]interface{}{
"users": []map[string]interface{}{
{
"id": "marcel",
"books": []map[string]interface{}{
{"id": "book1", "title": "My first book"},
{"id": "book2", "title": "My second book"},
},
},
{
"id": "didier",
"books": []map[string]interface{}{
{"id": "bookA", "title": "Good book"},
{"id": "bookB", "title": "Bad book"},
},
},
},
}
Outputs:
User: 0 Book: 0
User: 0 Book: 1
User: 1 Book: 0
User: 1 Book: 1
When calling a helper in a template, raymond expects the same number of arguments as the number of helper function parameters.
So this template:
{{add a}}
With this helper:
raymond.RegisterHelper("add", func(val1, val2 int) string {
return strconv.Itoa(val1 + val2)
})
Will simply panics, because we call the helper with one argument whereas it expects two.
Let's create a concat
helper that expects two strings and concat them:
source := `{{concat a b}}`
ctx := map[string]interface{}{
"a": "Jean",
"b": "Valjean",
}
raymond.RegisterHelper("concat", func(val1, val2 string) string {
return val1 + " " + val2
})
Everything goes well, two strings are passed as arguments to the helper that outputs:
Jean VALJEAN
But what happens if there is another type than string
in the context ? For example:
ctx := map[string]interface{}{
"a": 10,
"b": "Valjean",
}
Actually, raymond perfoms automatic string conversion. So because the first parameter of the helper is typed as string
, the first argument will be converted from the 10
integer to "10"
, and the helper outputs:
10 VALJEAN
Note that this kind of automatic conversion is done with bool
type too, thanks to the IsTrue()
function.
If a helper needs the Options
argument, just add it at the end of helper parameters:
raymond.RegisterHelper("add", func(val1, val2 int, options *raymond.Options) string {
return strconv.Itoa(val1 + val2) + " " + options.ValueStr("bananas")
})
Thanks to the options
argument, helpers have access to the current evaluation context, to the Hash
arguments, and they can manipulate the private data variables.
The Options
argument is even necessary for Block Helpers to evaluate block and "else block".
Helpers fetch current context values with options.Value()
and options.ValuesStr()
.
Value()
returns an interface{}
and lets the helper do the type assertions whereas ValueStr()
automatically converts the value to a string
.
For example:
source := `{{concat a b}}`
ctx := map[string]interface{}{
"a": "Marcel",
"b": "Beliveau",
"suffix": "FOREVER !",
}
raymond.RegisterHelper("concat", func(val1, val2 string, options *raymond.Options) string {
return val1 + " " + val2 + " " + options.ValueStr("suffix")
})
Outputs:
Marcel Beliveau FOREVER !
Helpers can get the entire current context with options.Ctx()
that returns an interface{}
.
Helpers access hash arguments with options.HashProp()
and options.HashStr()
.
HashProp()
returns an interface{}
and lets the helper do the type assertions whereas HashStr()
automatically converts the value to a string
.
For example:
source := `{{concat suffix first=a second=b}}`
ctx := map[string]interface{}{
"a": "Marcel",
"b": "Beliveau",
"suffix": "FOREVER !",
}
raymond.RegisterHelper("concat", func(suffix string, options *raymond.Options) string {
return options.HashStr("first") + " " + options.HashStr("second") + " " + suffix
})
Outputs:
Marcel Beliveau FOREVER !
Helpers can get the full hash with options.Hash()
that returns a map[string]interface{}
.
Helpers access private data variables with options.Data()
and options.DataStr()
.
Data()
returns an interface{}
and lets the helper do the type assertions whereas DataStr()
automatically converts the value to a string
.
Helpers can get the entire current data frame with options.DataFrame()
that returns a *DataFrame
.
For helpers that need to inject their own private data frame, use options.NewDataFrame()
to create the frame and options.FnData()
to evaluate the block with that frame.
For example:
source := `{{#voodoo kind=a}}Voodoo is {{@magix}}{{/voodoo}}`
ctx := map[string]interface{}{
"a": "awesome",
}
raymond.RegisterHelper("voodoo", func(options *raymond.Options) string {
// create data frame with @magix data
frame := options.NewDataFrame()
frame.Set("magix", options.HashProp("kind"))
// evaluates block with new data frame
return options.FnData(frame)
})
Helpers that need to evaluate the block with a private data frame and a new context can call options.FnCtxData()
.
In addition to Escape()
, raymond provides utility functions that can be usefull for helpers.
Str()
converts its parameter to a string
.
Booleans:
raymond.Str(3) + " foos and " + raymond.Str(-1.25) + " bars"
// Outputs: "3 foos and -1.25 bars"
Numbers:
"everything is " + raymond.Str(true) + " and nothing is " + raymond.Str(false)
// Outputs: "everything is true and nothing is false"
Maps:
raymond.Str(map[string]string{"foo": "bar"})
// Outputs: "map[foo:bar]"
Arrays and Slices:
raymond.Str([]interface{}{true, 10, "foo", 5, "bar"})
// Outputs: "true10foo5bar"
IsTrue()
returns the truthy version of its parameter.
It returns false
when parameter is either:
- an empty array
- an empty slice
- an empty map
""
nil
0
false
For all others values, IsTrue()
returns true
.
In addition to helpers, lambdas found in context are evaluated.
For example, that template and context:
source := "I {{feeling}} you"
ctx := map[string]interface{}{
"feeling": func() string {
rand.Seed(time.Now().UTC().UnixNano())
feelings := []string{"hate", "love"}
return feelings[rand.Intn(len(feelings))]
},
}
Randomly renders I hate you
or I love you
.
Those context functions behave like helper functions: they can be called with parameters and they can have an Options
argument.
You can register template partials before execution:
tpl := raymond.MustParse("{{> foo}} baz")
tpl.RegisterPartial("foo", "<span>bar</span>")
result := tpl.MustExec(nil)
fmt.Print(result)
Output:
<span>bar</span> baz
You can register several partials at once:
tpl := raymond.MustParse("{{> foo}} and {{> baz}}")
tpl.RegisterPartials(map[string]string{
"foo": "<span>bar</span>",
"baz": "<span>bat</span>",
})
result := tpl.MustExec(nil)
fmt.Print(result)
Output:
<span>bar</span> and <span>bat</span>
You can registers global partials that will be accessible by all templates:
raymond.RegisterPartial("foo", "<span>bar</span>")
tpl := raymond.MustParse("{{> foo}} baz")
result := tpl.MustExec(nil)
fmt.Print(result)
Or:
raymond.RegisterPartials(map[string]string{
"foo": "<span>bar</span>",
"baz": "<span>bat</span>",
})
tpl := raymond.MustParse("{{> foo}} and {{> baz}}")
result := tpl.MustExec(nil)
fmt.Print(result)
It's possible to dynamically select the partial to be executed by using sub expression syntax.
For example, that template randomly evaluates the foo
or baz
partial:
tpl := raymond.MustParse("{{> (whichPartial) }}")
tpl.RegisterPartials(map[string]string{
"foo": "<span>bar</span>",
"baz": "<span>bat</span>",
})
ctx := map[string]interface{}{
"whichPartial": func() string {
rand.Seed(time.Now().UTC().UnixNano())
names := []string{"foo", "baz"}
return names[rand.Intn(len(names))]
},
}
result := tpl.MustExec(ctx)
fmt.Print(result)
It's possible to execute partials on a custom context by passing in the context to the partial call.
For example:
tpl := raymond.MustParse("User: {{> userDetails user }}")
tpl.RegisterPartial("userDetails", "{{firstname}} {{lastname}}")
ctx := map[string]interface{}{
"user": map[string]string{
"firstname": "Jean",
"lastname": "Valjean",
},
}
result := tpl.MustExec(ctx)
fmt.Print(result)
Displays:
User: Jean Valjean
Custom data can be passed to partials through hash parameters.
For example:
tpl := raymond.MustParse("{{> myPartial name=hero }}")
tpl.RegisterPartial("myPartial", "My hero is {{name}}")
ctx := map[string]interface{}{
"hero": "Goldorak",
}
result := tpl.MustExec(ctx)
fmt.Print(result)
Displays:
My hero is Goldorak
You can use following utility fuctions to parse and register partials from files:
ParseFile()
- reads a file and return parsed templateTemplate.RegisterPartialFile()
- reads a file and registers its content as a partial with given nameTemplate.RegisterPartialFiles()
- reads several files and registers them as partials, the filename base is used as the partial name
Handlebars is a superset of mustache but it differs on those points:
- Alternative delimiters are not supported
- There is no recursive lookup
These handlebars options are currently NOT implemented:
compat
- enables recursive field lookupknownHelpers
- list of helpers that are known to exist (truthy) at template execution timeknownHelpersOnly
- allows further optimizations based on the known helpers listtrackIds
- include the id names used to resolve parameters for helpersnoEscape
- disables HTML escaping globallystrict
- templates will throw rather than silently ignore missing fieldsassumeObjects
- removes object existence checks when traversing pathspreventIndent
- disables the auto-indententation of nested partialsstringParams
- resolves a parameter to it's name if the value isn't present in the context stack
These handlebars features are currently NOT implemented:
- raw block content is not passed as a parameter to helper
blockHelperMissing
- helper called when a helper can not be directly resolvedhelperMissing
- helper called when a potential helper expression was not found@contextPath
- value set intrackIds
mode that records the lookup path for the current context@level
- log level
You should not use the lexer directly, but for your information here is an example:
package main
import (
"fmt"
"github.com/aymerick/raymond/lexer"
)
func main() {
source := "You know {{nothing}} John Snow"
output := ""
lex := lexer.Scan(source)
for {
// consume next token
token := lex.NextToken()
output += fmt.Sprintf(" %s", token)
// stops when all tokens have been consumed, or on error
if token.Kind == lexer.TokenEOF || token.Kind == lexer.TokenError {
break
}
}
fmt.Print(output)
}
Outputs:
Content{"You know "} Open{"{{"} ID{"nothing"} Close{"}}"} Content{" John Snow"} EOF
You should not use the parser directly, but for your information here is an example:
package main
import (
"fmt"
"github.com/aymerick/raymond/ast"
"github.com/aymerick/raymond/parser"
)
fu nc main() {
source := "You know {{nothing}} John Snow"
// parse template
program, err := parser.Parse(source)
if err != nil {
panic(err)
}
// print AST
output := ast.Print(program)
fmt.Print(output)
}
Outputs:
CONTENT[ 'You know ' ]
{{ PATH:nothing [] }}
CONTENT[ ' John Snow' ]
First, fetch mustache tests:
$ git submodule update --init
To run all tests:
$ go test ./...
To filter tests:
$ go test -run="Partials"
To run all test and all benchmarks:
$ go test -bench . ./...
To test with race detection:
$ go test -race ./...
- http://handlebarsjs.com/
- https://mustache.github.io/mustache.5.html
- https://github.com/golang/go/tree/master/src/text/template
- https://www.youtube.com/watch?v=HxaD_trXwRE
- handlebars.js - javascript
- handlebars.java - java
- handlebars.rb - ruby
- handlebars.php - php
- handlebars-objc - Objective C
- rumblebars - rust