Custom tags
A Liquid tag is defined by a class extending Tag
. It has just one abstract method, parse()
, which takes an instance of TokenStream
and returns a Node
. The returned node will be added to a template's abstract syntax tree and, when rendered, its render_to_output()
method will be called.
render_to_output()
receives the active render context and an output buffer. It is responsible for either updating the render context or writing to the buffer, or both.
Tip
See liquid/builtin/tags for lots of examples.
Add a tag
This example implements the with
tag, which allows template authors to define block scoped variables. {% with %}
is a block tag. It has a start tag, an end tag ({% endwith %}
), and Liquid markup in between. We should ensure that we leave the closing tag token at the head of the stream.
The tag
from typing import TextIO
from liquid import BlockNode
from liquid import Node
from liquid import RenderContext
from liquid import Tag
from liquid import Token
from liquid import TokenStream
from liquid.builtin.expressions import KeywordArgument
from liquid.parser import get_parser
from liquid.token import TOKEN_EOF
from liquid.token import TOKEN_TAG
class WithTag(Tag):
name = "with"
block = True
def parse(self, stream: TokenStream) -> Node:
# Assert that the token at the head of the stream is a tag token
# and consume it.
token = stream.eat(TOKEN_TAG)
# Using the KeywordArgument.parse static method ensure consistent
# parsing behavior between tags.
args = KeywordArgument.parse(self.env, stream.into_inner(tag=token))
# Parse Liquid markup until we reach a tag called "endwith" or the end
# of the stream
block = get_parser(self.env).parse_block(stream, ("endwith", TOKEN_EOF))
# Assert that the token at the head of the stream is a tag token
# called "endwith".
stream.expect(TOKEN_TAG, value="endwith")
return WithNode(token, args, block)
When parse()
is called, the token at the head of the stream is guaranteed to be of type TOKEN_TAG
, representing the start of our tag. For block tags, like {% with %}
, we use TokenStream.eat()
to both assert the current token type and consume it.
{% with %}
expects one or more arguments in the form of key/value pairs. TokenStream.into_inner()
asserts that the next token is of type TOKEN_EXPRESSION
and returns a new stream of tokens over the tag's expression. Here we've passed it straight to KeywordArgument.parse()
because we only want to accepts key/value arguments in this example.
Next we get an instance of Parser
and use it to parse a block of Liquid markup until we reach a tag called "endwith". TokenStream.expect()
asserts that we did find an "endwith" tag an not reach the end of the stream.
The node
class WithNode(Node):
__slots__ = ("args", "block")
def __init__(self, token: Token, args: list[KeywordArgument], block: BlockNode):
super().__init__(token)
self.args = args
self.block = block
self.blank = self.block.blank
def render_to_output(self, context: RenderContext, buffer: TextIO) -> int:
namespace = dict(arg.evaluate(context) for arg in self.args)
with context.extend(namespace):
return self.block.render(context, buffer)
WithNode.render_to_output()
evaluates its arguments, extends the render context and renders its block to the output buffer. The RenderContext.extend
context manager is used to ensure the variables added by our tag go out of scope after the block has been rendered.
Usage
We can now register WithTag
with a Liquid environment using Environment.add_tag
.
from liquid import Environment
from .with_tag import WithTag
env = Environment()
env.add_tag(WithTag)
template = env.from_string(
"{% with greeting: 'Hello', name: 'Sally' -%}"
" {{ greeting }}, {{ name }}!"
"{%- endwith %}"
)
print(template.render()) # Hello, Sally
Replace a tag
Environment.add_tag
uses the name
class attribute of its argument when registering tags. If your custom tag has a name matching a builtin tag, add_tag()
will replace it without warning.
from liquid import Environment
from .my_tag import MyTag
env = Environment()
env.add_tag(MyTag)
# ...
Remove a tag
Remove a built-in tag by deleting it from your environment's tags
dictionary. The example removes the builtin ifchanged
tag