liquid

package module
v0.0.0-...-2b7bb06 Latest Latest
Warning

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

Go to latest
Published: Jul 27, 2026 License: BSD-3-Clause Imports: 7 Imported by: 0

README

go-ruby-liquid/liquid

liquid — go-ruby-liquid

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of Shopify's Liquid template engine — the deterministic, interpreter-independent core of MRI's Liquid::Template.parse(src).render(assigns). It parses a Liquid template and renders it against a tree of Go values, matching the gem's output byte-for-byte across the standard tag and filter set — without any Ruby runtime.

It is the Liquid backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-regexp (the Onigmo engine), go-ruby-erb (the ERB compiler) and go-ruby-yaml (the Psych emitter/loader).

What it is — and isn't. Parsing and rendering the Liquid markup language (tags, filters, the variable/lookup grammar, truthiness, operators, whitespace control, lax/strict/warn error modes) is fully deterministic and needs no interpreter, so it lives here as pure Go. Templates render against a small, explicit value model — Go scalars, []any, map[string]any, and any type implementing the Drop interface — that the host maps to and from its own objects.

Features

Faithful port of Liquid's parser and renderer, validated against the liquid gem on every supported platform:

  • Tagsif / elsif / else, unless, case / when / else, for (with limit:, offset: / offset:continue, reversed, ranges, forloop drop, else, and break / continue), tablerow (with cols: / limit: / offset: and the tablerowloop drop), assign, capture, increment / decrement, cycle (named groups), raw, and comment.
  • Filters — the complete standard set: upcase downcase capitalize strip lstrip rstrip strip_newlines newline_to_br escape escape_once url_encode url_decode strip_html append prepend replace replace_first remove remove_first truncate truncatewords slice split join first last concat map where sort sort_natural uniq reverse size compact plus minus times divided_by modulo round ceil floor abs at_least at_most default, and date (strftime).
  • Whitespace control{%- / -%} and {{- / -}} strip the adjacent text run exactly as the gem does.
  • Variable lookup — dotted (a.b.c), bracketed (a[0], a["k"], a[var]), the size / first / last pseudo-properties, negative array indexing, and the Drop interface for host objects.
  • Operators== != < > <= >= contains, plus and / or with Liquid's right-to-left, no-parentheses precedence.
  • Literals — strings, numbers, true / false / nil / empty / blank, and (a..b) ranges.
  • Truthiness — only nil and false are falsy (everything else, including 0 and "", is truthy), matching Ruby/Liquid semantics.
  • Error modesLax (inline Liquid error: …, never fails), Warn (lax plus collected errors), and Strict (render! — first error returned).

CGO-free, dependency-free, 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x) and three OSes (Linux, macOS, Windows).

Install

go get github.com/go-ruby-liquid/liquid

Usage

package main

import (
	"fmt"

	"github.com/go-ruby-liquid/liquid"
)

func main() {
	tmpl, err := liquid.Parse(`{% for p in products %}{{ p.name | upcase }}: {{ p.price | times: 2 }}
{% endfor %}`)
	if err != nil {
		panic(err)
	}

	out, _ := tmpl.Render(map[string]any{
		"products": []any{
			map[string]any{"name": "shirt", "price": 10},
			map[string]any{"name": "hat", "price": 5},
		},
	})
	fmt.Print(out)
	// SHIRT: 20
	// HAT: 10
}

API

// Parse compiles a template (Liquid::Template.parse). In Lax/Warn mode parse
// never fails; in Strict mode a syntax error is returned.
func Parse(src string, opts ...Option) (*Template, error)

// MustParse is Parse but panics on error (convenient for static templates).
func MustParse(src string, opts ...Option) *Template

// Render renders against assigns in Lax mode (Liquid::Template#render).
func (t *Template) Render(assigns map[string]any) (string, error)

// RenderStrict renders like the gem's render!: the first error is returned.
func (t *Template) RenderStrict(assigns map[string]any) (string, error)

// Errors returns the errors collected in Warn mode.
func (t *Template) Errors() []error

type ErrorMode int
const (
	Lax    ErrorMode = iota // inline "Liquid error: …", never fails
	Warn                    // lax, but collects errors on the template
	Strict                  // render!: first error returned
)

func WithErrorMode(m ErrorMode) Option // Liquid error_mode:

// Drop lets a host object expose lookups to the template (Liquid::Drop); the
// bool reports whether the key resolved.
type Drop interface {
	LiquidGet(name string) (any, bool)
}

Tests & coverage

The suite pairs deterministic, ruby-free tests (which alone hold coverage at 100%, so the qemu cross-arch and Windows lanes pass the gate) with a differential gem oracle: a wide corpus of templates is rendered here and by the system liquid gem and the outputs compared byte-for-byte — across tags, filters, ranges, forloop, whitespace control, operators, and error rendering. The oracle script $stdout.binmodes so Windows text-mode never pollutes the bytes, and skips itself where ruby / the liquid gem is absent.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-liquid/liquid authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package liquid is a pure-Go (CGO-free) reimplementation of Shopify's Ruby Liquid template engine. It parses a Liquid source template and renders it against a set of assigns, matching the rendered output of the `liquid` gem.

The entry point mirrors the gem's API:

tmpl, err := liquid.Parse(src)        // Liquid::Template.parse(src)
out, err  := tmpl.Render(assigns)     // tmpl.render(assigns)
out, err  := tmpl.RenderStrict(assigns) // tmpl.render!(assigns) — raises

Value model

Assigns are an ordinary Go value tree; the engine accepts and produces the small, fixed set of Go types a host (such as go-embedded-ruby) maps to and from its own object graph:

Ruby            Go
----            --
nil             nil
true / false    bool
Integer         int, int64
Float           float64
String          string
Array           []any
Hash            map[string]any
Drop            Drop (LiquidValue / context-aware lookups)

Render walks the parsed tree and writes a string, so a host binds object.Value to and from these Go shapes.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Drop

type Drop interface {
	LiquidGet(name string) (any, bool)
}

Drop is the interface a host object may implement to expose context-aware member lookups to templates (the gem's Liquid::Drop). LiquidGet returns the value for a member name and whether it is defined.

type Error

type Error struct {
	Type    string
	Message string
}

Error is a Liquid error. Type names the gem error class (SyntaxError, ArgumentError, ZeroDivisionError, …) and Message is the rendered text. In Lax mode a runtime Error renders inline as "Liquid error: <Message>".

func (*Error) Error

func (e *Error) Error() string

type ErrorMode

type ErrorMode int

ErrorMode selects how parse and render errors are surfaced.

const (
	// Lax swallows recoverable errors, rendering an inline "Liquid error: …"
	// message in their place (the gem's default at render time).
	Lax ErrorMode = iota
	// Warn behaves like Lax but also collects the errors on the template.
	Warn
	// Strict turns recoverable parse/render errors into a returned error.
	Strict
)

type Filter

type Filter func(input any, args []any) (any, error)

Filter is a host-supplied filter implementation. It receives the piped input value and the already-evaluated positional arguments, and returns the transformed value. Returning a non-nil error surfaces through the active ErrorMode exactly like a built-in filter failure.

Custom filters let a host (for example a Jekyll front-end) extend the filter vocabulary — relative_url, jsonify, slugify, … — without forking the engine. A registered name takes precedence over a built-in of the same name.

type Option

type Option func(*parseConfig)

Option configures Parse.

func WithErrorMode

func WithErrorMode(m ErrorMode) Option

WithErrorMode selects the parse/render error mode (default Lax).

func WithFilter

func WithFilter(name string, fn Filter) Option

WithFilter registers a single custom Filter under name. It may be passed to Parse more than once and combines with WithFilters.

func WithFilters

func WithFilters(m map[string]Filter) Option

WithFilters registers a set of custom filters in one call. Later registrations (including a subsequent WithFilter) override earlier ones for the same name.

type Template

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

Template is a parsed Liquid template ready to render.

func MustParse

func MustParse(src string, opts ...Option) *Template

MustParse is Parse without error handling, for tests and trusted templates.

func Parse

func Parse(src string, opts ...Option) (*Template, error)

Parse compiles a Liquid source template. It mirrors Liquid::Template.parse(src). A syntax error is returned only in Strict mode; in Lax/Warn mode parse never fails and malformed constructs render as inline errors.

func (*Template) Errors

func (t *Template) Errors() []error

Errors returns the errors collected during the most recent Render (Lax/Warn mode), mirroring template.errors in the gem.

func (*Template) Render

func (t *Template) Render(assigns map[string]any) (string, error)

Render renders the template against assigns in Lax mode: recoverable runtime errors are written inline as "Liquid error: …" and the returned error is nil.

func (*Template) RenderStrict

func (t *Template) RenderStrict(assigns map[string]any) (string, error)

RenderStrict renders the template like the gem's render!: the first recoverable runtime error stops rendering and is returned.

Jump to

Keyboard shortcuts

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