/liquid.py

A port of liquid template engine for python

Primary LanguagePythonApache License 2.0Apache-2.0

liquid.py

A port of liquid template engine for python

Pypi Github PythonVers Travis building Codacy Codacy coverage

Table of Contents

Install

# install released version
pip install liquid.py
# install lastest version
pip install https://github.com/pwwang/liquid.py.git

Baisic usage

from liquid import Liquid
liq = Liquid('{{a}}')
ret = liq.render({'a': 1})
# ret == '1'

With environments:

liq = Liquid('{{os.path.basename(a)}}', {'os': __import__('os')})
ret = liq.render({'a': "path/to/file.txt"})
# ret == 'file.txt'

Documentation

liquid.py basically implements almost all the features supported by liquid, however, it has some differences and specific features due to the language feature itself.
Anything that is different from liquid will be underscored.

Tags

liquid.py supports all tags that liquid does: {{, }}, {%, %}, and their non-whitespace variants: {{-, -}}, {%- and -%}. Beside these tags, liquid.py supports {#, #} (non-whitespace variants: {#-, -#} have some comments in the template.

Operators, types, truthy and falsy

They basically follow python syntax. Besides that, liquid.py also has true, false and nil keywords as liquid does, which correspond to True, False and None in python.

White space control

Same as liquid does, you can include a hyphen in your tag syntax {{-, -}}, {%-, -%}, {#- and -#} to strip whitespace from the left or right side of a rendered tag.
They basically strip the whitespace before and after the tag, as well as the newline character in the line of the tag.
So they match the regular expression r'[ \t]*{{-.*?-}}[ \t]*\n?'
Input

{% assign my_variable = "tomato" %}
{{ my_variable }}

Output


tomato

Input

{%- assign my_variable = "tomato" -%}
{{ my_variable }}

Output

tomato

Input

{% assign username = "John G. Chalmers-Smith" %}
{% if username and username.size > 10 %}
  Wow, {{ username }}, you have a long name!
{% else %}
  Hello there!
{% endif %}

Output



  Wow, John G. Chalmers-Smith, you have a long name!

Input

{%- assign username = "John G. Chalmers-Smith" -%}
{%- if username and username.size > 10 -%}
  Wow, {{ username }}, you have a long name!
{%- else -%}
  Hello there!
{%- endif -%}

Output

  Wow, John G. Chalmers-Smith, you have a long name!

NOTE: the leading spaces of the line is not stripped, this is slightly different from liquid

The above behavior of the tags are in loose mode, which is a default mode of liquid.py. You may also change the default mode globally:

from liquid import Liquid
Liquid.DEFAULT_MODE = 'compact'

By change the default mode to compact, then the whitespace tags will act exactly the same as the non-whitespace tags.

We also have a mixed mode, where only {# and {% act like {#- and {%-, as well as their closing tags. {{ remains the same.

You may also change the mode for each Liquid instance. Put {% mode compact %}, {% mode mixed %} or {% mode loose %} at the FIRST LINE to tell the engine to use the corresponding mode.
Input

{% mode compact %}
{% assign username = "John G. Chalmers-Smith" %}
{% if username and username.size > 10 %}
  Wow, {{ username }}, you have a long name!
{% else %}
  Hello there!
{% endif %}

Output

  Wow, John G. Chalmers-Smith, you have a long name!

Unless decleared explictly, the mode will be mixed in this document.

Blocks

Comment

Unlike liquid, liquid.py has two comment systems: comment block {% comment %}...{% endcomment %} and comment tag {# ... #}.
The former one is also supported by liquid, but acts differently. In liquid, anything between the comment block will be igored, however, it turns to python comments in liquid.py. If you want the comments to be ignore, you should use comment tag instead:
Input

Anything you put between
{% comment %}
and 
{% endcomment %}
tags is turned into a comment.

Output

Anything you put between
# and
tags is turned into a comment.

Input

Anything you put between
{# and #}
tags is turned into a comment.

Output

Anything you put between
tags is turned into a comment.

Use a different comment sign:
Input

{% comment // %}
This
will be
translated
as comments
{% endcomment %}

Output

// This
// will be
// translated
// as comments

Python source

You may also insert python source code to the template, one line each time

Input

{% python from os import path %}
{% python from glob import glob %}
{% python d = './date' %}
{% for filepath in glob(path.join(d, '*.txt')) %}
  {{path.basename(filepath)}}
{% endfor %}

Output

  a.txt
  b.txt

Control flow

if

Executes a block of code only if a certain condition is True.
Input

{% if product.title == 'Awesome Shoes' %}
  These shoes are awesome!
{% endif %}

Output

  These shoes are awesome!

unless

The opposite of if – executes a block of code only if a certain condition is not met.

Input

{% unless product.title == 'Awesome Shoes' %}
  These shoes are not awesome.
{% endunless %}

Output

These shoes are not awesome.

This would be the equivalent of doing the following:

{% if product.title != 'Awesome Shoes' %}
  These shoes are not awesome.
{% endif %}

elsif(elif, else if) / else

Adds more conditions within an if or unless block. liquid.py recoglizes not only elsif keyword as liquid does, it also treats else if and elif as elsif in liquid, or elif in python

Input

{% if customer.name == 'kevin' %} Hey Kevin! {% elsif customer.name == 'anonymous' %} Hey Anonymous! {% else %} Hi Stranger! {% endif %}

Output

  Hey Anonymous!

case/when

Creates a switch statement to compare a variable with different values. case initializes the switch statement, and when compares its values.

Input

{% assign handle = 'cake' %}
{% case handle %}
  {% when 'cake' %}
     This is a cake
  {% when 'cookie' %}
     This is a cookie
  {% else %}
     This is not a cake nor a cookie
{% endcase %}

Output

     This is a cake

Iteration/Loop

Iteration tags run blocks of code repeatedly.
for(parameters), tablerow and cycle are abandoned in liquid.py, because they can be easied performed using python expressions

while

liquid doesn't support while, but we have it here. Use it just like you are writing python codes:

Input

{% assign i = 3 %}
{% while i > 0 %}
{{i}}
{% assign i = i - 1 %}
{% endwhile %}

Output

3
2
1

for

Repeatedly executes a block of code. For a full list of attributes available within a for loop.

Input

{# collection.products = [Product(title = 'hat'), Product(title = 'shirt'), Product(title = 'pants')] #}
{% for product in collection.products %}
  {{ product.title }}
{% endfor %}

Output

  hat
  shirt
  pants

break/continue

Exit the loop or skip current iteration.

Input

{# collection.products = [Product(title = 'hat'), Product(title = 'shirt'), Product(title = 'pants')] #}
{% for product in collection.products %}
  {% if product.title == 'shirt' %}
    {% break %}
  {% endif %}
  {{ product.title }}
{% endfor %}

Output

  hat

Input

{# collection.products = [Product(title = 'hat'), Product(title = 'shirt'), Product(title = 'pants')] #}
{% for product in collection.products %}
  {% if product.title == 'shirt' %}
    {% continue %}
  {% endif %}
  {{ product.title }}
{% endfor %}

Output

  hat
  pants

Raw

Raw temporarily disables tag processing. This is useful for generating content (eg, Mustache, Handlebars) which uses conflicting syntax.

Input

{% raw %}
  In Handlebars, {{ this }} will be HTML-escaped, but
  {{{ that }}} will not.
{% endraw %}

Output

  In Handlebars, {{ this }} will be HTML-escaped, but
  {{{ that }}} will not.

Variable

assign

Creates a new variable.

Input

{% assign my_variable = false %}
{% if my_variable != true %}
  This statement is valid.
{% endif %}

Output

  This statement is valid.

Input

{% assign foo = "bar" %}
{{ foo }}

Output

bar

capture

Captures the string inside of the opening and closing tags and assigns it to a variable. Variables created through {% capture %} are strings.

Input

{% capture my_variable %}I am being captured.{% endcapture %}
{{ my_variable }}

Output

I am being captured.

Using capture, you can create complex strings using other variables created with assign.

Input

{% assign favorite_food = 'pizza' %}
{% assign age = 35 %}
{% capture about_me %}
I am {{ age }} and my favorite food is {{ favorite_food }}.
{% endcapture %}
{{ about_me }}

Output

I am 35 and my favourite food is pizza.

increment

Creates a new number variable, and increases its value by one every time it is called. The variable has to be initate before increment

Input

{% assign my_counter = 0 %}
{{my_counter}}
{% increment my_counter %}
{{my_counter}}
{% increment my_counter %}
{{my_counter}}
{% increment my_counter %}
{{my_counter}}

Output

0
1
2
3

NOTE: Unlike liquid, Variables created through the increment tag affects variables created through assign or capture.

decrement

Creates a new number variable, and decreases its value by one every time it is called.

Input

{% assign variable = 0 %}
{% decrement variable %}
{{ variable }}
{% decrement variable %}
{{ variable }}
{% decrement variable %}
{{ variable }}

Output

-1
-2
-3

Filters

liquid.py tries to support liquid filters, however, to support python filters themselves, we put @ before the filters to mark it as liquid filters.
escapse_once, sort_natural, first and last filters are abandoned.

Where you can use filters

  • In expression tags: {{, }} and {{-, -}}
  • In assign block: {% assign a = "abc" | len %}
  • In case/when block:
    {% case var | len %}
      {% when -3 | @abs %}
        {{Length is 3}}
      {% when 2 %}
        {{Length is 2}}
      {% else %}
        {{Other length}}
    {% endcase %}

liquid filters

Math filters

  • abs: Returns the absolute value of a number.
    {{ -17 | @abs }}
    {# output: 17 #}
    
    {{ 4 | @abs }}
    {# output: 4 #}
    
    {{ "-19.86" | @abs }}
    {# output: 19.86 #}
  • at_least: Limits a number to a minimum value.
  • at_most: Limits a number to a maximum value.
    {{ 4 | @at_least: 5 }}
    {# output: 5 #}
    
    {{ 4 | @at_least: 3 }}
    {# output: 4 #}
    
    {{ 4 | @at_most: 5 }}
    {# output: 4 #}
    
    {{ 4 | @at_most: 3 }}
    {# output: 3 #}
  • ceil: Rounds the input up to the nearest whole number.
    {{ 1.2 | @ceil }}
    {# output: 2.0 #}
    
    {{ 2.0 | @ceil }}
    {# output: 2.0 #}
    
    {{ 183.357 | @ceil }}
    {# output: 184.0 #}
    
    {{ "3.5" | @ceil }}
    {# output: 4.0 #}
  • divided_by: Divides a number by the specified number.
    {{ 16 | @divided_by: 4 | int }}
    {# output: 4 #}
    
    {{ 5 | @divided_by: 3 | int }}
    {# output: 1 #}
  • floor: Rounds a number down to the nearest whole number.
    {{ 1.2 | @floor }}
    {# output: 1.0 #}
    
    {{ 2.0 | @floor }}
    {# output: 2.0 #}
    
    {{ 183.357 | @floor }}
    {# output: 183.0 #}
    
    {{ "3.5" | @floor }}
    {# output: 3.0 #}
  • minus: Subtracts a number from another number.
    {{ 4 | @minus: 2 }}
    {# output: 2 #}
    
    {{ 16 | @minus: 4 }}
    {# output: 12 #}
    
    {{ 183.357 | @minus: 12 }}
    {# output: 171.357 #}
  • modulo(mod): Returns the remainder of a division operation.
    {{ 3 | @modulo: 2 }}
    {# python2 output: 1 #}
    {# python3 output: 1.0 #}
    
    {{ 24 | @mod: 7 | int }}
    {# output: 3 #}
    
    {{ 183.357 | @mod: 12 | int }}
    {# output: 3 #}
  • plus: Adds a number to another number.
    {{ 4 | @plus: 2 }}
    {# output: 6 #}
    
    {{ 16 | @plus: 4 }}
    {# output: 20 #}
    
    {{ 183.357 | @plus: 12 }}
    {# output: 195.357 #}
  • round: Rounds an input number to the nearest integer or, if a number is specified as an argument, to that number of decimal places.
    {{ 1.2 | @round }}
    {# output: 1 #}
    
    {{ 2.7 | @round }}
    {# output: 3 #}
    
    {{ 183.357 | @round: 2 }}
    {# output: 183.36 #}
  • times Multiplies a number by another number.
    {{ 3 | @times: 2 }}
    {# output: 6 #}
    
    {{ 24 | @times: 7 }}
    {# output: 168 #}
    
    {{ 183.357 | @times: 12 }}
    {# output: 2200.284 #}

String filters

  • append: Concatenates two strings and returns the concatenated value.
    {{ "/my/fancy/url" | @append: ".html" }}
    {# output: /my/fancy/url.html #}
    
    {% assign filename = "/index.html" %}
    {{ "website.com" | @append: filename }}
    {# output: website.com/index.html #}
  • capitalize: Makes the first character of a string capitalized.
    {{ "title" | @capitalize }}
    {# output: Title #}
    
    {{ "my great title" | @capitalize }}
    {# output: My great title #}
  • downcase: Makes each character in a string lowercase. It has no effect on strings which are already all lowercase.
    {{ "Parker Moore" | @downcase }}
    {# output: parker moore #}
    
    {{ "apple" | @downcase }}
    {# output: apple #}
  • escapse: Escapes a string by replacing characters with escape sequences (so that the string can be used in a URL, for example). It doesn’t change strings that don’t have anything to escape.
    {{ "Have you read 'James & the Giant Peach'?" | @escape }}
    {# output: Have you read 'James & the Giant Peach'? #}
    
    {{ "Tetsuro Takara" | @escape }}
    {# output: Tetsuro Takara #}
  • join: Combines the items in an array into a single string using the argument as a separator.
    {% assign beatles = "John, Paul, George, Ringo" | @split: ", " %}
    {{ beatles | join: " and " }}
    {# output: John and Paul and George and Ringo #}
  • lstrip: Removes all whitespaces (tabs, spaces, and newlines) from the beginning of a string. The filter does not affect spaces between words.
    {{ "          So much room for activities!          " | @lstrip }}
    {# output: So much room for activities!           #}
  • newline_to_br(nl2br): Replaces every newline (\n) with an HTML line break (<br />).
    {% capture string_with_newlines %}
    Hello
    there
    {% endcapture %}
    {{ string_with_newlines | @newline_to_br }}
    {# output: Hello<br />there<br /> #}
  • prepend: Adds the specified string to the beginning of another string.
    {{ "apples, oranges, and bananas" | @prepend: "Some fruit: " }}
    {# output: Some fruit: apples, oranges, and bananas #}
    
    {% assign url = "liquidmarkup.com" %}
    {{ "/index.html" | @prepend: url }}
    {# output: liquidmarkup.com/index.html #}
  • remove: Removes every occurrence of the specified substring from a string.
  • remove_first: Removes only the first occurrence of the specified substring from a string.
    {{ "I strained to see the train through the rain" | @remove: "rain" }}
    {# output: I sted to see the t through the #}
    
    {{ "I strained to see the train through the rain" | @remove_first: "rain" }}
    {# output: I sted to see the train through the rain #}
  • replace: Replaces every occurrence of an argument in a string with the second argument.
  • replace_first: Replaces only the first occurrence of the first argument in a string with the second argument.
    {{ "Take my protein pills and put my helmet on" | @replace: "my", "your" }}
    {# output: Take your protein pills and put your helmet on #}
    
    {% assign my_string = "Take my protein pills and put my helmet on" %}
    {{ my_string | @replace_first: "my", "your" }}
    {# output: Take your protein pills and put my helmet on #}
  • rstrip: Removes all whitespaces (tabs, spaces, and newlines) from the end of a string. The filter does not affect spaces between words.
    {{ "          So much room for activities!          " | @rstrip }}
    {# output:           So much room for activities! #}
  • slice: Returns a substring of 1 character beginning at the index specified by the argument passed in. An optional second argument specifies the length of the substring to be returned.
    {{ "Liquid" | @slice: 0 }}
    {# output: L #}
    
    {{ "Liquid" | @slice: 2 }}
    {# output: q #}
    
    {{ "Liquid" | @slice: 2, 5 }}
    {# output: quid #}
    
    {{ "Liquid" | @slice: -3, 2 }}
    {# output: ui #}
  • split: Divides an input string into an array using the argument as a separator.
    {% assign beatles = "John, Paul, George, Ringo" | @split: ", " %}
    {% for member in beatles %}
      {{- member -}},
    {% endfor %}
    {# output: John,Paul,George,Ringo, #}
  • strip: Removes all whitespace (tabs, spaces, and newlines) from both the left and right side of a string. It does not affect spaces between words.
    {{ "          So much room for activities!          " | @strip }}
    {# output: So much room for activities! #}
  • strip_html: Removes any HTML tags from a string.
    {{ "Have <em>you</em> read <strong>Ulysses</strong>?" | @strip_html }}
    {# output: Have you read Ulysses? #}
  • strip_newlines: Removes any newline characters (line breaks) from a string.
    {% capture string_with_newlines %}
    Hello
    there
    {% endcapture %}
    {{ string_with_newlines | @strip_newlines }}
    {# output: Hellothere #}
  • truncate: Shortens a string down to the number of characters passed as a parameter. If the number of characters specified is less than the length of the string, an ellipsis (…) is appended to the string and is included in the character count.
  • truncatewords: Shortens a string down to the number of words passed as the argument. If the specified number of words is less than the number of words in the string, an ellipsis (…) is appended to the string.
    {{ "Ground control to Major Tom." | @truncate: 20 }}
    {# output: Ground control to... #}
    
    {{ "Ground control to Major Tom." | @truncate: 25, ", and so on" }}
    {# output: Ground control, and so on #}
    
    {{ "Ground control to Major Tom." | @truncate: 20, "" }}
    {# output: Ground control to Ma #}
    
    {{ "Ground control to Major Tom." | @truncatewords: 3 }}
    {# output: Ground control to... #}
    
    {{ "Ground control to Major Tom." | @truncatewords: 3, "--" }}
    {# output: Ground control to-- #}
    
    {{ "Ground control to Major Tom." | @truncatewords: 3, "" }}
    {# output: Ground control to #}
  • upcase: Makes each character in a string uppercase. It has no effect on strings which are already all uppercase.
    {{ "Parker Moore" | @upcase }}
    {# output: PARKER MOORE #}
    
    {{ "APPLE" | @upcase }}
    {# output: APPLE #}
  • url_encode: Converts any URL-unsafe characters in a string into percent-encoded characters using cgi.urlencode.
  • url_decode: Decodes a string that has been encoded as a URL or by cgi.unquote.
    {{ "%27Stop%21%27%20said%20Fred" | @url_decode }}
    {# output: 'Stop!' said Fred #}
    
    {{ "john@liquid.com" | @url_encode }}
    {# output: john%40liquid.com #}
    
    {{ "Tetsuro Takara" | @url_encode }}
    {# output: Tetsuro+Takara #}

List/Array filters

  • compact: Removes any empty values from an array.
    {% assign site_categories = site.pages | @map: 'category' %}
    {% for category in site_categories %}
      {{ category }}
    {% endfor %}
    {% comment %}
    Output:
      business
      celebrities
    
      lifestyle
      sports
    
      technology
    {% endcomment %}
    
    {% assign site_categories = site.pages | @map: 'category' | @compact %}
    {% for category in site_categories %}
      {{ category }}
    {% endfor %}
    {% comment %}
    Output:
      business
      celebrities
      lifestyle
      sports
      technology
    {% endcomment %}
  • concat: Concatenates (joins together) multiple arrays. The resulting array contains all the items from the input arrays.
    {% assign fruits = "apples, oranges, peaches" | @split: ", " %}
    {% assign vegetables = "carrots, turnips, potatoes" | @split: ", " %}
    {% assign everything = fruits | @concat: vegetables %}
    {% for item in everything %}
    - {{ item }}
    {% endfor %}
    {% comment %}
    Output:
    - apples
    - oranges
    - peaches
    - carrots
    - turnips
    - potatoes
    {% endcomment %}
  • map: Creates an array of values by extracting the values of a named property from another object.
    {% assign all_categories = site.pages | @map: "category" %}
    {% for item in all_categories %}
    {{ item }}
    {% endfor %}
    {% comment %}
    Output:
    business
    celebrities
    lifestyle
    sports
    technology
    {% endcomment %}
  • reverse: Reverses the order of the items in an array. reverse cannot reverse a string.
    {% assign my_array = "apples, oranges, peaches, plums" | @split: ", " %}
    {{ my_array | @reverse | @join: ", " }}
    {# output: plums, peaches, oranges, apples #}
  • sort: Sorts items in an array by a property of an item in the array. The order of the sorted array is case-sensitive.
    {% assign my_array = "zebra, octopus, giraffe, Sally Snake" | @split: ", " %}
    {{ my_array | @sort | @join: ", " }}
    {# output: Sally Snake, giraffe, octopus, zebra #}
  • uniq: Removes any duplicate elements in an array (order not preserved).
    {% assign my_array = "ants, bugs, bees, bugs, ants" | split: ", " %}
    {{ my_array | @uniq | @join: ", " | @sort }}
    {# output: ants, bugs, bees #}

Other filters

  • date: Converts a date format into another date format. The format for this syntax is the same as datetime.datetime.strptime and datetime.datetime.strftime
    {% assign article = lambda: None %}
    {% assign article.published_at = '07/17/2015' %}
    {{ article.published_at | @date: "%a, %b %d, %y", "%m/%d/%Y" }}
    {# output: Fri, Jul 17, 15 #}
    
    {{ article.published_at | @date: "%Y", "%m/%d/%Y" }}
    {# output: 2015 #}
    
    {{ "March 14, 2016" | @date: "%b %d, %y", "%B %d, %Y" }}
    {# output: Mar 14, 16 #}
    
    This page was last updated at {{ "now" | @date: "%Y-%m-%d %H:%M" }}.
    {# output: This page was last updated at 2018-09-19 21:18 #}
  • default: Allows you to specify a fallback in case a value if falsy.
    {% assign product_price = None %}
    {{ product_price | @default: 2.99 }}
    {# output: 2.99 #}
    
    {% assign product_price = 4.99 %}
    {{ product_price | @default: 2.99 }}
    {# output: 4.99 #}
    
    {% assign product_price = "" %}
    {{ product_price | @default: 2.99 }}
    {# output: 2.99 #}
  • size: Equivalent of len in python
    {{ "Ground control to Major Tom." | @size }}
    {# output: 28 #}
    
    {% assign my_array = "apples, oranges, peaches, plums" | @split: ", " %}
    {{ my_array | @size }}
    {# output: 4 #}

Python filters

Direct filters

Any function in the environment that takes the first arguments as the values on the left of the expression (value before |) could be used as direct filters:

{{"123" | len}}
{# output: 3 #}

{{ 1.234, 2 | round }}
{# output: 1.23 #}

You can even use python expressions on the left:

{{len("123") | @plus: 3}}
{# output: 6 #}

{{ 1.234, 1+1 | round }}
{# output: 1.23 #}

Extra arguments can be pass as liquid filters:

{{ 1.234 | round: 2 }}
{# output: 1.23 #}

You may also define a function and then pass it to the environment of the template compilation:

def someComplicatedLogic(val):
	# your logic goes here
	return val

liq = Liquid('{{ "value" | myfilter }}', {'myfilter': someComplicatedLogic})
liq.render()

Getitem filters

You can get the items directly from the values on the left:

{{ [1,2,3] | [0] }}
{# output: 1 #}

{{ [1,2,3] | [1:] | sum }}
{# output: 5 #}

{{ {"a": 1} | ["a"] }}
{# output: 1 #}

Remember if you if have multiple values on the left, they will be treated as a tuple:

{{ 1.234, 1+1 | [1] }}
{# output: 2 #}

Attribute filters

Get the value from the attribute of an object. If it is callable, you can also use it as a filter:

{{ "," | .join: ['a', 'b'] }}
{# output: a,b #}

{{ "{}, {}!" | .format: "Hello", "world" }}
{# output: Hello, world! #}

Get non-callable attribute values:

{{ '' | .__doc__ }}
{# output: str(object='') ... #}

What if callable attribute takes no argument:

{{ '1' | .isdigit }}
{# output: <function isdigit> #}

To call it:

{{ '1' | .isdigit() }}
{# or #}
{{ '1' | .isdigit: }}

Lambda filters

You may also apply lambda filters:

{% python from os import path %}
{{ "/path/to/file.txt" | lambda p, path = path: path.join( path.dirname(p), path.splitext(p)[0] + '.sorted' + path.splitext(p)[1] ) }}
{# output: /path/to/file.sorted.txt #}

If you don't have to use global variables in lambda, you may also omit the lambda keyword:

{{ "/path/to/file.txt" | :len(a) - 4 }}
{# output: 13 #}

The argument names start from a, up to z.