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 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 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.
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
// 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 custom tags.
func (Engine) Check ¶ added in v0.3.0
Check is the package-level Check, also accepting e's custom tags.
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
Msg 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
}
Filter is one application of a registered filter.
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 spends at least what
// liquidjs charges it (the lengths of its input and arguments, and join's and slice's
// sizes), 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
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, valid only while its TagFunc runs.
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
|
|
|
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. |