Skip to content

Commit

Permalink
Migrate and update custom tag docs [skp ci]
Browse files Browse the repository at this point in the history
  • Loading branch information
jg-rp committed Mar 11, 2025
1 parent ea36778 commit ccad880
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 3 deletions.
1 change: 1 addition & 0 deletions docs/api/parser.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: liquid.parser.Parser
1 change: 1 addition & 0 deletions docs/api/undefined.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
::: liquid.StrictUndefined
::: liquid.DebugUndefined
::: liquid.StrictDefaultUndefined
::: liquid.FalsyStrictUndefined
133 changes: 133 additions & 0 deletions docs/custom_tags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
A Liquid tag is defined by a class extending [`Tag`](api/tag.md). It has just one abstract method, [`parse()`](api/tag.md#liquid.Tag.parse), which takes an instance of [`TokenStream`](api/tokens.md#liquid.TokenStream) and returns a [`Node`](api/ast.md#liquid.Node). The returned node will be added to a template's abstract syntax tree and, when rendered, its [`render_to_output()`](api/ast.md#liquid.Node.render_to_output) method will be called.

`render_to_output()` receives the active [render context](api/render_context.md) 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](https://github.com/jg-rp/liquid/tree/main/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

```python title="with_tag.py"
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`](api/parser.md) 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

```python title="with_tag.py (continued)"
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`](api/render_context.md#liquid.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`](api/environment.md#liquid.Environment.add_tag).

```python
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`](api/environment.md#liquid.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.

```python
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](environment.md) [`tags`](api/environment.md#liquid.Environment.tags) dictionary. The example removes the builtin `ifchanged` tag

```python
from liquid import Environment

env = Environment()
del env.tags["ifchanged"]

# ...
```

!!! tip

You can add, remove and replace tags on `liquid.DEFAULT_ENVIRONMENT` too. Convenience functions [`parse()`](api/convenience.md#liquid.parse) and [`render()`](api/convenience.md#liquid.render) use `DEFAULT_ENVIRONMENT`
51 changes: 50 additions & 1 deletion docs/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,53 @@ Together `{% if site_description %}` and `{% endif %}` form a _block tag_. Block

## Content

`<main>` and `\n <h3>About Us</h3>` are examples of template content. That's anything not inside `{%` and `%}` or `{{` and `}}`. With the exception of [whitespace control](whitespace_control.md), template content is output unchanged.
`<main>` and `\n <h3>About Us</h3>` are examples of template content. That's anything not inside `{%` and `%}` or `{{` and `}}`. With the exception of whitespace control, template content is output unchanged.

## Whitespace control

By default, all whitespace immediately before and after a tag is preserved. This can result in a lot of unwanted whitespace.

```liquid2
<ul>
{% for x in (1..4) %}
<li>{{ x }}</li>
{% endfor %}
</ul>
```

```plain title="output"
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
```

We can include a `-` at the start or end of a tag or output markup to strip preceding or trailing whitespace.

```liquid2
<ul>
{% for x in (1..4) -%}
<li>{{ x }}</li>
{% endfor -%}
</ul>
```

```plain title="output"
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
```

!!! note

Fine grained control over when to remove newlines vs indentation is not a standard feature of Liquid templates.
4 changes: 2 additions & 2 deletions docs/variables_and_drops.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ Hello

### Strict undefined

When [`StrictUndefined`](api/undefined.md#liquid.undefined.StrictUndefined) is passed as the `undefined` argument to an [`Environment`](api/environment.md), any operation on an undefined variable will raise an `UndefinedError`.
When [`StrictUndefined`](api/undefined.md#liquid.StrictUndefined) is passed as the `undefined` argument to an [`Environment`](api/environment.md), any operation on an undefined variable will raise an `UndefinedError`.

```python
from liquid import Environment, StrictUndefined
Expand All @@ -406,7 +406,7 @@ template.render()

### Falsy strict undefined

[`FalsyStrictUndefined`](api/undefined.md#liquid.undefined.FalsyStrictUndefined) is the same as [`StrictUndefined`](#strict-undefined), but can be tested for truthiness and equality without raising an exception.
[`FalsyStrictUndefined`](api/undefined.md#liquid.FalsyStrictUndefined) is the same as [`StrictUndefined`](#strict-undefined), but can be tested for truthiness and equality without raising an exception.

```python
from liquid import Environment
Expand Down
5 changes: 5 additions & 0 deletions liquid/extra/tags/_with.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

if TYPE_CHECKING:
from liquid.context import RenderContext
from liquid.expression import Expression
from liquid.stream import TokenStream

TAG_WITH = sys.intern("with")
Expand Down Expand Up @@ -56,6 +57,10 @@ def children(
"""Return this node's children."""
yield self.block

def expressions(self) -> Iterable[Expression]:
"""Return this node's expressions."""
yield from (arg.value for arg in self.args)

def block_scope(self) -> Iterable[Identifier]:
"""Return variables this node adds to the node's block scope."""
yield from (Identifier(p.name, token=p.token) for p in self.args)
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ nav:
- Exceptions: "api/exceptions.md"
- Builtin: "api/builtin.md"
- AST: "api/ast.md"
- Parser: "api/parser.md"
- Expression: "api/expression.md"
- Render context: "api/render_context.md"
- Filter helpers: "api/filter.md"
Expand Down

0 comments on commit ccad880

Please sign in to comment.