The ELC Community Blog
A knowledge exchange on Ruby on Rails and Agile Development
Liquid Coolness
by Emmanuel on July 28, 2008
For the unaware Liquid is "a ruby library for rendering safe templates which cannot affect the security of the server they are rendered on" by Tobias Lütke.
Although Liquid has been around for a long time now, I had the opportunity to use it just recently. I found its source code very informative, so I thought of sharing my findings.
You don't need rails at all to use liquid. Here is a very minimal script for rendering a liquid template:
1 require "rubygems"
2 require "liquid"
3 Liquid::Template.parse("hello {{ place }}").render( "place" => "world" ) # => "hello world"
Markup
The documentation shows two types of markup:
- Output, surrounded by {{ two curly brackets }} (similar to the erb markup )
- Tags, surrounded by {% a curly bracket and a percent %} (similar to the erb markup )
Important note: liquid does NOT allow ruby code inside the markup. Example:
1 Liquid::Template.parse("{{ 1 + 2}}").render # => ?
What would you expect this example to output? Well, Liquid will parse the string "1" and ignore everything else. This very restriction is what makes Liquid so safe.
Tags and Blocks
Some tags work as method calls:
1 '{% a_tag "some param" %}' # => calls "method" a_tag with param "some_param".
Some standard Liquid tags that behave like this are: "cycle", "assign".
Others tags capture what's between the beginning and the end of the call:
1 <<-TEMPLATE
2 {% a_block_tag "some param" %}
3 {{ something }}
4 {% enda_block_tag %}
5 TEMPLATE
Example block tags: "comment", "for". In these cases the end tag is always equal to the opener but with the string "end" prefixed.
You can find more information about the available tags in liquid's wiki page for designers and eventually, by looking at the source of the standard tags.
Filters
Filters can be applied on output tags:
1 template = '{{ "some string" | upcase }}'
2 Liquid::Template.parse(template).render # => "SOME STRING"
In the previous example, the filter is named "upcase" and is invoked using the pipe syntax.
Filters can be chained up:
1 template = "{{ 'some text' | upcase | truncate, 7 }}"
2 Liquid::Template.parse(template).render # => "SOME..."
A non existent filter in the chain will be just skipped:
1 template = "{{ 'some text' | non-existent-filter | upcase | truncate, 7 }}"
2 Liquid::Template.parse(template).render # => "SOME..."
Another interesting thing is that you can use assigns (variables) as parameters for the filters:
1 template = "{% assign length = 5 %}{{ 'some text' | upcase | truncate, length }}"
2 Liquid::Template.parse(template).render # => “SO...”
You can take a peek at the standard filters in the documentation.
Some Internals
There's a page on the wiki which describes some aspects of the Liquid's API and how to extend the system.
Liquid templates are handled in two stages: first parsing, then rendering. A template string that has been parsed just once can be rendered many times with different assigns in its context data:
1 parsed_template = Liquid::Template.parse( "some {{ my_var }} template" )
2 parsed_template.render( "my_var" => "nice" ) # => some nice template
3 parsed_template.render( "my_var" => "cool" ) # => some cool template
In parsing stage the template string is tokenized according to the markup rules. Some regular expressions are used for this purpose. In this stage, Liquid::SyntaxError exception may be raised if an error is found.
The Context
In the rendering stage, an instance of Liquid::Context class is used for several things: storing "assigns" (local variables), "registers" and "scopes".
Assigns
The context's assigns (local variables) can be initialized when calling "render" method on the parsed template. We need to supply a hash for this purpose:
1 parsed_template = Liquid::Template.parse( "{{here}} have been {{are}}" )
2 parsed_template.render( Hash[ "here" => "the assigns", "are" => "initialized" ] )
3 # => "the assigns have been initialized"
Providing assigns to the render method is not mandatory. It is also possible to modify the assigns using markup. In particular the “assign” tag does just that.
Scopes
More than one Hash is used when storing assigns: there will be one per scope. The context's scopes are managed in an Array of assigns Hashes.
1 parsed_template = Liquid::Template.parse(<<-EOTEMPLATE)
2 {{ var }}
3 {% assign var = "end" %}
4 {% for var in collection %}
5 {{ var }}
6 {% endfor %}
7 {{ var }}
8 EOTEMPLATE
9 parsed_template.render("var" => "begin", "collection" => [1, 2]) # => begin 1 2 end
In this example, outside the "for" block, hello stores "begin". At the time of the first output, the "upper" assigns Hash holds "begin" as the value of "var". Later, the value of "var" is changed to "end" by means of the assign tag. But inside the for block, hello is assigned 1 and later 2. The final value is not overwritten because a new scope is created inside the "for" block.
You can't give access to objects of arbitrary classes to end users. Due security concerns, only String, Numeric, Hash, Array, Proc, Boolean or Liquid::Drop are allowed by default. The final value rendered in the template is the result of sending "to_liquid" message to the resolved object. Liquid extends some of the ruby standard classes with that method.
Resolution will be performed by looking at the assigns hash at the "top" of the scopes array, and moving down the stack if the value can't be found there. See Liquid::Context#find_variable(key) for the implementation details.
A Liquid "assign" tag may overwrite any previous value inside its scope.
Registers
Registers are passed in a hash to Context instances. Registers holds data related to the rendering of tags that will not be directly accessible by the end user, but still will be necessary for the functioning of a Liquid tag or block.
The registers are global to all tags and blocks in any scope. An example of registers usage can be found in the implementation of the "ifchanged" tag.
You can pass data to the registers in the call for render:
1 psd_templ.render({"var" => "..."}, :registers => { "some_data" => AnyClass.new })
Manipulation of the registers only makes sense if you are planning to use the data inside a custom Liquid tag or block. As an example, I have used registers to give Liquid tags access to rails session data.
In a following post I will talk about the different ways of extending Liquid.
Here are some interesting links related
Timeline
- Transform BitmapData into JPEGs (AS3, JPGEncoder, Rails, Rmagick)
- In the Land of the Magic Kingdom you will find... SPORTS!
- ELC Expands to NYC
- Create a jQuery plugin with a spin
- Investigating named_scope
- Liquid Coolness
- Ehcache for JRuby / Rails
- JS Routes plugin
- AWS-S3 gem extensions and Amazon's Copy API
- Warble with Console
- Speedy Solr: XML Libraries
Comments