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 ¶
- Variables
- func Append(dst []byte, tpl string, vars map[string]any, strict bool) ([]byte, error)
- func Check(tpl string) error
- func Location(name string) (*time.Location, bool)
- func Render(tpl string, vars map[string]any, strict bool) (string, error)
- type Comparer
- type Constructs
- type Dialect
- type Engine
- func (e Engine) Append(dst []byte, tpl string, vars map[string]any, strict bool) (out []byte, err error)
- func (e Engine) Check(tpl string) (err error)
- func (e Engine) Render(tpl string, vars map[string]any, strict bool) (string, error)
- func (e Engine) RenderChain(steps []Step, vars map[string]any) (res []Result, n int, hydrated map[string]any)
- type Error
- type Filter
- type FilterFunc
- type Keyword
- type Limits
- type Result
- type Step
- type Tag
- type TagFunc
- type Undefined
Examples ¶
Constants ¶
This section is empty.
Variables ¶
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") // ErrInternal is a panic in mist or in a registered tag, filter or dialect hook, // recovered so the caller can fall back. It wraps ErrUnsupported. It means a bug, so // report it: Error.Stack says where it happened. ErrInternal = fmt.Errorf("%w: internal error", ErrUnsupported) )
Functions ¶
func Append ¶ added in v0.2.0
Append is Render appending to dst, so callers can reuse a buffer and render without allocating. On error out is nil.
func Check ¶
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 Location ¶ added in v0.37.0
Location is the IANA zone name as the date filter loads it, or false where Go can't load it or it's a name Go loads but JavaScript's Intl rejects (see ianaName). Locations are cached.
func Render ¶
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 Comparer ¶ added in v0.23.0
type Comparer interface {
// Compare evaluates v op other for ==, !=, <, >, <=, >=, with other as dialect
// hooks see values. When v is on the right, op is mirrored: a < v is v > a.
// Returning an error that wraps ErrUnsupported bails; any other stops rendering.
Compare(op string, other any) (bool, error)
}
Comparer is a value a registered filter or tag returns that compares itself in conditions, as a JavaScript object with its own semantics does in liquidjs (a decimal.js Decimal, say). Elsewhere mist treats it as any other unknown type.
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, object, or
// the Blank or Empty keyword. Strings, nil and undefined never reach it.
Output func(dst []byte, v any) ([]byte, error)
// Compare evaluates ==, !=, <, >, <=, >= in conditions, and matches when values
// against the case subject with ==.
Compare func(op string, a, b any) (bool, error)
// Contains evaluates a contains b in conditions. With Compare set and Contains
// nil, contains bails.
Contains func(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
// NumberText hands number literals to hooks and registered filters as
// json.Number holding their text, so an engine that distinguishes 2.0 from 2
// (Ruby's Float and Integer) sees which was written.
NumberText bool
// NoBuiltinFilters makes every filter that isn't in Engine.Filters bail,
// built-ins included, for an engine whose filters all differ from liquidjs's.
// A registered filter returning ErrBuiltin bails too.
NoBuiltinFilters bool
// StrictAssign makes an undefined variable in an assign's value ErrUndefined under
// strict, as Ruby Liquid's strict_variables does; liquidjs reads it leniently.
StrictAssign bool
// RubyLookups looks variables up as Ruby Liquid's Context#variable does: when the
// first segment is nil or false the whole lookup is that value, and past it a
// property or index of nil or false is undefined (ErrUndefined under strict).
RubyLookups 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
// Limits bound each render; zero fields take the defaults.
Limits Limits
}
Engine renders templates with custom tags and filters. The zero value has none, and the package-level functions use it. An Engine is safe for concurrent use as long as nothing modifies it or its maps while it renders.
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 configuration.
type Error ¶
type Error struct {
// Kind is ErrUnsupported, ErrUndefined, ErrInternal, or the error a registered tag,
// filter or dialect hook returned.
Kind error
// Code names the reason in a form that stays the same across releases, for metrics
// and grouping: "undefined", "internal", "tag:NAME" or "filter:NAME" for a tag or
// filter mist doesn't support, "hook:tag:NAME", "hook:filter:NAME" or
// "hook:dialect:output" for a hook's error, and otherwise a kebab-case reason such as
// "nesting-too-deep" (testdata/codes.txt lists them all). Msg is for people and may change.
Code string
Pos int // byte offset into the template
// Line and Col are Pos as liquidjs counts them: lines split on \n, columns in UTF-16
// code units, both from 1.
Line, Col int
Msg string
// Path, for ErrUndefined, is what liquidjs's error names: the keys up to and including
// the first undefined one, bracket indexes as their values' JavaScript strings, so its
// message is "undefined variable: " + strings.Join(Path, ".") + ", line:Line, col:Col".
// It's nil where mist can't say.
Path []string
Stack []byte // ErrInternal: the stack where the panic happened
}
Error is the error Render, Append and Check return. errors.Is matches its Kind.
type Filter ¶ added in v0.4.0
type Filter struct {
Name string
Input any // nil for both nil and undefined; inside arrays and objects, undefined is Undefined
Args []any // positional arguments
Strict bool
Now func() time.Time // the engine's clock: Engine.Now, or time.Now when that's nil
// contains filtered or unexported fields
}
Filter is one application of a registered filter.
func (Filter) ArgUndefined ¶ added in v0.39.0
ArgUndefined reports whether a nil Args[i] is JavaScript's undefined rather than null.
func (Filter) InputUndefined ¶ added in v0.39.0
InputUndefined reports whether a nil Input is JavaScript's undefined, such as a missing variable or a built-in filter's undefined result, rather than null.
type FilterFunc ¶ added in v0.4.0
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 and Dialect.Output.
type Limits ¶ added in v0.21.0
type Limits struct {
// Memory is a budget in liquidjs's memoryLimit units. Each filter and range spends at
// least what liquidjs charges it (the lengths of a filter's input and arguments, join's
// and slice's sizes, and a range's items), so with Memory at or below the full engine's
// memoryLimit, mist never renders a template that engine rejects. Default 10,000,000.
Memory int
// Output is the most bytes one render writes, captures included. Default 4 MiB.
Output int
// Iterations is the most for-loop iterations in one render. Default 1,000,000.
Iterations int
}
Limits bound one render. Going over one bails with ErrUnsupported, so the full engine, with its own limits, decides.
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 ¶
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
Source string // the tag as written, {% to %}; a {% liquid %} line has no delimiters
Vars map[string]any // the data passed to Render, without assigns or loop variables
Strict bool
Depth int // how many Tag.Render calls the tag runs inside: 0 in the template itself
// contains filtered or unexported fields
}
Tag is one use of a custom tag, valid only while its TagFunc runs.
func (Tag) Lookup ¶ added in v0.3.0
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; undefined items of an array or object it returns are Undefined.
func (Tag) Render ¶ added in v0.24.0
Render appends src rendered as Liquid in the tag's scope, as liquidjs renders a template with the caller's Context: src sees the tag's loop variables, assigns and Vars, and its assigns, counters and cycles persist after it. It shares the render's limits. Blocks src opens must close in it, and its break and continue can't reach the tag's loops. A TagFunc can return its error as is: an *Error stops the outer render with the same Kind and Code, at the tag. On error out is nil.
type TagFunc ¶ added in v0.3.0
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.
type Undefined ¶ added in v0.21.0
type Undefined struct{}
Undefined is JavaScript's undefined, a missing value, which liquidjs distinguishes from null: only undefined trips strict mode, and undefined != null under ==. Hooks see it inside arrays and objects mist builds, such as `map` of a missing property; at the top level they get nil for it instead.
Source Files
¶
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
|
|
|
fuzzcases
command
Command fuzzcases turns Go fuzz corpus files (testdata/fuzz, or the fuzz cache under $(go env GOCACHE)/fuzz) into oracle.mjs input, so what fuzzing found gets a liquidjs differential.
|
Command fuzzcases turns Go fuzz corpus files (testdata/fuzz, or the fuzz cache under $(go env GOCACHE)/fuzz) into oracle.mjs input, so what fuzzing found gets a liquidjs differential. |
|
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. |