Extending
Tags and filters live in registries owned by an Environment, copied from the built-in defaults when it is constructed. Registering into one environment never affects another.
Custom filters
A filter is an ordinary Julia function called as f(input, args...; kwargs...). There is nothing to subclass and no wrapper to write.
julia> using Liquid
julia> shout(input) = uppercase(string(input)) * "!";
julia> env = Environment();
julia> Liquid.register_filter!(env.filters, "shout", shout);
julia> render(parse_template("{{ name | shout }}"; env); name = "world")
"WORLD!"Arguments from the template arrive as ordinary arguments:
julia> repeat_filter(input, n) = repeat(string(input), Int(n));
julia> Liquid.register_filter!(env.filters, "repeat", repeat_filter);
julia> render(parse_template("{{ 'ab' | repeat: 3 }}"; env))
"ababab"A filter that needs the render context declares it, and is then called as f(ctx, input, args...):
Liquid.needs_context(::typeof(myfilter)) = trueRejecting an input is done with filter_error; the position in the template is added for you, since the filter has no way to know it.
Calling a filter with arguments it does not accept is an error. An unknown filter is a no-op by default, and an error when the environment sets strict_filters = true.
Custom tags
A tag is a TagDef: a name, a parse function, and the names of the tags that belong to its block. The parse function receives the Parser positioned just after the tag token and returns a Node.
Give the node a render_node method and it is renderable.
julia> using Liquid
julia> struct ShoutNode <: Liquid.Node
body::Vector{Liquid.Node}
end
julia> function parse_shout(p::Liquid.Parser, tok::Liquid.Token)
body, _ = Liquid.parse_block!(p, ["endshout"])
return ShoutNode(body)
end;
julia> function Liquid.render_node(io::IO, node::ShoutNode, ctx::Liquid.Context)
inner = IOBuffer()
Liquid.render_nodes(inner, node.body, ctx)
print(io, uppercase(String(take!(inner))))
end;
julia> env = Environment();
julia> Liquid.register_tag!(env.tags,
Liquid.TagDef("shout", parse_shout; inner = ["endshout"]));
julia> render(parse_template("{% shout %}hello {{ name }}{% endshout %}"; env);
name = "world")
"HELLO WORLD"The inner list is what lets the parser report a stray {% endshout %} as an unexpected tag rather than an unknown one.
What a parse function may use
parse_block! reads a body up to a closing tag, tag_args gives the tag's arguments with their position, and syntax_error reports a problem against the template.
For tags that take operands there are three expression entry points: parse_value for a bare value, parse_condition for an if-style condition, and parse_filtered for a value with a filter chain.
Blank nodes
A block whose body can only produce whitespace has its output dropped, so {% if true %} {% endif %} renders nothing. A node type from outside the package is assumed to produce output; give it an is_blank_node method returning true if it does not.
Custom loaders
Subtype AbstractLoader and implement get_source:
struct DictLoader <: Liquid.AbstractLoader
templates::Dict{String,String}
end
function Liquid.get_source(loader::DictLoader, name::AbstractString)
haskey(loader.templates, name) ||
throw(ArgumentError("template not found: $(repr(name))"))
return loader.templates[name]
endFileSystemLoader checks that a resolved name stays inside its root, so a template name cannot escape the directory with ../. A loader of your own that touches the filesystem should do the same.