mist

package module
v0.14.0 Latest Latest
Warning

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

Go to latest
Published: Oct 4, 2026 License: MIT Imports: 19 Imported by: 0

README

mist

CI

A single-pass renderer for a strict subset of Liquid, fast enough that it's cheap to try before falling back to a full engine.

mist evaluates as it scans: no tokenize, compile or AST step, and no allocations on the hot path. As soon as it meets anything outside the subset it returns ErrUnsupported, and you render with your full Liquid engine instead. For every template it accepts, its output matches liquidjs 10 (lenientIf: true) byte for byte.

out, err := mist.Render(tpl, vars, strict)
switch {
case errors.Is(err, mist.ErrUnsupported):
	// outside the subset: render with the full engine
case errors.Is(err, mist.ErrUndefined):
	// strict mode, undefined variable; the full engine fails too
}

Append(dst, tpl, vars, strict) does the same into a reusable buffer, with no allocations.

RenderChain renders a sequence of templates whose outputs feed later ones, such as snippets, then subject, then body, then layout. It stops at the first step mist can't handle, so only the remaining steps go to the full engine.

Custom tags and filters

Register inline tags and filters on an Engine; the package-level functions use an Engine with none. Registered filters override built-in ones. PassUnknownFilters passes filters liquidjs doesn't define through unchanged, as liquidjs does by default; leave it off unless Filters covers every filter your full engine has.

e := mist.Engine{Tags: map[string]mist.TagFunc{
	"link": func(dst []byte, t mist.Tag) ([]byte, error) {
		url, ok := t.Lookup(t.Args) // {% link product.url %}, resolved in scope
		if !ok {
			return nil, mist.ErrUnsupported // let the full engine handle it
		}
		return fmt.Appendf(dst, `<a href="%s">`, url), nil
	},
}}
out, err := e.Render(tpl, vars, strict)
e.Filters = map[string]mist.FilterFunc{
	"shout": func(f mist.Filter) (any, error) {
		s, ok := f.Input.(string)
		if !ok {
			return nil, mist.ErrUnsupported
		}
		return strings.ToUpper(s) + "!", nil
	},
}

Dialects

Engine.Dialect adapts mist to a Liquid engine that differs from liquidjs. It has hooks for how values print (Output) and compare (Compare), a set of constructs to reject, and a switch for default's leniency. Parity with the target engine is then up to the dialect's author; see SPEC.md § Dialects.

What's supported

Variables and paths ({{ a.b[0]['k'] }}, {{ a[key] }}), string literals with escapes, number/boolean/nil literals, if/elsif/else/unless, case/when, for … in with ranges, limit/offset/reversed, break/continue and forloop, assign, comment, raw, capture, comparisons including contains and == blank/== empty, and/or (with filters on conditions), whitespace control, and the default, capitalize, date, downcase, upcase, append, prepend, replace, replace_first, remove, remove_first, strip, lstrip, rstrip, truncate, truncatewords, escape, escape_once, url_encode, strip_newlines, newline_to_br, strip_html, json, split, slice, first, last, join, size, where, reject, map, sum, sort, sort_natural, find, find_index, has, push, concat, compact, uniq, reverse, replace_last, remove_last, url_decode, base64_encode, base64_decode, hmac_sha256, at_most, plus, minus, times, divided_by, modulo, abs, ceil, floor, round and at_least filters. SPEC.md is the normative grammar and semantics.

Check whether templates are in the subset:

go run github.com/hownowstephen/mist/cmd/mistcheck@latest template.liquid

-tags a,b and -filters a,b treat those names as registered custom tags and filters, and -pass-unknown-filters sets PassUnknownFilters. -stats finds every unsupported construct, not just the first, and summarizes how many templates each one blocks, and which sets of blockers most often appear together. That shows what to add next. .jsonl input holds one template per line as a JSON string:

mistcheck -stats templates.jsonl

Testing

Beyond unit tests, mist is diffed against liquidjs on every template from Shopify/liquid-spec and Shopify/liquid's test suite, plus grammar-generated templates (100k new ones nightly) and fuzzing. See SPEC.md § Verification.

License

MIT

Documentation

Overview

Package mist renders a strict subset of Liquid in a single pass, bailing with ErrUnsupported on anything outside the subset so the caller can fall back to a full engine. See SPEC.md for the exact subset.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnsupported means the template is outside the subset; render it with the full engine instead.
	ErrUnsupported = errors.New("mist: unsupported")
	// ErrUndefined is a strict-mode undefined variable. The full engine fails too, though a
	// syntax error later in the template may take precedence there.
	ErrUndefined = errors.New("mist: undefined variable")
)

Functions

func Append added in v0.2.0

func Append(dst []byte, tpl string, vars map[string]any, strict bool) ([]byte, error)

Append is Render appending to dst, so callers can reuse a buffer and render without allocating. On error out is nil.

func Check

func Check(tpl string) error

Check reports whether tpl is inside the subset, parsing every branch without data. Templates that pass can still bail at render time on data-dependent rules (see SPEC.md).

func Render

func Render(tpl string, vars map[string]any, strict bool) (string, error)

Render renders tpl with vars. strict makes undefined variables in output an error.

Types

type Constructs added in v0.5.0

type Constructs uint32

Constructs is a set of template constructs a Dialect can reject.

const (
	TrimMarkers       Constructs = 1 << iota // {{- -}} {%- -%}
	NegativeLiterals                         // -3, including negative indexes
	UnspacedOperators                        // a comparison operator with no whitespace before it: x==2
	RawBlocks                                // {% raw %}…{% endraw %}
	BlankKeyword                             // == blank, != blank
	EmptyKeyword                             // == empty, != empty
)

type Dialect added in v0.5.0

type Dialect struct {
	// Output appends a printed non-string value: a number, bool, array or object.
	// Strings, nil and undefined never reach it.
	Output func(dst []byte, v any) ([]byte, error)

	// Compare evaluates ==, !=, <, >, <=, >= in conditions.
	Compare func(op string, a, b any) (bool, error)

	// Reject makes these constructs bail, in both Render and Check.
	Reject Constructs

	// NoDefaultLeniency turns off liquidjs's rule that a leading `| default`
	// makes an undefined input lenient under strict.
	NoDefaultLeniency bool
}

Dialect changes how values print and compare, and which constructs mist accepts, so an application can target a Liquid engine that differs from liquidjs. With a dialect set, matching that engine is up to the dialect's author.

type Engine added in v0.3.0

type Engine struct {
	// Tags maps inline tag names, as in {% name args %}, to their implementations.
	// Built-in tag names can't be overridden.
	Tags map[string]TagFunc
	// Filters maps filter names, as in {{ x | name: arg }}, to their implementations.
	// They override built-in filters of the same name, as registerFilter does in liquidjs.
	Filters map[string]FilterFunc
	// Dialect, if set, changes how values print and compare and which constructs
	// bail. nil means liquidjs, the SPEC parity target.
	Dialect *Dialect
	// Now is the clock for the date filter's 'now' and 'today'. nil means time.Now.
	Now func() time.Time
	// PassUnknownFilters passes the input of a filter that is neither built in nor in
	// Filters through unchanged, after evaluating its arguments, as liquidjs does with
	// strictFilters off (its default). Leave it false, so such templates bail, unless
	// Filters already covers every filter the full engine defines.
	PassUnknownFilters bool
}

Engine renders templates with custom tags and filters. The zero value has none, and the package-level functions use it.

func (Engine) Append added in v0.3.0

func (e Engine) Append(dst []byte, tpl string, vars map[string]any, strict bool) (out []byte, err error)

Append is the package-level Append with e's custom tags.

func (Engine) Check added in v0.3.0

func (e Engine) Check(tpl string) (err error)

Check is the package-level Check, also accepting e's custom tags.

func (Engine) Render added in v0.3.0

func (e Engine) Render(tpl string, vars map[string]any, strict bool) (string, error)

Render is the package-level Render with e's custom tags.

func (Engine) RenderChain added in v0.3.0

func (e Engine) RenderChain(steps []Step, vars map[string]any) (res []Result, n int, hydrated map[string]any)

RenderChain is the package-level RenderChain with e's custom tags.

type Error

type Error struct {
	Kind error // ErrUnsupported or ErrUndefined
	Pos  int   // byte offset into the template
	Msg  string
}

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

type Filter added in v0.4.0

type Filter struct {
	Name   string
	Input  any   // nil for both nil and undefined
	Args   []any // positional arguments
	Strict bool
}

Filter is one application of a registered filter.

type FilterFunc added in v0.4.0

type FilterFunc func(f Filter) (any, error)

FilterFunc implements a filter. Returning an error that wraps ErrUnsupported hands the template to the full engine; any other error stops rendering.

type Keyword added in v0.5.0

type Keyword string

Keyword is how a keyword literal reaches Dialect.Compare.

const (
	Blank Keyword = "blank"
	Empty Keyword = "empty"
)

Blank and Empty are the blank and empty keywords as Dialect.Compare sees them.

type Result

type Result struct {
	Out string
	Err error // ErrUndefined or a custom tag's error; unsupported steps are never returned
}

func RenderChain

func RenderChain(steps []Step, vars map[string]any) (res []Result, n int, hydrated map[string]any)

RenderChain renders steps in order, binding each output into the vars seen by later steps (see SPEC.md § Chains). It stops at the first step mist can't render and returns n, its index; the caller renders steps[n:] with the full engine using the returned vars. The caller's maps are never mutated.

type Step

type Step struct {
	Body   string
	Key    []string // where the output is bound for later steps
	Strict bool
	Vars   map[string]any // replaces the chain vars for this step only
}

Step is one template in a RenderChain.

type Tag added in v0.3.0

type Tag struct {
	Name   string
	Args   string         // raw text after the name, trimmed
	Vars   map[string]any // the data passed to Render, without assigns or loop variables
	Strict bool
	// contains filtered or unexported fields
}

Tag is one use of a custom tag.

func (Tag) Lookup added in v0.3.0

func (t Tag) Lookup(path string) (v any, ok bool)

Lookup resolves a variable path such as `product.url` or `items[0]` in the tag's scope: loop variables, then assigns, then Vars. ok is false if it's undefined or not a valid path.

type TagFunc added in v0.3.0

type TagFunc func(dst []byte, t Tag) ([]byte, error)

TagFunc appends a custom tag's output to dst. Returning an error that wraps ErrUnsupported hands the template to the full engine; any other error stops rendering.

Directories

Path Synopsis
cmd
mistcheck command
Command mistcheck reports whether Liquid templates are inside the mist subset.
Command mistcheck reports whether Liquid templates are inside the mist subset.
scripts
gen command
Command gen writes random templates drawn from the SPEC.md grammar, with random data, in harvest.rb's format for oracle.mjs.
Command gen writes random templates drawn from the SPEC.md grammar, with random data, in harvest.rb's format for oracle.mjs.

Jump to

Keyboard shortcuts

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