Old Emmanuel Oga's Weblog (new one is at www.emmanueloga.com)

Liquid Coolness

Posted in rails, ruby by emmanueloga on julio 26, 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:

require "rubygems"
require "liquid"
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:

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:

'{% 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:

<<-TEMPLATE
  {% a_block_tag "some param" %}
    {{ something }}
  {% enda_block_tag  %}
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:

  template = '{{ "some string" | upcase }}'
  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:

  template = "{{ 'some text'  | upcase | truncate, 7 }}"
  Liquid::Template.parse(template).render # => "SOME..."

A non existent filter in the chain will be just skipped:

  template = "{{ 'some text'  | non-existent-filter | upcase | truncate, 7 }}"
  Liquid::Template.parse(template).render # => "SOME..."

Another interesting thing is that you can use assigns (variables) as parameters for the filters:

  template = "{% assign length = 5 %}{{ 'some text'  | upcase | truncate, length }}"
  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:

parsed_template = Liquid::Template.parse( "some {{ my_var }} template" )
parsed_template.render( "my_var" => "nice" ) # => some nice template
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:

parsed_template = Liquid::Template.parse( "{{here}} have been {{are}}" )
parsed_template.render( Hash[ "here" => "the assigns", "are" => "initialized" ] ) 
# => "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.

parsed_template = Liquid::Template.parse(<<-EOTEMPLATE)
  {{ var }}
  {% assign var = "end" %}
  {% for var in collection %}
    {{ var }}
  {% endfor %}
  {{ var }}
EOTEMPLATE
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:

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

This was also published here.

About these ads

Una respuesta

Suscríbete a los comentarios mediante RSS.

  1. Web Development » Liquid Refreshment said, on febrero 25, 2010 at 8:16 am

    [...] So my first task was to try and dig up as much information about Liquid as I could find. As is so often the case with these cutting-edge technologies, there isn’t much out there. The Liquid developers’ wiki was useful, but not exactly comprehensive. I was also able to find a few blog posts about it, for example this one about custom tags, and this overview of Liquid’s features. [...]


Deja un comentario

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

Seguir

Recibe cada nueva publicación en tu buzón de correo electrónico.

%d personas les gusta esto: