API reference

Liquid.LiquidModule
Liquid

A pure-Julia implementation of the Liquid template language.

Templates are processed in three separate stages, each usable on its own: tokenize turns source into tokens, the parser turns tokens into an AST of Nodes, and the renderer walks that AST against a context object. Rendering never evaluates Julia code from template text.

source

Rendering

Liquid.renderFunction
render(io::IO, template::Template, data)

Render template into io. This is the primitive the other methods build on; use it to stream a large result instead of building a string.

source
render(template::Template, data) -> String
render(template::Template; kwargs...) -> String
render(source::AbstractString, data) -> String
render(source::AbstractString; kwargs...) -> String

Render a template and return the result.

data may be a NamedTuple, a Dict keyed by strings or symbols, or keyword arguments. Given a string, the template is parsed against default_environment and rendered once; parse it yourself with parse_template when it will be reused.

render("Hello, {{ name }}!"; name = "World")
render("{{ a.b }}", Dict("a" => Dict("b" => 1)))
source
Liquid.parse_templateFunction
parse_template(source; env = default_environment(), name = "") -> Template

Parse source into a reusable Template.

Parse once and render many times when a template is used more than once; parsing is the expensive half and rendering does not mutate the result.

tmpl = parse_template("Hello, {{ name }}!")
render(tmpl; name = "World")
render(tmpl; name = "Liquid")
source
Liquid.get_templateFunction
get_template(env, name) -> Template

Load and parse the template called name through env's loader.

env = Environment(loader = FileSystemLoader("./templates"))
tmpl = get_template(env, "letter.liquid")
source
Liquid.TemplateType
Template

A parsed template: its node list, the environment it was parsed against, and the name it was loaded under (empty for templates parsed from a string).

Parse once and render many times; rendering does not mutate the template.

source

Configuration

Liquid.EnvironmentType
Environment(; loader, autoescape, strict_variables, strict_filters)

Configuration shared by the templates parsed against it: which tags and filters exist, where templates are loaded from, and how strict rendering is.

Each environment owns its own tag and filter registries, copied from the built-in defaults at construction, so registering a tag in one environment never affects another.

  • loader: where get_template finds templates. Default NullLoader.
  • autoescape: HTML-escape the result of every output statement. Default false.
  • strict_variables: raise LiquidUndefinedError instead of rendering an undefined variable as the empty string. Default false.
  • strict_filters: raise instead of ignoring an unknown filter. Default false.
source
Liquid.to_globalsFunction
to_globals(data) -> Dict{String,Any}

Normalise the caller's data into the root scope.

Accepts a NamedTuple, a Dict keyed by String or Symbol, any other AbstractDict, or nothing at all. Keys become strings because that is what a template names them by.

source

Loaders

Liquid.NullLoaderType
NullLoader()

A loader that has no templates. The default, used when templates come from strings rather than from disk.

source
Liquid.FileSystemLoaderType
FileSystemLoader(root)

Load templates from files under root.

Names are resolved against root and checked to stay inside it, so a template name cannot reach outside the directory with ../.

source
Liquid.get_sourceFunction
get_source(loader, name) -> String

The source text of the template called name. Throws ArgumentError when the loader has no such template.

source

Exposing your own types

Liquid.liquid_propertiesFunction
liquid_properties(::Type{T}) -> Tuple{Vararg{Symbol}}

The fields of T that templates may read. Returns () by default, which makes values of T opaque: no field is reachable from a template.

Opt a type in by adding a method:

struct Product
    name::String
    price::Float64
    cost::Float64      # stays invisible to templates
end

Liquid.liquid_properties(::Type{Product}) = (:name, :price)

This is the Julia counterpart of Ruby Liquid's Drops. Only the symbols returned here are ever passed to getfield, so a hostile template cannot name a field that the type's author did not list.

See also liquid_get and @liquid_drop.

source
Liquid.liquid_getFunction
liquid_get(obj, key::AbstractString) -> Any

Read the property key from obj, or nothing when there is no such property.

The default implementation looks key up among liquid_properties and reads that field. Override it to expose computed properties:

function Liquid.liquid_get(p::Product, key::AbstractString)
    key == "discounted" && return p.price * 0.9
    return invoke(Liquid.liquid_get, Tuple{Any,AbstractString}, p, key)
end

Returning nothing for an unknown key is what keeps rendering lenient: a template that asks for a property that does not exist gets nil, not an error.

source
Liquid.@liquid_dropMacro
@liquid_drop T field...

Shorthand for a liquid_properties method.

@liquid_drop Product name price

expands to Liquid.liquid_properties(::Type{Product}) = (:name, :price). Use the function directly when the property list is computed rather than literal.

source

Errors

Liquid.LiquidErrorType
LiquidError <: Exception

Supertype of every error raised by Liquid.jl.

Liquid is a lenient language: rendering a template is not supposed to fail. Undefined variables render as the empty string, incompatible types are coerced rather than rejected, and nothing behaves like a false value. Errors are therefore concentrated in the parsing stage, where a malformed template can and should be reported with a precise location.

source
Liquid.LiquidSyntaxErrorType
LiquidSyntaxError(msg, line, col; template_name = "")

Raised when a template cannot be parsed. Carries the 1-based line and col of the offending construct so the message can point at it.

source
Liquid.LiquidArgumentErrorType
LiquidArgumentError(msg, line, col; template_name = "")

Raised at render time for the few operations Liquid does not forgive: an order comparison between incompatible types ({% if '2' > 1 %}), a filter called with the wrong number of arguments, or a loop modifier that is not a number.

Everything else is lenient by design; see LiquidError.

source
Liquid.LiquidUndefinedErrorType
LiquidUndefinedError(name, line, col; template_name = "")

Raised for an undefined variable, but only when the environment was built with strict_variables = true. By default an undefined variable renders as the empty string and no error is raised.

source

Value model

The rules that make rendering lenient. These are the functions to add methods to when your own type should behave a particular way in a template.

Liquid.is_truthyFunction
is_truthy(x) -> Bool

Liquid truthiness: only false and nil are false.

Everything else is true, including 0, the empty string, the empty array and the empty hash. Julia's nothing and missing both stand for Liquid's nil.

source
Liquid.to_liquid_stringFunction
to_liquid_string(x) -> String

Render x the way Liquid writes a value into the output.

nil renders as the empty string, booleans as "true"/"false", arrays as their elements concatenated with no separator, and a type the package does not know renders as the empty string rather than leaking its Julia representation. Add a method for your own type to change that.

source
Liquid.to_numberFunction
to_number(x) -> Union{Int,Float64,Nothing}

Coerce x to a number for arithmetic filters, or nothing when it cannot be read as one. Liquid parses a leading number out of a string and treats nil as zero-ish, which is why callers usually fall back to 0.

source
Liquid.is_blankFunction
is_blank(x) -> Bool

Whether x counts as Liquid's blank: nil, false, an empty or whitespace-only string, or an empty collection.

source
Liquid.is_emptyFunction
is_empty(x) -> Bool

Whether x counts as Liquid's empty: an empty string or an empty collection. Unlike is_blank, nil and false are not empty, and whitespace is not empty either.

source
Liquid.liquid_equalFunction
liquid_equal(a, b) -> Bool

Liquid's ==. Never raises: values of unrelated types are simply unequal.

nil equals nil (and missing), numbers compare across Int and Float, and the empty and blank keywords compare equal to any value that is empty or blank respectively.

source
Liquid.liquid_lessFunction
liquid_less(a, b) -> Bool

Liquid's <. Both operands must be numbers, or both strings; anything else raises, and the caller turns that into a LiquidArgumentError.

This is the one deliberate exception to the package's leniency, and it matches the reference implementation: {% if 'abc' < 'acb' %} is fine, {% if '2' > 1 %} is an error.

source
Liquid.liquid_containsFunction
liquid_contains(haystack, needle) -> Bool

Liquid's contains: substring search for strings, membership for arrays. Anything else, nil included, contains nothing.

source
Liquid.IncomparableValuesType
IncomparableValues(left, right)

Internal signal that an order comparison could not be made, because the two values are not both numbers or both strings.

evaluate catches it and rethrows a LiquidArgumentError carrying the template position, which the comparison itself does not know.

source
Liquid.EMPTYConstant
EMPTY

Liquid's empty keyword: equal to any empty string, array or mapping.

source
Liquid.BLANKConstant
BLANK

Liquid's blank keyword: equal to any empty or whitespace-only value.

source

Extension points

Tags

Liquid.TagDefType
TagDef(name, parse, inner = String[])

A registered tag.

  • parse is called as parse(p::Parser, tok::Token) with the parser positioned just after the tag token, and must return a Node or nothing (for a tag that produces no output, such as comment).
  • inner lists the tag names that belong to this tag's block and may not appear on their own, e.g. ["elsif", "else", "endif"] for if. It is used only to report a stray {% endif %} as an unexpected tag rather than an unknown one.
source
Liquid.register_tag!Function
register_tag!(tags::Dict{String,TagDef}, def::TagDef)

Add def to a tag registry, replacing any tag of the same name.

source
Liquid.default_tagsFunction
default_tags() -> Dict{String,TagDef}

A fresh registry holding the built-in tags. Each [Environment] gets its own copy, so registering a tag in one never affects another.

source
Liquid.parse_block!Function
parse_block!(p::Parser, stop) -> (nodes, stop_token)

Parse nodes until one of the tag names in stop is reached, without consuming that tag. This is what a block tag calls to read its body.

Reaching the end of the template first is a syntax error naming what was expected.

source
Liquid.parse_nodesFunction
parse_nodes(source, tags; template_name = "") -> Vector{Node}

Parse a whole template: tokenize source, apply whitespace control, and turn the result into a node list using the tag registry tags.

source
Liquid.peekFunction
peek(p, ahead = 0) -> Union{Token,Nothing}

The token ahead positions from the cursor without consuming it, or nothing past the end. Also defined for the expression parser, returning an ExprToken.

source
Liquid.advance!Function
advance!(p) -> Token

Consume and return the token at the cursor. Also defined for the expression parser, returning an ExprToken.

source

Filters

Liquid.register_filter!Function
register_filter!(filters::Dict{String,Any}, name, f)

Add f to a filter registry under name, replacing any filter already there.

source
Liquid.default_filtersFunction
default_filters() -> Dict{String,Any}

A fresh registry holding the built-in filters. Each Environment gets its own copy, so registering a filter in one never affects another.

source
Liquid.needs_contextFunction
needs_context(f) -> Bool

Whether the filter function f wants the render context as its first argument.

Defaults to false, so a filter is a plain function of its input. Opt in with Liquid.needs_context(::typeof(myfilter)) = true when a filter has to read the environment or the current scope.

source
Liquid.filter_errorFunction
filter_error(msg)

Raise a FilterError from inside a filter.

Use it when a filter is handed something it cannot work with. The template position is added by apply_filter, so the message should describe only what was wrong with the value.

source
Liquid.FilterErrorType
FilterError(msg)

Internal signal raised by a filter that was given something it cannot work with, such as {{ 10 | divided_by: 0 }}. apply_filter catches it and rethrows it as a LiquidArgumentError carrying the template position, which the filter itself has no way to know.

source
Liquid.apply_filterFunction
apply_filter(value, call::FilterCall, ctx::Context) -> Any

Look the filter up in the environment and call it.

An unknown filter is a no-op unless strict_filters is set. A filter called with arguments it does not accept raises LiquidArgumentError; that is where {{ "hello" | upcase: 5 }} is rejected.

source

Nodes and rendering

Liquid.NodeType
Node

Supertype of every node in a parsed template.

New node types can be added from outside the package: register a tag that returns one (see TagDef) and add a render_node method for it.

source
Liquid.render_nodeFunction
render_node(io, node, ctx)

Render one node. Add a method for your own Node type to make a custom tag renderable.

source
Liquid.render_nodesFunction
render_nodes(io, nodes, ctx)

Render a node list in order, stopping early if a break or continue is pending.

source
Liquid.render_blockFunction
render_block(io, nodes, ctx; blank = all_blank(nodes))

Render the body of a block tag, discarding the output when the body is blank.

The nodes are still rendered, into a throwaway buffer, because a blank body can carry side effects: {% if true %}{% assign x = 1 %} {% endif %} sets x.

source
Liquid.is_blank_nodeFunction
is_blank_node(node) -> Bool

Whether node can only ever produce whitespace.

Liquid drops the output of a block whose body is entirely blank, which is why {% if true %} {% endif %} renders nothing at all rather than two spaces. The rule is about the kind of node, not its rendered value: an output statement is never blank even when it renders empty, so {{ '' }} inside a block keeps that block's whitespace.

source
Liquid.ContextType
Context

Everything rendering needs besides the AST.

scopes is a stack, innermost last; scopes[1] holds the caller's data and is also where {% assign %} writes, which is why an assignment inside a loop outlives the loop. counters is a separate namespace for increment and decrement, so {% assign a = 5 %}{% increment a %} prints 0, not 5. interrupt carries a pending break or continue up to the enclosing loop. cycles remembers how far each {% cycle %} group has advanced, and loop_positions how far each named loop got, which is what offset: continue resumes from.

source
Liquid.evaluateFunction
evaluate(expr, ctx::Context) -> Any

Evaluate expr against ctx. Undefined names become nothing unless the environment sets strict_variables.

source
Liquid.to_iterableFunction
to_iterable(x)

The sequence a {% for %} loop walks.

Arrays and ranges iterate as themselves and a mapping iterates as [key, value] pairs. A non-empty string is a single item, not its characters, so {% for i in 'hello' %} runs once and {% for i in '' %} not at all. nil and numbers iterate as nothing at all, which sends the loop to its {% else %} body.

source
Liquid.escape_htmlFunction
escape_html(s) -> String

Escape the five characters that matter in HTML text and attributes. Used by autoescape and by the escape filter.

source

Stages

The three stages are usable on their own.

Lexer

Liquid.tokenizeFunction
tokenize(source; template_name = "") -> Vector{Token}

Split raw template source into text, output and tag tokens.

Whitespace control markers are recorded on the tokens but not yet applied; run apply_whitespace_control on the result for that. The body of a {% raw %} block is returned as a single TEXT token.

Throws LiquidSyntaxError for an unclosed delimiter, an unterminated string literal inside a delimiter, or a {% raw %} with no {% endraw %}.

source
Liquid.apply_whitespace_controlFunction
apply_whitespace_control(tokens) -> Vector{Token}

Apply the - whitespace markers recorded on tokens and drop the text tokens that become empty.

A trim_left marker strips trailing whitespace from the preceding text token, trim_right strips leading whitespace from the following one. Liquid removes all adjacent whitespace, newlines included, not just up to a line boundary.

source
Liquid.TokenType
Token

One top-level template token.

  • kind: see TokenKind.
  • value: for TEXT, the literal text; for OUTPUT and TAG, the whole content between the delimiters with surrounding whitespace stripped.
  • name: for TAG, the tag name ("if", "endfor"); empty otherwise.
  • trim_left, trim_right: whether the token was written with - on that side of its delimiter, e.g. {{- x -}}. These say what should happen to the neighbouring text, and are consumed by apply_whitespace_control.
  • line, col: 1-based position of value in the source.

line and col point at the first character of value, not at the opening delimiter, so an error reported against a token points at its content.

source
Liquid.tag_argsFunction
tag_args(tok::Token) -> (args, line, col)

The arguments of a tag token: everything after the tag name, with its own position so that errors inside the arguments are reported accurately.

source

Expressions

Liquid.tokenize_expressionFunction
tokenize_expression(src, line, col; template_name = "") -> Vector{ExprToken}

Tokenize src, the content of a delimiter, which starts at absolute position (line, col) in the template. Positions on the returned tokens are absolute, so errors can be reported against the original source.

Throws LiquidSyntaxError on an unterminated string or a character that cannot start a token.

source
Liquid.ExprTokenType
ExprToken

One token from inside a delimiter. value holds the text, except for STRING, where the surrounding quotes are removed. line and col are absolute positions in the original template.

source
Liquid.ExprTokenKindType
ExprTokenKind

Lexical class of an ExprToken.

IDENT covers keywords too (and, true, nil); the parser decides what they mean. OP covers the comparison operators written with symbols.

source
Liquid.ExpressionType
Expression

Supertype of every node in an expression tree. Expressions appear inside {{ ... }} and as the operands of tags; they are evaluated against a context at render time and never compiled to Julia code.

source
Liquid.VariableExprType
VariableExpr(path, line, col)

A variable lookup such as foo, foo.bar[0] or ["key"].

path is the chain of lookups, root first, each an Expression that evaluates to a key: foo.bar becomes [LiteralExpr("foo"), LiteralExpr("bar")] and a[b] becomes [LiteralExpr("a"), VariableExpr(...)]. Keeping the root in the path makes {{ ["foo"] }}, a lookup with a computed root, fall out for free instead of needing a special case.

source
Liquid.RangeExprType
RangeExpr(start, stop)

A range literal, (1..5). Both ends are evaluated and truncated to integers at render time, so (1.4..5) iterates from 1.

source
Liquid.CompareExprType
CompareExpr(op, left, right)

A comparison such as a == b or a contains b. Liquid does not chain comparisons, so the operands are always plain values.

source
Liquid.CompareOpType
CompareOp

The binary operators usable in a condition. != and its alias <> both map to NE.

source
Liquid.BooleanExprType
BooleanExpr(op, left, right)

left and right or left or right. op is :and or :or. Both operators share one precedence level and associate to the right.

source
Liquid.NotExprType
NotExpr(operand)

Logical negation. Liquid has no not operator; this node exists so that {% unless c %} can be parsed into the same shape as {% if %} with its first condition negated, instead of duplicating the whole conditional node.

source
Liquid.FilterCallType
FilterCall(name, args, kwargs, line, col)

One | name: arg, key: value step. Positional and keyword arguments may be interleaved in the source; they are separated here.

source
Liquid.FilteredExprType
FilteredExpr(expr, filters)

An expression followed by a filter chain: what appears inside {{ ... }} and on the right-hand side of assign and echo. filters is empty when there are none.

source
Liquid.parse_valueFunction
parse_value(src, line, col; template_name = "") -> Expression

Parse a single value with no filters and no comparison, such as the operand of {% increment %} or the right-hand side of a for ... in.

source
Liquid.parse_conditionFunction
parse_condition(src, line, col; template_name = "") -> Expression

Parse the condition of {% if %}, {% elsif %} or {% unless %}.

source
Liquid.parse_filteredFunction
parse_filtered(src, line, col; template_name = "") -> FilteredExpr

Parse an expression with an optional filter chain: the content of {{ ... }}, of {% echo %}, and of the right-hand side of {% assign %}.

source

AST

Liquid.TextNodeType
TextNode(text)

Literal template text, emitted verbatim. The body of {% raw %} also arrives as one of these.

source
Liquid.IfNodeType
IfNode(branches, else_body, line)

A conditional. branches[1] is the if itself and the rest are its elsif arms, each tested in order; else_body is empty when there is no else.

{% unless %} parses into this same node with its first condition wrapped in a NotExpr, so the renderer only ever sees one shape of conditional.

source
Liquid.ConditionalBranchType
ConditionalBranch(condition, body)

One if/elsif/unless arm: the condition to test and the nodes to render when it holds. Not a Node itself.

source
Liquid.CaseNodeType
CaseNode(subject, branches, line)

A {% case %} block.

branches is kept as one ordered list, when and else arms together, rather than split, because Liquid evaluates them in source order and does not stop at the first match: every when whose value matches renders, and an else renders whenever no when has matched so far. So {% case 'x' %}{% when 'y' %}a{% else %}b{% when 'x' %}c{% endcase %} renders "bc".

source
Liquid.CaseBranchType
CaseBranch(values, body)

One arm of a {% case %}: the values a {% when %} tests, or nothing for the {% else %} arm.

source
Liquid.ForNodeType
ForNode(varname, iterable, limit, offset, reversed, body, else_body, line)

A {% for %} loop.

limit and offset are expressions, not numbers, because Liquid allows {% for x in a limit: n %} where n is a variable; they are nothing when absent. continue_offset marks the special form offset: continue, which resumes where a previous loop over the same collection stopped. iterable_source is the collection as written, needed for the loop's name. else_body renders when the collection turns out to be empty.

source
Liquid.loop_nameFunction
loop_name(node::ForNode) -> String

The identifier Liquid gives a loop, variable-collection, as reported by {{ forloop.name }} and used to remember where offset: continue should resume.

source
Liquid.ContinueNodeType
ContinueNode(line)

{% continue %}: skip to the next iteration of the innermost enclosing loop.

source
Liquid.AssignNodeType
AssignNode(name, expr, line)

{% assign name = value | filters %}. Assignment writes to the outermost scope, so it outlives the block it appears in.

source
Liquid.CaptureNodeType
CaptureNode(name, body, line)

{% capture name %}...{% endcapture %}: render the body and bind the result to name instead of writing it out.

source
Liquid.EchoNodeType
EchoNode(expr, line)

{% echo value | filters %}, the tag form of an output statement. Unlike {{ }}, an empty {% echo %} is legal and renders nothing.

source
Liquid.CycleNodeType
CycleNode(group, items, key, line)

{% cycle 'a', 'b' %} or {% cycle group: 'a', 'b' %}.

group is an expression evaluated at render time, so {% cycle a: 1, 2 %} follows the current value of a; key is the fallback identity used when no group is named, derived from the argument list so that two cycle tags with different arguments advance independently.

source
Liquid.IncrementNodeType
IncrementNode(name, line)

{% increment name %}: print the counter, then add one. Counters live in their own namespace, separate from variables.

source
Liquid.DecrementNodeType
DecrementNode(name, line)

{% decrement name %}: subtract one from the counter, then print it. Note the order differs from increment, which is why one starts at 0 and the other at -1.

source

Index