From ff6681cb9b81208f3617459528eaad9da803a391 Mon Sep 17 00:00:00 2001 From: James Prior Date: Tue, 11 Mar 2025 11:10:58 +0000 Subject: [PATCH] More docs updates and migration --- docs/custom_filters.md | 139 +++++++++++++++++++++++++++++++++++++++ docs/environment.md | 41 ++++++++---- docs/known_issues.md | 76 +++++++++++++++++++++ docs/optional_filters.md | 2 + docs/optional_tags.md | 4 +- 5 files changed, 249 insertions(+), 13 deletions(-) diff --git a/docs/custom_filters.md b/docs/custom_filters.md index e69de29b..49cb72f4 100644 --- a/docs/custom_filters.md +++ b/docs/custom_filters.md @@ -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'{label}' + ) + +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` diff --git a/docs/environment.md b/docs/environment.md index 64e20df6..2ce25ca9 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -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 @@ -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`. ``` {{ if else }} @@ -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? diff --git a/docs/known_issues.md b/docs/known_issues.md index e69de29b..c6906dc5 100644 --- a/docs/known_issues.md +++ b/docs/known_issues.md @@ -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 + +``` diff --git a/docs/optional_filters.md b/docs/optional_filters.md index 9b04741b..84b7c20f 100644 --- a/docs/optional_filters.md +++ b/docs/optional_filters.md @@ -1,3 +1,5 @@ +TODO: examples of registering these filters + ## currency ``` diff --git a/docs/optional_tags.md b/docs/optional_tags.md index 99b166b6..178ce804 100644 --- a/docs/optional_tags.md +++ b/docs/optional_tags.md @@ -1,3 +1,5 @@ +TODO: examples of registering these tags + ## extends ``` @@ -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.