Skip to content

Commit ff6681c

Browse files
committed
More docs updates and migration
1 parent ccad880 commit ff6681c

File tree

5 files changed

+249
-13
lines changed

5 files changed

+249
-13
lines changed

docs/custom_filters.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
[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_.
2+
3+
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).
4+
5+
Also see the [filter helpers](api/filter.md) API documentation.
6+
7+
!!! tip
8+
9+
See [liquid/builtin/filters](https://github.com/jg-rp/liquid/tree/main/liquid/builtin/filters) for lots of examples.
10+
11+
## Add a filter
12+
13+
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.
14+
15+
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.
16+
17+
```python
18+
from liquid import Environment
19+
from liquid.filter import string_filter
20+
21+
22+
@string_filter
23+
def ends_with(left: str, val: str) -> bool:
24+
return left.endswith(val)
25+
26+
27+
env = Environment()
28+
env.filters["ends_with"] = ends_with
29+
30+
source = """\
31+
{% assign foo = "foobar" | ends_with: "bar" %}
32+
{% if foo %}
33+
do something
34+
{% endif %}"""
35+
36+
template = env.from_string(source)
37+
print(template.render())
38+
```
39+
40+
### With context
41+
42+
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`.
43+
44+
Here we use the render context to resolve a variable called "handle".
45+
46+
```python
47+
from liquid import Environment
48+
from liquid.filter import string_filter
49+
from liquid.filter import with_context
50+
51+
52+
@string_filter
53+
@with_context
54+
def link_to_tag(label, tag, *, context):
55+
handle = context.resolve("handle", default="")
56+
return (
57+
f'<a title="Show tag {tag}" href="/collections/{handle}/{tag}">{label}</a>'
58+
)
59+
60+
class MyEnvironment(Environment):
61+
def register_tags_and_filters(self):
62+
super().register_tags_and_filters()
63+
self.filters["link_to_tag"] = link_to_tag
64+
65+
env = MyEnvironment()
66+
# ...
67+
```
68+
69+
### With environment
70+
71+
Use the `@with_environment` decorator to have the current [`Environment`](api/environment.md) passed to your filter callable as a keyword argument named `environment`.
72+
73+
```python
74+
import re
75+
76+
from markupsafe import Markup
77+
from markupsafe import escape as markupsafe_escape
78+
79+
from liquid import Environment
80+
from liquid.filter import string_filter
81+
from liquid.filter import with_environment
82+
83+
RE_LINETERM = re.compile(r"\r?\n")
84+
85+
86+
@with_environment
87+
@string_filter
88+
def strip_newlines(val: str, *, environment: Environment) -> str:
89+
if environment.autoescape:
90+
val = markupsafe_escape(val)
91+
return Markup(RE_LINETERM.sub("", val))
92+
return RE_LINETERM.sub("", val)
93+
94+
# ...
95+
```
96+
97+
## Replace a filter
98+
99+
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).
100+
101+
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.
102+
103+
```python
104+
from liquid import Environment
105+
from liquid.filter import int_arg
106+
from liquid.filter import sequence_filter
107+
108+
@sequence_filter
109+
def myslice(val, start, stop=None):
110+
start = int_arg(start)
111+
112+
if stop is None:
113+
return val[start]
114+
115+
stop = int_arg(stop)
116+
return val[start:stop]
117+
118+
119+
env = Environment()
120+
env.filters["slice"] = myslice
121+
# ...
122+
```
123+
124+
## Remove a filter
125+
126+
Remove a built-in filter by deleting it from your [environment's](environment.md) [`filters`](api/environment.md#liquid.Environment.filters) dictionary.
127+
128+
```python
129+
from liquid import Environment
130+
131+
env = Environment()
132+
del env.filters["safe"]
133+
134+
# ...
135+
```
136+
137+
!!! tip
138+
139+
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`

docs/environment.md

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,20 @@ class MyLiquidEnvironment(Environment):
9090

9191
## Tolerance
9292

93-
TODO:
93+
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).
94+
95+
Available modes are `Mode.STRICT`, `Mode.WARN` and `Mode.LAX`.
96+
97+
```python
98+
from liquid import Environment
99+
from liquid import FileSystemLoader
100+
from liquid import Mode
101+
102+
env = Environment(
103+
loader=FileSystemLoader("templates/"),
104+
tolerance=Mode.LAX,
105+
)
106+
```
94107

95108
## HTML auto escape
96109

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

246-
### String sequences
259+
## String sequences
260+
261+
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.
262+
263+
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.
247264

248-
TODO
265+
## String first and last
249266

250-
### String first and last
267+
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.
251268

252-
TODO:
269+
## Logical not operator
253270

254-
### Logical not operator
271+
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.
255272

256-
TODO:
273+
## Logical parentheses
257274

258-
### Logical parentheses
275+
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.
259276

260-
TODO:
277+
## Ternary expressions
261278

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

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

286-
### Keyword assignment
303+
## Keyword assignment
287304

288-
TODO:
305+
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.
289306

290307
## What's next?
291308

docs/known_issues.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
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.
2+
3+
## Coercing Strings to Integers Inside Filters
4+
5+
**_See issue [#49](https://github.com/jg-rp/liquid/issues/49)_**
6+
7+
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`
8+
9+
**template:**
10+
11+
```liquid
12+
{{ 3.14 | plus: '7,42' }}
13+
{{ '123abcdef45' | plus: '1,,,,..!@qwerty' }}
14+
```
15+
16+
**output**
17+
18+
```plain
19+
10.14
20+
124
21+
```
22+
23+
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.
24+
25+
This does not apply to parsing of integer literals, only converting strings to integers (not floats) inside filters.
26+
27+
## The Date Filter
28+
29+
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.
30+
31+
## Orphaned `{% break %}` and `{% continue %}`
32+
33+
**_See issue [#76](https://github.com/jg-rp/liquid/issues/76)_**
34+
35+
Shopify/liquid shows some unintuitive behavior when `{% break %}` or `{% continue %}` are found outside a `{% for %}` tag block.
36+
37+
```liquid
38+
{%- if true -%}
39+
before
40+
{%- if true %}
41+
hello{% break %}goodbye
42+
{% endif -%}
43+
after
44+
{%- endif -%}
45+
{% for x in (1..3) %}
46+
{{ x }}
47+
{% endfor %}
48+
{% for x in (1..3) %}
49+
{{ x }}
50+
{% endfor %}
51+
```
52+
53+
Shopify/iquid output in both strict and lax modes:
54+
55+
```plain
56+
before
57+
hello
58+
```
59+
60+
Python Liquid raises a `LiquidSyntaxError` in strict mode and jumps over the entire outer `{% if %}` block in lax mode.
61+
62+
```plain
63+
1
64+
65+
2
66+
67+
3
68+
69+
70+
1
71+
72+
2
73+
74+
3
75+
76+
```

docs/optional_filters.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
TODO: examples of registering these filters
2+
13
## currency
24

35
```

docs/optional_tags.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
TODO: examples of registering these tags
2+
13
## extends
24

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

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

93-
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.
95+
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.
9496

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

0 commit comments

Comments
 (0)