Skip to content

Commit

Permalink
More docs updates and migration
Browse files Browse the repository at this point in the history
  • Loading branch information
jg-rp committed Mar 11, 2025
1 parent ccad880 commit ff6681c
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 13 deletions.
139 changes: 139 additions & 0 deletions docs/custom_filters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
[Filters](tag_reference.md#filters) are usually implemented as simple Python functions. When rendered, Liquid will find the function in [`Environment.filters`](api/environment.md#liquid.Environment.filters), then call it, passing the input value as the first argument, followed by positional and keyword arguments given by the template author. The function's return value then becomes the result of the _filtered expression_.

Filters can actually be any Python [callable](https://docs.python.org/3/glossary.html#term-callable). Implementing a filter as a class with a `__call__` method or as a closure can be useful if you want to configure the filter before registering it with an [`Environment`](environment.md).

Also see the [filter helpers](api/filter.md) API documentation.

!!! tip

See [liquid/builtin/filters](https://github.com/jg-rp/liquid/tree/main/liquid/builtin/filters) for lots of examples.

## Add a filter

To add a filter, add an item to [`Environment.filters`](api/environment.md#liquid.Environment.filters). It's a regular dictionary mapping filter names to callables.

In this example we add `ends_with`, a filter that delegates to Python's `str.endswith`. The `@string_filter` decorator coerces the input value to a string, if it is not one already.

```python
from liquid import Environment
from liquid.filter import string_filter


@string_filter
def ends_with(left: str, val: str) -> bool:
return left.endswith(val)


env = Environment()
env.filters["ends_with"] = ends_with

source = """\
{% assign foo = "foobar" | ends_with: "bar" %}
{% if foo %}
do something
{% endif %}"""

template = env.from_string(source)
print(template.render())
```

### With context

Sometimes a filter will need access to the current [render context](render_context.md). Use the `@with_context` decorator to have an instance of [`RenderContext`](api/render_context.md) passed to your filter callable as a keyword argument named `context`.

Here we use the render context to resolve a variable called "handle".

```python
from liquid import Environment
from liquid.filter import string_filter
from liquid.filter import with_context


@string_filter
@with_context
def link_to_tag(label, tag, *, context):
handle = context.resolve("handle", default="")
return (
f'<a title="Show tag {tag}" href="/collections/{handle}/{tag}">{label}</a>'
)

class MyEnvironment(Environment):
def register_tags_and_filters(self):
super().register_tags_and_filters()
self.filters["link_to_tag"] = link_to_tag

env = MyEnvironment()
# ...
```

### With environment

Use the `@with_environment` decorator to have the current [`Environment`](api/environment.md) passed to your filter callable as a keyword argument named `environment`.

```python
import re

from markupsafe import Markup
from markupsafe import escape as markupsafe_escape

from liquid import Environment
from liquid.filter import string_filter
from liquid.filter import with_environment

RE_LINETERM = re.compile(r"\r?\n")


@with_environment
@string_filter
def strip_newlines(val: str, *, environment: Environment) -> str:
if environment.autoescape:
val = markupsafe_escape(val)
return Markup(RE_LINETERM.sub("", val))
return RE_LINETERM.sub("", val)

# ...
```

## Replace a filter

To replace a default filter implementation with your own, simply update the [`filters`](api/environment.md#liquid.Environment.filters) dictionary on your Liquid [Environment](environment.md).

Here we replace the default `slice` filter with one which uses start and stop values instead of start and length, and is a bit more forgiving in terms of allowed inputs.

```python
from liquid import Environment
from liquid.filter import int_arg
from liquid.filter import sequence_filter

@sequence_filter
def myslice(val, start, stop=None):
start = int_arg(start)

if stop is None:
return val[start]

stop = int_arg(stop)
return val[start:stop]


env = Environment()
env.filters["slice"] = myslice
# ...
```

## Remove a filter

Remove a built-in filter by deleting it from your [environment's](environment.md) [`filters`](api/environment.md#liquid.Environment.filters) dictionary.

```python
from liquid import Environment

env = Environment()
del env.filters["safe"]

# ...
```

!!! tip

You can add, remove and replace filters on `liquid.DEFAULT_ENVIRONMENT` too. Convenience functions [`parse()`](api/convenience.md#liquid.parse) and [`render()`](api/convenience.md#liquid.render) use `DEFAULT_ENVIRONMENT`
41 changes: 29 additions & 12 deletions docs/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,20 @@ class MyLiquidEnvironment(Environment):

## Tolerance

TODO:
Templates are parsed and rendered in strict mode by default. Where syntax and render-time type errors raise an exception as soon as possible. You can change the error tolerance mode with the `tolerance` argument to [`Environment`](api/environment.md).

Available modes are `Mode.STRICT`, `Mode.WARN` and `Mode.LAX`.

```python
from liquid import Environment
from liquid import FileSystemLoader
from liquid import Mode

env = Environment(
loader=FileSystemLoader("templates/"),
tolerance=Mode.LAX,
)
```

## HTML auto escape

Expand Down Expand Up @@ -243,23 +256,27 @@ template.render(you="something longer that exceeds our limit")
# liquid.exceptions.OutputStreamLimitError: output stream limit reached
```

### String sequences
## String sequences

By default, strings in Liquid can not be looped over with the `{% for %}` tag and characters in a string can not be selected by index.

Setting the `string_sequences` class attribute to `True` tells Python Liquid to treat strings as sequences, meaning we can loop over Unicode characters in a string for retrieve a Unicode "character" by its index.

TODO
## String first and last

### String first and last
Strings don't respond to the special `.first` and `.last` properties by default. Set `string_first_and_last` to `True` to enable `.first` and `.last` for strings.

TODO:
## Logical not operator

### Logical not operator
The logical `not` operator is disabled by default. Set the `logical_not_operator` class attribute to `True` to enable `not` inside `{% if %}`, `{% unless %}` and ternary expressions.

TODO:
## Logical parentheses

### Logical parentheses
By default, terms in `{% if %}` tag expressions can not be grouped to control precedence. Set the `logical_parentheses` class attribute to `True` to enable grouping terms with parentheses.

TODO:
## Ternary expressions

### Ternary expressions
Enable ternary expressions in output statements, assign tags and echo tags by setting the `ternary_expressions` class attribute to `True`.

```
{{ <expression> if <expression> else <expression> }}
Expand All @@ -283,9 +300,9 @@ Or applied to the result of the conditional expression as a whole using _tail fi
{{ "bar" if x else "baz" || upcase | append: "!" }}
```

### Keyword assignment
## Keyword assignment

TODO:
By default, named arguments must separated names from values with a colon (`:`). Set the `keyword_assignment` class attribute to `True` to allow equals (`=`) or a colon between names and their values.

## What's next?

Expand Down
76 changes: 76 additions & 0 deletions docs/known_issues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
This page documents known compatibility issues between Python Liquid's default [`Environment`](api/environment.md) and [Shopify/liquid](https://shopify.github.io/liquid/), the reference implementation written in Ruby. We strive to be 100% compatible with Shopify/liquid. That is, given an equivalent render context, a template rendered with Python Liquid should produce the same output as when rendered with Ruby Liquid.

## Coercing Strings to Integers Inside Filters

**_See issue [#49](https://github.com/jg-rp/liquid/issues/49)_**

Many filters built in to Liquid will automatically convert a string representation of a number to an integer or float as needed. When converting integers, Ruby Liquid uses [Ruby's String.to_i method](https://ruby-doc.org/core-3.1.1/String.html#method-i-to_i), which will disregard trailing non-digit characters. In the following example, `'7,42'` is converted to `7`

**template:**

```liquid
{{ 3.14 | plus: '7,42' }}
{{ '123abcdef45' | plus: '1,,,,..!@qwerty' }}
```

**output**

```plain
10.14
124
```

Python Liquid currently falls back to `0` for any string that can't be converted to an integer in its entirety. As is the case in Ruby Liquid for strings without leading digits.

This does not apply to parsing of integer literals, only converting strings to integers (not floats) inside filters.

## The Date Filter

The built-in [`date`](filter_reference.md#date) filter uses [dateutil](https://dateutil.readthedocs.io/en/stable/) for parsing strings to `datetime`s, and `strftime` for formatting. There are likely to be some inconsistencies between this and the reference implementation's equivalent parsing and formatting of dates and times.

## Orphaned `{% break %}` and `{% continue %}`

**_See issue [#76](https://github.com/jg-rp/liquid/issues/76)_**

Shopify/liquid shows some unintuitive behavior when `{% break %}` or `{% continue %}` are found outside a `{% for %}` tag block.

```liquid
{%- if true -%}
before
{%- if true %}
hello{% break %}goodbye
{% endif -%}
after
{%- endif -%}
{% for x in (1..3) %}
{{ x }}
{% endfor %}
{% for x in (1..3) %}
{{ x }}
{% endfor %}
```

Shopify/iquid output in both strict and lax modes:

```plain
before
hello
```

Python Liquid raises a `LiquidSyntaxError` in strict mode and jumps over the entire outer `{% if %}` block in lax mode.

```plain
1
2
3
1
2
3
```
2 changes: 2 additions & 0 deletions docs/optional_filters.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
TODO: examples of registering these filters

## currency

```
Expand Down
4 changes: 3 additions & 1 deletion docs/optional_tags.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
TODO: examples of registering these tags

## extends

```
Expand Down Expand Up @@ -90,7 +92,7 @@ In this example we use `{{ block.super }}` in the `footer` block to output the b

The `macro` tag defines a parameterized block that can later be called using the `call` tag.

A macro is like defining a function. You define a parameter list, possibly with default values, that are expected to be provided by a `call` tag. A macro tag's block has its own scope including its arguments and template global variables, just like the [`render`](#render) tag.
A macro is like defining a function. You define a parameter list, possibly with default values, that are expected to be provided by a `call` tag. A macro tag's block has its own scope including its arguments and template global variables, just like the [`render`](tag_reference.md#render) tag.

Note that argument defaults are bound late. They are evaluated when a call expression is evaluated, not when the macro is defined.

Expand Down

0 comments on commit ff6681c

Please sign in to comment.