liquid is a pure Go implementation of
Shopify Liquid. It was developed for
Gojekyll, a Go port of the Jekyll
static-site generator.
go get github.com/osteele/liquid@latestengine := liquid.NewEngine()
template := `<h1>{{ page.title }}</h1>`
bindings := map[string]any{
"page": map[string]string{
"title": "Introduction",
},
}
out, err := engine.ParseAndRenderString(template, bindings)
if err != nil {
log.Fatal(err)
}
fmt.Println(out)
// Output: <h1>Introduction</h1>See the API documentation for additional examples.
Optional Jekyll extensions support syntax that is not part of Shopify Liquid.
To enable Jekyll compatibility mode:
engine := liquid.NewEngine()
engine.EnableJekyllExtensions()Jekyll mode allows dot notation in assignment targets, such as
{% assign page.canonical_url = "/about/" %}. It creates missing intermediate
maps. Nested assignments use copy-on-write and do not modify maps supplied by
the caller.
Example:
engine := liquid.NewEngine()
engine.EnableJekyllExtensions()
template := `{% assign page.meta.author = "John Doe" %}{{ page.meta.author }}`
bindings := map[string]any{
"page": map[string]any{
"title": "Home",
},
}
out, err := engine.ParseAndRenderString(template, bindings)
if err != nil {
log.Fatal(err)
}
fmt.Println(out)
// Output: John DoeJekyll extensions are disabled by default.
go install github.com/osteele/liquid/cmd/liquid@latest installs a command-line
liquid executable for testing templates and preparing bug reports.
$ liquid --help
usage: liquid [FILE]
$ echo '{{ "Hello World" | downcase | split: " " | first | append: "!"}}' | liquid
hello!Read the security policy before rendering untrusted templates.
The engine has no built-in CPU, memory, iteration, or output limits.
Auto-escaping is opt-in. The include and render tags can read through the
configured template store. Registered extensions and callable bound values
execute application Go code.
Use FRender to limit output and check cooperative
cancellation. Use process or container isolation when you need enforceable
resource limits.
The API reference documents exported Go types and methods. The
guides cover custom template stores,
FRender, security, and
loop-modifier differences.
The following Shopify Liquid feature is not implemented:
- Warn and lax error modes.
Engine.LaxFilters()does provide Shopify-compatible pass-through behavior for undefined filters.
Drops have a different design from the Shopify (Ruby) implementation. A Ruby
drop sets liquid_attributes to a list of attributes that are exposed to
Liquid. A Go drop implements ToLiquid() any, that returns a proxy
object. The proxy is usually a map or struct that defines the exposed
properties. See the
Drop API documentation
for details.
Render and friends take a Bindings parameter. This is a map of string to
any that associates template variable names with Go values.
Any Go value can be used as a variable value. These values have special meaning:
falseandnil- These, and no other values, are recognized as false by
and,or,{% if %},{% elsif %}, and{% case %}.
- These, and no other values, are recognized as false by
- Integers
- Integers can be used as array indices:
array[1];array[n], wherearrayhas an array value andnhas an integer value. - (Only) integers can be used as the endpoints of a range:
{% for item in (1..5) %},{% for item in (start..end) %}wherestartandendhave integer values.
- Integers can be used as array indices:
- Integers and floats
- Integers and floats are converted to their join type for comparison:
1 == 1.0evaluates totrue. Similarly,int8(1),int16(1), anduint8(1)are all equal. - Complex numbers receive no special treatment.
- Integers and floats are converted to their join type for comparison:
- Integers, floats, and strings
- Integers, floats, and strings can be used in comparisons
<,>,<=,>=. Integers and floats can be usefully compared with each other. Strings can be usefully compared with each other, but not with other values. Any other comparison, e.g.1 < "one",1 > "one", is always false.
- Integers, floats, and strings can be used in comparisons
- Arrays (and slices)
- An array can be indexed by integer value:
array[1];array[n]wherenhas an integer value. - Arrays have
first,last, andsizeproperties:array.first == array[0],array[array.size-1] == array.last(wherearray.size > 0)
- An array can be indexed by integer value:
- Maps
- A map can be indexed by a string:
hash["key"];hash[s]whereshas a string value. - A map can be accessed using property syntax:
hash.key. - Maps have a special
sizeproperty, that returns the size of the map.
- A map can be indexed by a string:
- Drops
- A value
valueof a type that implements theDropinterface acts as the valuevalue.ToLiquid(). There is no guarantee about how many timesToLiquidwill be called. [This is in contrast to Shopify Liquid, which both uses a different interface for drops, and makes stronger guarantees.]
- A value
- Structs
- A public field of a struct can be accessed by its name:
value.FieldName,value["FieldName"].- A field tagged
liquid:"name"is accessed asvalue.nameinstead. - If the value of the field is a function that takes no arguments and returns either one or two values, accessing it invokes the function, and the value of the property is its first return value.
- If the second return value is non-nil, accessing the field panics instead.
- A field tagged
- A function defined on a struct can be accessed by function name e.g.
value.Func,value["Func"].- The same rules apply as to accessing a func-valued public field.
- Note that despite being array- and map-like, structs do not have a special
value.sizeproperty.
- A public field of a struct can be accessed by its name:
[]byte- A value of type
[]byteis rendered as the corresponding string, and presented as a string to filters that expect one. A[]byteis not (currently) equivalent to astringfor all uses; for example,a < b,a contains b,hash[b]will not behave as expected whereaorbis a[]byte.
- A value of type
MapSlice- An instance of
yaml.MapSliceacts as a map. It implementsm.key,m[key], andm.size.
- An instance of
TemplateStore loads files for the include and render tags. Implement it
to load templates from an embedded filesystem, database, or service:
type TemplateStore interface {
ReadTemplate(templateName string) ([]byte, error)
}
engine.RegisterTemplateStore(myTemplateStore)FileTemplateStore is the default. It confines reads to Root; an empty root
uses the current working directory. Include and render paths are relative to
the source template and cannot escape its directory.
See the embedded template-store example.
Use FRender to write directly to an io.Writer:
var buf bytes.Buffer
err := template.FRender(&buf, bindings)Writer wrappers can limit output, check a cancellation context when output is
written, or transform output. Writer errors are returned from FRender and
support errors.Is.
See the FRender guide for examples and limitations.
Bug reports, test cases, documentation, and code contributions are welcome. Read the contribution guide before opening a pull request.
Thanks to these contributors (emoji key):
This project follows the all-contributors specification. Contributions of all kinds are welcome.
| Package | Author | Description | License |
|---|---|---|---|
| Ragel | Adrian Thurston | scanning expressions | MIT |
| gopkg.in/yaml.v2 | Canonical | MapSlice | Apache License 2.0 |
Michael Hamrah's Lexing with Ragel and Parsing with Yacc using
Go
was essential to understanding go yacc.
The original Liquid engine, of course, for the design and documentation of the Liquid template language. Many of the tag and filter test cases are taken directly from the Liquid documentation.
- karlseguin/liquid is a dormant implementation that inspired a lot of forks.
- acstech/liquid is a more active fork of Karl Seguin's implementation.
- hownowstephen/go-liquid
See Shopify's ports of Liquid to other environments.
MIT License