mist

package module
v0.19.0 Latest Latest
Warning

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

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

README

mist

CI Go Reference OpenSSF Scorecard OpenSSF Best Practices

A single-pass Go renderer for most of Liquid, fast enough to try before falling back to a full engine.

mist evaluates as it scans, with no parse tree and no allocations on the hot path: about 10–80× faster than liquidjs on its benchmark templates. For every template it accepts, its output matches liquidjs 10 (lenientIf: true) byte for byte. Anything it can't render exactly returns ErrUnsupported, so you render that template with your full engine instead.

Use it in front of liquidjs (or an engine that behaves like it) when you render many templates and most of them are ordinary. It isn't a complete Liquid engine on its own.

Install

go get github.com/hownowstephen/mist

Requires Go 1.25 or newer.

Quickstart

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

vars is plain JSON-shaped data (map[string]any). mist.Append renders into a reusable buffer without allocating.

Will my templates render?

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

It reports the first construct each template uses that mist doesn't render; -stats summarizes a whole set. See docs/mistcheck.md.

Docs

docs/supported.md What renders, by tag, expression and filter, and what falls back
docs/extending.md Engine: custom tags and filters, chains of templates, dialects for other Liquid engines
docs/mistcheck.md The template checker
SPEC.md The normative grammar and semantics, and how parity is verified
pkg.go.dev API reference with runnable examples
CONTRIBUTING.md · AGENTS.md Working on mist, for people and coding agents
SECURITY.md Reporting vulnerabilities, verifying releases

Releases and their notes are on the releases page. Report bugs, including any output that differs from liquidjs, in issues.

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

Examples

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")
	// ErrBuiltin returned by a FilterFunc applies the built-in filter of the same name
	// to the same input and arguments, as if none were registered.
	ErrBuiltin = errors.New("mist: use the built-in filter")
	// ErrUnchanged returned by a FilterFunc leaves the filter's input as it was. Unlike
	// returning Filter.Input, it keeps an undefined input undefined rather than null.
	ErrUnchanged = errors.New("mist: leave the input unchanged")
)

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).

Example
package main

import (
	"errors"
	"fmt"

	"github.com/hownowstephen/mist"
)

func main() {
	fmt.Println(mist.Check("{% if a %}{{ a | upcase }}{% endif %}"))
	fmt.Println(errors.Is(mist.Check("{% tablerow x in xs %}{% endtablerow %}"), mist.ErrUnsupported))
}
Output:
<nil>
true

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.

Example
package main

import (
	"fmt"

	"github.com/hownowstephen/mist"
)

func main() {
	out, err := mist.Render("Hi {{ name | capitalize }}!", map[string]any{"name": "ada"}, true)
	fmt.Println(out, err)
}
Output:
Hi Ada! <nil>
Example (Fallback)

Templates outside the subset return ErrUnsupported; render those with the full engine.

package main

import (
	"errors"
	"fmt"

	"github.com/hownowstephen/mist"
)

func main() {
	_, err := mist.Render("{% tablerow x in xs %}{% endtablerow %}", nil, false)
	if errors.Is(err, mist.ErrUnsupported) {
		fmt.Println("render with the full engine")
	}
}
Output:
render with the full engine

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
)

The constructs a Dialect can reject.

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.

Example (CustomFilter)
package main

import (
	"fmt"
	"strings"

	"github.com/hownowstephen/mist"
)

func main() {
	e := mist.Engine{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
		},
	}}
	out, _ := e.Render("{{ greeting | shout }}", map[string]any{"greeting": "hello"}, true)
	fmt.Println(out)
}
Output:
HELLO!
Example (CustomTag)
package main

import (
	"fmt"

	"github.com/hownowstephen/mist"
)

func main() {
	e := mist.Engine{Tags: map[string]mist.TagFunc{
		"link": func(dst []byte, t mist.Tag) ([]byte, error) {
			url, ok := t.Lookup(t.Args)
			if !ok {
				return nil, mist.ErrUnsupported
			}
			return fmt.Appendf(dst, `<a href="%s">`, url), nil
		},
	}}
	out, _ := e.Render("{% for p in products %}{% link p.url %}{% endfor %}",
		map[string]any{"products": []any{map[string]any{"url": "/a"}, map[string]any{"url": "/b"}}}, true)
	fmt.Println(out)
}
Output:
<a href="/a"><a href="/b">

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
}

Error is the error Render, Append and Check return. errors.Is matches its Kind.

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, ErrBuiltin applies the built-in filter of the same name instead, and ErrUnchanged leaves the input as it was; 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
}

Result is one step's output in a RenderChain.

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