Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

535 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Liquid templates for Go

go badge Golangci-lint badge Go Report Card badge Go Doc MIT License

liquid is a pure Go implementation of Shopify Liquid. It was developed for Gojekyll, a Go port of the Jekyll static-site generator.

Installation

go get github.com/osteele/liquid@latest

Usage

engine := 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.

Jekyll compatibility

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 Doe

Jekyll extensions are disabled by default.

Command-line tool

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!

Security

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.

Documentation

The API reference documents exported Go types and methods. The guides cover custom template stores, FRender, security, and loop-modifier differences.

Status

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

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.

Value Types

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:

  • false and nil
    • These, and no other values, are recognized as false by and, or, {% if %}, {% elsif %}, and {% case %}.
  • Integers
    • Integers can be used as array indices: array[1]; array[n], where array has an array value and n has an integer value.
    • (Only) integers can be used as the endpoints of a range: {% for item in (1..5) %}, {% for item in (start..end) %} where start and end have integer values.
  • Integers and floats
    • Integers and floats are converted to their join type for comparison: 1 == 1.0 evaluates to true. Similarly, int8(1), int16(1), and uint8(1) are all equal.
    • Complex numbers receive no special treatment.
  • 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.
  • Arrays (and slices)
    • An array can be indexed by integer value: array[1]; array[n] where n has an integer value.
    • Arrays have first, last, and size properties: array.first == array[0], array[array.size-1] == array.last (where array.size > 0)
  • Maps
    • A map can be indexed by a string: hash["key"]; hash[s] where s has a string value.
    • A map can be accessed using property syntax: hash.key.
    • Maps have a special size property, that returns the size of the map.
  • Drops
    • A value value of a type that implements the Drop interface acts as the value value.ToLiquid(). There is no guarantee about how many times ToLiquid will be called. [This is in contrast to Shopify Liquid, which both uses a different interface for drops, and makes stronger guarantees.]
  • Structs
    • A public field of a struct can be accessed by its name: value.FieldName, value["FieldName"].
      • A field tagged liquid:"name" is accessed as value.name instead.
      • 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 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.size property.
  • []byte
    • A value of type []byte is rendered as the corresponding string, and presented as a string to filters that expect one. A []byte is not (currently) equivalent to a string for all uses; for example, a < b, a contains b, hash[b] will not behave as expected where a or b is a []byte.
  • MapSlice
    • An instance of yaml.MapSlice acts as a map. It implements m.key, m[key], and m.size.

Template Store

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.

Advanced Rendering

Custom Writers (FRender)

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.

References

Contributing

Bug reports, test cases, documentation, and code contributions are welcome. Read the contribution guide before opening a pull request.

Contributors

Thanks to these contributors (emoji key):


Oliver Steele

πŸ’» πŸ“– πŸ€” πŸš‡ πŸ‘€ ⚠️

James Littlejohn

πŸ’» πŸ“– ⚠️

nsf

πŸ’» ⚠️

Tobias Salzmann

πŸ’»

Ben Doerr

πŸ’»

Daniil Gentili

πŸ’»

Carolyn Van Slyck

πŸ’»

Kimmo Lehto

πŸ’»

Victor "Vito" Gama

πŸ’»

Utpal Sarkar

πŸ’» ⚠️

Misko Lee

πŸ’»

Andre Lehmann

πŸ’»

James O'Gorman

πŸ’» πŸ›

Olivier Favre

πŸ’»

Peter Aba

πŸ“–

Christopher Hill

πŸ’» πŸ›

Steve Atkins

πŸ’» πŸ›

Preston Price

πŸ’»

jamslinger

πŸ’» πŸ›

Andreas Deininger

πŸ’»

Matteo Agius-D'Arrigo

πŸ’»

Cody Krieger

πŸ’»

StΓ©phane JAIS

πŸ’»

James Newman

πŸ’» πŸ›

chris

πŸ’»

Dmitry Panov

πŸ’»

Gauthier Hacout

πŸ›

Jaime Amate

πŸ’»

Michael Visser

πŸ’» ⚠️

Pierre

πŸ’» πŸ“– πŸ€” ⚠️

Jush Jiang

πŸ€”

tuchida

πŸ€”

Tim Anema

πŸ€”

This project follows the all-contributors specification. Contributions of all kinds are welcome.

Attribution

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.

Other Implementations

Go

Other Languages

See Shopify's ports of Liquid to other environments.

License

MIT License

Releases

Packages

Used by

Contributors

Languages