Jekyll One

QuickSearch

The Liquid template engine is an extraction from the E-Commerce system Shopify. The app powers many thousands of stores which all call for unique designs. To make this possible, the Liquid template engine was developed which allows customers of the Shopify system to create individual designs for their needs with freedom to not affect the integrity of the servers the Shopify software is running.

The template engine was designed for specific requirements in terms of security: rendering safe templates which cannot affect the security of the server they are rendered on. Hosting providers want to allow their customers to customize the appearance of their applications but don’t want them to run insecure code on their production systems.

  • Structure individual components to make them available multiple times

  • Render templates directly out of the applications configuration and files

  • Render all types of text-based data

Liquid and Jekyll

Jekyll is a generator to create static websites based on simple text files. That means no database system like MySQL is needed in behind; all data is being read from (text) files. These text files may contain configutation data, meta data (templates) to create the skeleton for the website or the content of the site to be generated.

Generators like Jekyll are based on processors to compute the data as a cascade. Jekyll make use of several processors, Liquid is one of them, working on a pipeline. For users of Linux or Unix this techique is well known.

Commandline applications on Linuy|Unix (and Windows as well) are very often used as a cascade using pipes. For example the command ls creates a directory listing that is piped to a grep filter to sort thing out. The filtered result is passed to e.g. the awk program to create formatted lines. Finally the result is passed to the sort filter to sort the data.

This processing cascade looks like:

ls | grep | awk | sort > file.txt

Generators like Jekyll are working a very same way using processors like:

  • the template engine Liquid

  • Markup processors like Asciidoctor for Asciidoc or Kramdown for data written in Markdown markup

  • Highlighters like Rouge for source code highlightning

A Jekyll pipeline may look like:

Jekyll | Layout | Liquid | Asciidoctor | Rouge > file.html

With this pipeline for example, Jekyll reads a configuration file, extracts layout data and pass them to the Liquid engine. The template engine process the relevant template data and give the results back to the pipeline. To convert the Asciidoc (content) portion, Asciidoctor process the Asciidoc Markup data and pass some information to the source code highlighter Rouge. In the very end, the processed data is taken from the pipeline and finally written to a HTML file.

To be honest, the generator Jekyll does not exactly process data that way but it is a good picture for how the data is processd and passed through a pipeline, generally spoken. All processors connected to the (Jekyll) pipeline are using this (pipeline) architecture as well. Markup language converters like Asciidoctor make use of pipes to run e.g. helper like highlighters. The Liquid template engine is using pipes for filtering and transforming data.

The following sections describe the basics of Liquid: the Markup Language, how data is being processed, what templates are and how these are used to create skeletons for the HTML files used for the website build by Jekyll.

The Front matter

Liquid for Jekyll is not an option. Liquid is a central component for creating websites using Jekyll - worth to know how it works. Beside configuration files being read by the site generator Jekyll to control the overall process of creating a website, Liquid templates are used as skeletons to create the base structure of the HTML files for a web - all of them.

What way came Liquid into the game? Quite simple, all files to be processed by Liquid has to have a so called Front Matter. All files having a Front Matter block will be tread by Jekyll as special files that are to be passed to Liquid for processing.

A Front Matter must be the first part in the file. A Front Matter consists of two triple-dashed lines. Here a basic example:

---
layout: post
title: Blogging Like a Hacker
...
---

Liquid Markups

There are two types of markups in Liquid: Output and Tag.

A Output markup, which may resolved to text, is surrounded by double curly brackets:

{{ pairs of double curly brackets }}

A Tag markup, which cannot resolved to text, is surrounded by curly brackets and percent signs {% and %}:

{% pairs of single curly brackets and percent signs %}

Output Markup

Here are some simple examples using the Output markup:

Hello {{name}}
Hello {{user.name}}
Hello {{ 'tobi' }}

Output and Filters

Output markup takes filters. Filters are simple methods. The first parameter is always the output of the left side of the filter. The return value of the filter will be the new left value when the next filter is run. When there are no more filters, the template will receive the resulting string.

Hello {{ 'tobi' |upcase }}
Hello tobi has {{ 'tobi' |size }} letters!
Hello {{ '*tobi*' |textilize |upcase }}
Hello {{ 'now' |date: "%Y %h" }}

Whitespace control

In Liquid, you can include a hyphen in your tag syntax {{-, -}}, {%-, and -%} to strip whitespace from the left or right side of a rendered tag. Normally, even if it doesn’t print text, any line of Liquid in your template will still print a blank line in your rendered HTML:

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

Notice the blank line before tomato in the rendered output:

Output

tomato

By including a hyphen in your assign closing tag, you can strip the whitespace following it from the rendered output:

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

If you don’t want any of your tags to print whitespace, as a general rule you can add hyphens to both sides of all your tags {%- and -%}:

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



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

Data Types (Variables)

Liquid objects can be one of six types:

  • String

  • Number

  • Boolean

  • Nil

  • Array

  • EmptyDrop

You can initialize Liquid variables using assign or capture tags.

String

Strings are sequences of characters wrapped in single or double quotes:

{% assign my_string = "Hello World!" %}
{% assign another_string = 'Hello World!' %}

Liquid does not convert escape sequences into special characters.

Number

Numbers include floats and integers:

{% assign my_int = 25 %}
{% assign my_float = -39.756 %}

Boolean

Booleans are either true or false. No quotations are necessary when declaring a boolean:

{% assign foo = true %}
{% assign bar = false %}

Nil

Nil is a special empty value that is returned when Liquid code has no results. It is not a string with the characters nil. Nil is treated as false in the conditions of if blocks and other Liquid tags that check the truthfulness of a statement.

In the following example, if the user does not exist (that is, user returns nil), Liquid will not print the greeting:

{% if user %}
  Hello {{ user.name }}!
{% endif %}

Tags or outputs that return nil will not print anything to the page.

Input
The current user is {{ user.name }}
Output
The current user is

Array

Arrays hold lists of variables of any type. To access all the items in an array, you can loop through each item in the array using an iteration tag.

<!-- if site.users = "Tobi", "Laura", "Tetsuro", "Adam" -->
{% for user in site.users %}
  {{ user }}
{% endfor %}
Output
  Tobi Laura Tetsuro Adam

You can use square bracket [ ] notation to access a specific item in an array. Array indexing starts at zero. A negative index will count from the end of the array.

<!-- if site.users = "Tobi", "Laura", "Tetsuro", "Adam" -->
{{ site.users[0] }}
{{ site.users[1] }}
{{ site.users[-1] }}
Output
Tobi
Laura
Adam

Initializing arrays

You cannot initialize arrays using only Liquid. You can, however, use the split filter to break a string into an array of substrings.

{% assign beatles = "John, Paul, George, Ringo" | split: ", " %}
{% for member in beatles %}
  {{ member }}
{% endfor %}
Output
John
Paul
George
Ringo

EmptyDrop

An EmptyDrop object is returned if you try to access a deleted object. In the example below, page_1, m and page_3 are all EmptyDrop objects:

{% assign variable = "hello" %}
{% assign page_1 = pages[variable] %}
{% assign page_2 = pages["does-not-exist"] %}
{% assign page_3 = pages.this-handle-does-not-exist %}

Checking for emptiness

You can check to see if an object exists or not before you access any of its attributes.

{% unless pages == empty %}
  <h1>{{ pages.frontpage.title }}</h1>
  <div>{{ pages.frontpage.content }}</div>
{% endunless %}

Both empty strings and empty arrays will return true if checked for equivalence with empty.

Variable Assignment

You can store data in your own variables, to be used in output or other tags as desired. The simplest way to create a variable is with the assign tag, which has a pretty straightforward syntax:

{% assign name = 'freestyle' %}
{% for t in collections.tags %}{% if t == name %}
  <p>Freestyle!</p>
{% endif %}{% endfor %}

Another way of doing this would be to assign true|false values to the variable:

{% assign freestyle = false %}
{% for t in collections.tags %}{% if t == 'freestyle' %}
  {% assign freestyle = true %}
{% endif %}{% endfor %}
{% if freestyle %}
  <p>Freestyle!</p>
{% endif %}

If you want to combine a number of strings into a single string and save it to a variable, you can do that with the capture tag. This tag is a block which "captures" whatever is rendered inside it, then assigns the captured value to the given variable instead of rendering it to the screen.

{% capture attribute_name %}{{ item.title }}-{{ i }}-color{% endcapture %}
<label for="{{ attribute_name }}">Color:</label>
<select name="attributes[{{ attribute_name }}]" id="{{ attribute_name }}">
  <option value="red">Red</option>
  <option value="green">Green</option>
  <option value="blue">Blue</option>
</select>

Filters

Table 1. Filters
Filter Description

append

Append a string e.g. {{ 'foo' |append:'bar' }} #⇒ 'foobar'

capitalize

capitalize words in the input sentence

ceil

Rounds a number up to the nearest integer, e.g. {{ 4.6 |ceil }} #⇒ 5

date

Reformat a date, see Liquid Syntax Reference

default

Returns the given variable unless it is null or the empty string, when it will return the given value, e.g. {{ undefined_variable |default: "Default value" }} #⇒ "Default value"

divided_by

Integer division e.g. {{ 10 |divided_by:3 }} #⇒ 3

downcase

convert an input string to lowercase

escape_once

Returns an escaped version of html without affecting existing escaped entities

escape

HTML escape a string

first

Get the first element of the passed in array

floor

Rounds a number down to the nearest integer, {{ 4.6 |floor }} #⇒ 4

join

Join elements of the array with certain character between them

last

Get the last element of the passed in array

lstrip

Strips all whitespace from the beginning of a string

map

Map/Collect an array on a given property

minus

Subtraction {{ 4 |minus:2 }} #⇒ 2

modulo

Remainder {{ 3 |modulo:2 }} #⇒ 1

newline_to_br

Replace each newline (\n) with html break

pluralize

Return the second word if the input is not 1, otherwise return the first word {{ 3 |pluralize: 'item', 'items' }} #⇒ 'items'

plus

Addition {{ '1' |plus:'1' }} #⇒ 2, {{ 1 |plus:1 }} #⇒ 2

prepend

Prepend a string {{ 'bar' |prepend:'foo' }} #⇒ 'foobar'

remove_first

Remove the first occurrence {{ 'barbar' |remove_first:'bar' }} #⇒ 'bar'

remove

Remove each occurrence {{ 'foobarfoobar' |remove:'foo' }} #⇒ 'barbar'

replace_first

Replace the first occurrence {{ 'barbar' |replace_first:'bar','foo' }} #⇒ 'foobar'

replace

Replace each occurrence {{ 'foofoo' |replace:'foo','bar' }} #⇒ 'barbar'

reverse

reverses the passed in array

round

Rounds input to the nearest integer or specified number of decimals {{ 4.5612 |round: 2 }} #⇒ 4.56

rstrip

Strips all whitespace from the end of a string

size

Return the size of an array or string

slice

Slice a string. Takes an offset and length, {{ "hello" |slice: -3, 3 }} #⇒ llo

sort

Sort elements of the array

split

Split a string on a matching pattern e.g. {{ "a~b" |split:"~" }} #⇒ ['a','b']

strip_html

Strip HTML markups from string

strip_newlines

Strip all newlines (\n) from string

strip

Strip all whitespace from both ends of the string

times

Multiplication {{ 5 |times:4 }} #⇒ 20

truncate

Truncate a string down to x characters. It also accepts a second parameter that will append to the string {{ 'foobarfoobar' |truncate: 5, '.' }} #⇒ 'foob.'

truncatewords

Truncate a string down to x words

uniq

Removed duplicate elements from an array, optionally using a given property to test for uniqueness

upcase

Convert an input string to uppercase

url_encode

URL encode a string

Tags

Tags are used for the logic in your template. Here is a list of supported tags:

Table 2. Tags
Tag Description

assign

Assigns some value to a variable

capture

Block tag that captures text into a variable

case

Block tag, its the standard case…​when block

comment

Block tag, comments out the text in the block

cycle

Cycle is usually used within a loop to alternate between values, like colors or DOM classes.

for

For loop

break

Exits a for loop

continue

Skips the remaining code in the current for loop and continues with the next loop

if

Standard if/else block

include

Includes another template; useful for partials

raw

temporarily disable tag processing to avoid syntax conflicts.

unless

Mirror of if statement

Comments

Any content that you put between {% comment %} and {% endcomment %} tags is turned into a comment (and no output is generated).

We made 1 million dollars {% comment %} in losses {% endcomment %} this year

Raw

Any content that you put between {% raw %} and {% endraw %} tags is turned into raw data. The tag temporarily disables Liquid from processing. This is useful for generating content (e.g., Mustache, Handlebars) which uses a conflicting syntax or the Output tag should be displayed as-it-is (e.g. for examples).

Temporarily disable processing for Handlebars
{% raw %}
In Handlebars, {{ this }} will be HTML-escaped, but {{{ that }}} will not.
{% endraw %}
Output tag displayed as-it-is
{% raw %}{{ page.date }}{% endraw %}

If and Else

The tags if and else should be well-known from any other programming language. Liquid allows you to write simple expressions in the if or unless (and optionally, elsif and else) clause.

Table 3. Clauses
Clause Description

{% if <CONDITION> %} …​ {% endif %}

Encloses a section of template which will only be run if the condition is true

{% elsif <CONDITION> %}

Can optionally be used within an if …​ endif block. Specifies another condition; if the initial "if" fails, Liquid tries the elsif, and runs the following section of template if it succeeds. You can use any number of elsifs in an if block

{% else %}

Can optionally be used within an if …​ endif block, after any elsif tags. If all preceding conditions fail, Liquid will run the section of template following the "else" tag

{% unless <CONDITION> %} …​ {% endunless %}

The reverse of an if statement. Don’t use elsif or else with an unless statement.

The condition of an if, elsif or unless tag should be either a normal Liquid expression or a comparison using Liquid expressions. Note that the comparison operators are implemented by the "if"-like tags; they don’t work anywhere else in Liquid.

Conditional operators

The available comparison operators are:

Table 4. Conditional operators
Operator Description

== | != | <>

Equality and inequality (note the two synonyms). There’s a secret special value empty (unquoted) that you can compare arrays to; the comparison is true if the array has no members.

< | <=

less | less-than

> | >=

greater | greater-than

contains

A wrapper around Ruby’s include? method, which is implemented on strings, arrays, and hashes. If the left argument is a string and the right isn’t, it stringifies the right.

and

Boolean and operator

or

Boolean or operator

Note that there is no not operator. Also note that you cannot use parentheses to control the order of operations, and the precedence of the operators appears to be unspecified. So when in doubt, use nested if statements instead of risking it.

Liquid expressions are tested for truthiness in what looks like a Ruby-like way:

  • Any string is true, including the empty string

  • Any array is true

  • Any hash is true

  • Any nonexistent or nil value (like a missing member of a hash) is false

{% if user %}
  Hello {{ user.name }}
{% endif %}
# Same as above
{% if user != null %}
  Hello {{ user.name }}
{% endif %}
{% if user.name == 'tobi' %}
  Hello tobi
{% elsif user.name == 'bob' %}
  Hello bob
{% endif %}
{% if user.name == 'tobi' or user.name == 'bob' %}
  Hello tobi or bob
{% endif %}
{% if user.name == 'bob' and user.age > 45 %}
  Hello old bob
{% endif %}
{% if user.name != 'tobi' %}
  Hello non-tobi
{% endif %}
# Same as above
{% unless user.name == 'tobi' %}
  Hello non-tobi
{% endunless %}
# Check for the size of an array
{% if user.payments == empty %}
   you never paid !
{% endif %}
{% if user.payments.size > 0  %}
   you paid !
{% endif %}
{% if user.age > 18 %}
   Login here
{% else %}
   Sorry, you are too young
{% endif %}
# array = 1,2,3
{% if array contains 2 %}
   array includes 2
{% endif %}
# string = 'hello world'
{% if string contains 'hello' %}
   string includes 'hello'
{% endif %}

Case Statement

If you need more conditions, you can use the case statement:

{% case condition %}
  {% when 1 %}
    hit 1
  {% when 2 or 3 %}
    hit 2 or 3
  {% else %}
  ... else ...
{% endcase %}

Example

{% case template %}
  {% when 'label' %}
       {{ label.title }}
  {% when 'product' %}
       {{ product.vendor |link&#x5F;to_vendor }} / {{ product.title }}
  {% else %}
       {{page_title}}
{% endcase %}

Cycle

Often you have to alternate between different colors or similar tasks. Liquid has built-in support for such operations, using the cycle tag.

{% cycle 'one', 'two', 'three' %}
{% cycle 'one', 'two', 'three' %}
{% cycle 'one', 'two', 'three' %}
{% cycle 'one', 'two', 'three' %}

will result in

one
two
three
one

If no name is supplied for the cycle group, then it’s assumed that multiple calls with the same parameters are one group. If you want to have total control over cycle groups, you can optionally specify the name of the group. This can even be a variable.

{% cycle 'group 1': 'one', 'two', 'three' %}
{% cycle 'group 1': 'one', 'two', 'three' %}
{% cycle 'group 2': 'one', 'two', 'three' %}
{% cycle 'group 2': 'one', 'two', 'three' %}

will result in

one
two
one
two

For loops

Liquid allows for loops over collections:

{% for item in array %}
  {{ item }}
{% endfor %}

Allowed collection types

For loops can iterate over arrays, hashes, and ranges of integers. When iterating a hash, item[0] contains the key, and item[1] contains the value:

{% for item in hash %}
  {{ item[0] }}: {{ item[1] }}
{% endfor %}

Instead of looping over an existing collection, you can also loop through a range of numbers. Ranges look like (1..10) - parentheses containing a start value, two periods, and an end value. The start and end values must be integers or expressions that resolve to integers.

# if item.quantity is 4...
{% for i in (1..item.quantity) %}
  {{ i }}
{% endfor %}
# results in 1,2,3,4

Breaking and continuing

You can exit a loop early with the following tags:

  • {% continue %}, immediately end the current iteration, and continue the for loop with the next value

  • {% break %}, immediately end the current iteration, then completely end the for loop

Both of these are only useful when combined with something like an if statement.

{% for page in pages %}
  # Skip anything in the hidden_pages array,
  # but keep looping over the rest of the values
  {% if hidden_pages contains page.url %}
    {% continue %}
  {% endif %}
  # If it's not hidden, print it.
  [page.title](page.url)
{% endfor %}
{% for page in pages %}
  [page.title](page.url)
  # After we reach the "cutoff" page, stop the list
  # and get on with whatever's
  # after the "for" loop:
  {% if cutoff_page == page.url %}
    {% break %}
  {% endif %}
{% endfor %}

Helper variables

During every for loop, the following helper variables are available for extra styling needs:

forloop.length      # => length of the entire for loop
forloop.index       # => index of the current iteration
forloop.index0      # => index of the current iteration (zero based)
forloop.rindex      # => how many items are still left?
forloop.rindex0     # => how many items are still left? (zero based)
forloop.first       # => is this the first iteration?
forloop.last        # => is this the last iteration?

Optional arguments

There are several optional arguments to the for tag that can influence which items you receive in your loop and what order they appear in:

  • limit:<INTEGER>, lets you restrict how many items you get

  • offset:<INTEGER>, lets you start the collection with the nth item

  • reversed iterates, over the collection from last to first

limit:int lets you restrict how many items you get. offset:int lets you start the collection with the nth item.

Restricting elements:

# array = [1,2,3,4,5,6]
{% for item in array limit:2 offset:2 %}
  {{ item }}
{% endfor %}
# results in 3,4

Reversing the loop:

{% for item in collection reversed %} {{item}} {% endfor %}

Instead of looping over an existing collection, you can define a range of numbers to loop through. The range can be defined by both literal and variable numbers:

# if item.quantity is 4...
{% for i in (1..item.quantity) %}
  {{ i }}
{% endfor %}
# results in 1,2,3,4

A for loop can take an optional else clause to display a block of text when there are no items in the collection:

# items => []
{% for item in items %}
   {{ item.title }}
{% else %}
   There are no items!
{% endfor %}

Liquid Programming Basics

It’s very simple to get started with Liquid. A Liquid template is rendered in two steps: Parse and Render. For an overview of the Liquid syntax, please read [[Liquid for Designers]].

@template = Liquid::Template.parse("hi {{name}}")  # Parses and compiles the template
@template.render( 'name' => 'tobi' )               # Renders the output => "hi tobi"

The parse step creates a fully compiled template which can be re-used as often as you like. You can store it in memory or in a cache for faster rendering later.

All parameters you want Liquid to work with have to be passed as parameters to the render method. Liquid does not know about your Ruby local, instance, and global variables.

Extending Liquid

Extending Liquid is very easy. However, keep in mind that Liquid is a young library and requires some outside help. If you create useful filters and tags, please consider making a pull request with them.

Create your own filters

Creating filters is very easy. Basically, they are just methods which take one parameter and return a modified string. You can use your own filters by passing an array of modules to the render call like this: @template.render(assigns, [MyTextFilters, MyDateFilters]).

module TextFilter
  def textilize(input)
    RedCloth.new(input).to_html
  end
end
@template = Liquid::Template.parse(" {{ '*hi*' |textilize }} ")
@template.render({}, :filters => [TextFilter])              # => "<strong>hi</strong>"

Alternatively, you can register your filters globally:

module TextFilter
  def textilize(input)
    RedCloth.new(input).to_html
  end
end
Liquid::Template.register_filter(TextFilter)

Once the filter is globally registered, you can simply use it:

@template = Liquid::Template.parse(" {{ '*hi*' |textilize }} ")
@template.render              # => "<b>hi</b>"

Create your own tags

To create a new tag, simply inherit from Liquid::Tag and register your block with Liquid::Template.

class Random < Liquid::Tag
  def initialize(tag_name, max, tokens)
     super
     @max = max.to_i
  end
  def render(context)
    rand(@max).to_s
  end
end
Liquid::Template.register_tag('random', Random)
@template = Liquid::Template.parse(" {% random 5 %}")
@template.render    # => "3"

Create your own tag blocks

All tag blocks are parsed by Liquid. To create a new block, you just have to inherit from Liquid::Block and register your block with Liquid::Template.

class Random < Liquid::Block
  def initialize(tag_name, markup, tokens)
     super
     @rand = markup.to_i
  end
  def render(context)
    value = rand(@rand)
    super.sub('^^^', value.to_s)  # calling `super` returns the content of the block
  end
end
Liquid::Template.register_tag('random', Random)
text = " {% random 5 %} you have drawn number ^^^, lucky you! {% endrandom %}"
@template = Liquid::Template.parse(text)
@template.render  # will return "you have drawn number 1, lucky you!" in 20% of cases

Caching of classes

If you get errors like A copy of …​ has been removed from the module tree but is still active! it’s probably because Liquid is caching your classes in development mode, the solution is to disable it in test and development modes:

# in config/environments/development.rb and config/environments/test.rb
Liquid.cache_classes = false