liquid

package module
v1.9.1 Latest Latest
Warning

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

Go to latest
Published: Aug 12, 2026 License: MIT Imports: 6 Imported by: 93

README

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

Documentation

Overview

Package liquid is a pure Go implementation of Shopify Liquid templates, developed for use in https://github.com/osteele/gojekyll.

See the project README https://github.com/osteele/liquid for additional information and implementation status.

The liquid package itself is versioned in gopkg.in. Subpackages have no compatibility guarantees. Except where specifically documented, the “public” entities of subpackages are intended only for use by the liquid package and its subpackages.

Example
engine := NewEngine()
source := `<h1>{{ page.title }}</h1>`
bindings := map[string]any{
	"page": map[string]string{
		"title": "Introduction",
	},
}

out, err := engine.ParseAndRenderString(source, bindings)
if err != nil {
	log.Fatalln(err)
}

fmt.Println(out)
Output:
<h1>Introduction</h1>

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func FromDrop added in v1.0.0

func FromDrop(object any) any

FromDrop returns object.ToLiquid() if object's type implements this function; else the object itself.

func IterationKeyedMap added in v1.2.3

func IterationKeyedMap(m map[string]any) tags.IterationKeyedMap

IterationKeyedMap returns a map whose {% for %} tag iteration values are its keys, instead of [key, value] pairs. Use this to create a Go map with the semantics of a Ruby struct drop.

Example
vars := map[string]any{
	"map":       map[string]any{"a": 1},
	"keyed_map": IterationKeyedMap(map[string]any{"a": 1}),
}
engine := NewEngine()

out, err := engine.ParseAndRenderString(
	`{% for k in map %}{{ k[0] }}={{ k[1] }}.{% endfor %}`, vars)
if err != nil {
	log.Fatal(err)
}

fmt.Println(out)

out, err = engine.ParseAndRenderString(
	`{% for k in keyed_map %}{{ k }}={{ keyed_map[k] }}.{% endfor %}`, vars)
if err != nil {
	log.Fatal(err)
}

fmt.Println(out)
Output:
a=1.
a=1.

Types

type Bindings

type Bindings map[string]any

Bindings is a map of variable names to values.

Clients need not use this type. It is used solely for documentation. Callers can use instances of map[string]any itself as argument values to functions declared with this parameter type.

type Drop

type Drop interface {
	ToLiquid() any
}

Drop indicates that the object will present to templates as its ToLiquid value.

Example (Map)
// type redConvertible struct{}
//
// func (c redConvertible) ToLiquid() any {
// 	return map[string]any{
// 		"color": "red",
// 	}
// }
engine := NewEngine()
bindings := map[string]any{
	"car": redConvertible{},
}
template := `{{ car.color }}`

out, err := engine.ParseAndRenderString(template, bindings)
if err != nil {
	log.Fatalln(err)
}

fmt.Println(out)
Output:
red
Example (Struct)
// type car struct{ color, model string }
//
// func (c car) ToLiquid() any {
// 	return carDrop{c.model, c.color}
// }
//
// type carDrop struct {
// 	Model string
// 	Color string `liquid:"color"`
// }
//
// func (c carDrop) Drive() string {
// 	return "AWD"
// }
engine := NewEngine()
bindings := map[string]any{
	"car": car{"blue", "S85"},
}
template := `{{ car.color }} {{ car.Drive }} Model {{ car.Model }}`

out, err := engine.ParseAndRenderString(template, bindings)
if err != nil {
	log.Fatalln(err)
}

fmt.Println(out)
Output:
blue AWD Model S85

type Engine

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

An Engine parses template source into renderable text.

An engine can be configured with additional filters and tags.

func NewBasicEngine added in v1.6.0

func NewBasicEngine() *Engine

NewBasicEngine returns a new Engine without the standard filters or tags.

func NewEngine

func NewEngine() *Engine

NewEngine returns a new Engine.

func (*Engine) Delims added in v1.2.1

func (e *Engine) Delims(objectLeft, objectRight, tagLeft, tagRight string) *Engine

Delims sets the action delimiters to the specified strings, to be used in subsequent calls to ParseTemplate, ParseTemplateLocation, ParseAndRender, or ParseAndRenderString. An empty delimiter stands for the corresponding default: objectLeft = {{, objectRight = }}, tagLeft = {% , tagRight = %}

func (*Engine) EnableJekyllExtensions added in v1.8.0

func (e *Engine) EnableJekyllExtensions()

EnableJekyllExtensions enables Jekyll-specific extensions to Liquid. This includes support for dot notation in assign tags (e.g., {% assign page.canonical_url = value %}). Note: This is not part of the Shopify Liquid standard but is used in Jekyll and Gojekyll.

func (*Engine) LaxFilters added in v1.8.0

func (e *Engine) LaxFilters()

LaxFilters causes the renderer to silently pass through the input value when the template contains an undefined filter, matching Shopify Liquid behavior. By default, undefined filters cause an error.

func (*Engine) ParseAndFRender added in v1.4.0

func (e *Engine) ParseAndFRender(w io.Writer, source []byte, b Bindings) SourceError

ParseAndFRender parses and then renders the template into w.

func (*Engine) ParseAndRender

func (e *Engine) ParseAndRender(source []byte, b Bindings) ([]byte, SourceError)

ParseAndRender parses and then renders the template.

func (*Engine) ParseAndRenderString

func (e *Engine) ParseAndRenderString(source string, b Bindings) (string, SourceError)

ParseAndRenderString is a convenience wrapper for ParseAndRender, that takes string input and returns a string.

Example
engine := NewEngine()
source := `{{ hello | capitalize | append: " Mundo" }}`
bindings := map[string]any{"hello": "hola"}

out, err := engine.ParseAndRenderString(source, bindings)
if err != nil {
	log.Fatalln(err)
}

fmt.Println(out)
Output:
Hola Mundo

func (*Engine) ParseString added in v1.2.1

func (e *Engine) ParseString(source string) (*Template, SourceError)

ParseString creates a new Template using the engine configuration.

func (*Engine) ParseTemplate

func (e *Engine) ParseTemplate(source []byte) (*Template, SourceError)

ParseTemplate creates a new Template using the engine configuration.

Example
engine := NewEngine()
source := `{{ hello | capitalize | append: " Mundo" }}`
bindings := map[string]any{"hello": "hola"}

tpl, err := engine.ParseString(source)
if err != nil {
	log.Fatalln(err)
}

out, err := tpl.RenderString(bindings)
if err != nil {
	log.Fatalln(err)
}

fmt.Println(out)
Output:
Hola Mundo

func (*Engine) ParseTemplateAndCache added in v1.3.0

func (e *Engine) ParseTemplateAndCache(source []byte, path string, line int) (*Template, SourceError)

ParseTemplateAndCache is the same as ParseTemplateLocation, except that the source location is used for error reporting and for the {% include %} tag. If parsing is successful, provided source is then cached, and can be retrieved by {% include %} tags, as long as there is not a real file in the provided path.

The path and line number are used for error reporting. The path is also the reference for relative pathnames in the {% include %} tag.

func (*Engine) ParseTemplateLocation added in v1.0.0

func (e *Engine) ParseTemplateLocation(source []byte, path string, line int) (*Template, SourceError)

ParseTemplateLocation is the same as ParseTemplate, except that the source location is used for error reporting and for the {% include %} tag.

The path and line number are used for error reporting. The path is also the reference for relative pathnames in the {% include %} tag.

func (*Engine) RegisterBlock

func (e *Engine) RegisterBlock(name string, td Renderer)

RegisterBlock defines a block e.g. {% tag %}…{% endtag %}.

Example
engine := NewEngine()
engine.RegisterBlock("length", func(c render.Context) (string, error) {
	s, err := c.InnerString()
	if err != nil {
		return "", err
	}

	n := len(s)

	return strconv.Itoa(n), nil
})

template := `{% length %}abc{% endlength %}`

out, err := engine.ParseAndRenderString(template, emptyBindings)
if err != nil {
	log.Fatalln(err)
}

fmt.Println(out)
Output:
3

func (*Engine) RegisterFilter

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

RegisterFilter defines a Liquid filter, for use as `{{ value | my_filter }}` or `{{ value | my_filter: arg }}`.

A filter is a function that takes at least one input, and returns one or two outputs. If it returns two outputs, the second must have type error.

Examples:

* https://github.com/osteele/liquid/blob/main/filters/standard_filters.go

* https://github.com/osteele/gojekyll/blob/master/filters/filters.go

Example
engine := NewEngine()
engine.RegisterFilter("has_prefix", strings.HasPrefix)

template := `{{ title | has_prefix: "Intro" }}`
bindings := map[string]any{
	"title": "Introduction",
}

out, err := engine.ParseAndRenderString(template, bindings)
if err != nil {
	log.Fatalln(err)
}

fmt.Println(out)
Output:
true
Example (Optional_argument)
engine := NewEngine()
// func(a, b int) int) would default the second argument to zero.
// Then we can't tell the difference between {{ n | inc }} and
// {{ n | inc: 0 }}. A function in the parameter list has a special
// meaning as a default parameter.
engine.RegisterFilter("inc", func(a int, b func(int) int) int {
	return a + b(1)
})

template := `10 + 1 = {{ m | inc }}; 20 + 5 = {{ n | inc: 5 }}`
bindings := map[string]any{
	"m": 10,
	"n": "20",
}

out, err := engine.ParseAndRenderString(template, bindings)
if err != nil {
	log.Fatalln(err)
}

fmt.Println(out)
Output:
10 + 1 = 11; 20 + 5 = 25

func (*Engine) RegisterTag

func (e *Engine) RegisterTag(name string, td Renderer)

RegisterTag defines a tag e.g. {% tag %}.

Further examples are in https://github.com/osteele/gojekyll/blob/master/tags/tags.go

Example
engine := NewEngine()
engine.RegisterTag("echo", func(c render.Context) (string, error) {
	return c.TagArgs(), nil
})

template := `{% echo hello world %}`

out, err := engine.ParseAndRenderString(template, emptyBindings)
if err != nil {
	log.Fatalln(err)
}

fmt.Println(out)
Output:
hello world

func (*Engine) RegisterTemplateStore added in v1.7.0

func (e *Engine) RegisterTemplateStore(templateStore render.TemplateStore)

func (*Engine) SetAutoEscapeReplacer added in v1.8.0

func (e *Engine) SetAutoEscapeReplacer(replacer render.Replacer)

SetAutoEscapeReplacer enables auto-escape functionality where the output of expression blocks ({{ ... }}) is passed though a render.Replacer during rendering, unless it's been marked as safe by applying the 'safe' filter. This filter is automatically registered when this method is called. The filter must be applied last. A replacer is provided for escaping HTML (see render.HtmlEscaper).

func (*Engine) StrictVariables added in v1.3.1

func (e *Engine) StrictVariables()

StrictVariables causes the renderer to error when the template contains an undefined variable.

func (*Engine) UnregisterTag added in v1.8.0

func (e *Engine) UnregisterTag(name string)

UnregisterTag removes the named tag definition from the engine's configuration. After calling UnregisterTag the tag will no longer be recognized by subsequent parsing or rendering operations. The call is idempotent — unregistering a tag that is not registered is a no-op.

type Renderer

type Renderer func(render.Context) (string, error)

A Renderer returns the rendered string for a block. This is the type of a tag definition.

See the examples at Engine.RegisterTag and Engine.RegisterBlock.

type SourceError added in v0.2.0

type SourceError interface {
	error
	Cause() error
	Path() string
	LineNumber() int
}

SourceError records an error with a source location and optional cause.

SourceError does not depend on, but is compatible with, the causer interface of https://github.com/pkg/errors.

type Template

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

A Template is a compiled Liquid template. It knows how to evaluate itself within a variable binding environment, to create a rendered byte slice.

Use Engine.ParseTemplate to create a template.

func (*Template) FRender added in v1.4.0

func (t *Template) FRender(w io.Writer, vars Bindings) SourceError

FRender executes the template with the specified variable bindings and renders it into w.

func (*Template) GetRoot added in v1.3.0

func (t *Template) GetRoot() render.Node

GetRoot returns the root node of the abstract syntax tree (AST) representing the parsed template.

func (*Template) Render

func (t *Template) Render(vars Bindings) ([]byte, SourceError)

Render executes the template with the specified variable bindings.

func (*Template) RenderString

func (t *Template) RenderString(b Bindings) (string, SourceError)

RenderString is a convenience wrapper for Render, that has string input and output.

Directories

Path Synopsis
cmd
liquid command
Package main defines a command-line interface to the Liquid engine.
Package main defines a command-line interface to the Liquid engine.
Package evaluator is an interim internal package that forwards to package values.
Package evaluator is an interim internal package that forwards to package values.
Package expressions is an internal package that parses and evaluates the expression language.
Package expressions is an internal package that parses and evaluates the expression language.
Package filters is an internal package that defines the standard Liquid filters.
Package filters is an internal package that defines the standard Liquid filters.
Package parser is an internal package that parses template source into an abstract syntax tree.
Package parser is an internal package that parses template source into an abstract syntax tree.
Package render is an internal package that renders a compiled template parse tree.
Package render is an internal package that renders a compiled template parse tree.
Package tags is an internal package that defines the standard Liquid tags.
Package tags is an internal package that defines the standard Liquid tags.
Package values is an internal package that defines methods such as sorting, comparison, and type conversion, that apply to interface types.
Package values is an internal package that defines methods such as sorting, comparison, and type conversion, that apply to interface types.

Jump to

Keyboard shortcuts

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