Liquid Overview

Liquid Overview

Marketpath CMS templates utilize a templating language similar to the popular handlebars templating language. This language is based off of the Liquid framework originally created by Shopify and later translated and extended across numerous languages and applications. The Marketpath version of Liquid includes several key differences from the original Liquid markup, as well as numerous additional methods and objects.

Markup Overview

There are three types of markup in Liquid: Output, Methods, and Text

Output Markup is surrounded by matching curly brackets ({{ and }}). When Output Markup is processed, it is evaluated and converted into a string which is output to the template.

Method Markup is surrounded by metching method markup ({% and %}), and always starts with the name of the method to be evaluated. There are a large number of methods. Some methods control the flow of logic, some methods store and manipulate variables, and some methods output strings to the template. Each method also has its own syntax which defines how it should be used.

All methods may be divided into two types of methods. Block methods have an open method markup and a close method markup (eg: {% if %}...{% endif %}), they change the way that content in between the open and close markup is handled, and most create a new child scope for variables. Inline methods do not have any closing markup and do not have any impact on other markup.

Text Markup is everything that is not included inside Output or Method makup, and it is always output to the template.

Variables Overview

Variables are objects that are available for reference by the template. Some variables are reserved for use by Marketpath CMS and are defined by the request or page, while other may be created or modified using template markup. All variables are stored on a scope and are accessible to all markup inside that scope.

Scope Overview

Scope is the context in which a variable is accessible. Scopes may be nested similarly to how partial templates may be nested - with a root scope which may contain child scopes, each of which may also contain child scopes.

Any object on a parent scope is accessible in all child scopes unless the child scope has another object by the same name. Object on a child scope, however, are no longer accessible on the parent scope once the child scope is complete.

Additionally, if an object exists by the same name on both a parent and a child scope, only the object on the child scope will be accessible until the child scope is complete.

When variable scoping is utilized correclty, it is possible for multiple templates to use the same variable names without affecting each others behavior. Alternatively, if the template developer needs a template to modify the behavior of another template, they may use the root scope to share information between templates that are not nested.

Scope Creation

The root scope is the top-most scope used by the initial page template.

Child scopes are created every time you open up most block tags (eg: {% if %}, {% for %}, {% capture %}, etc...) or include a partial template. The child scope is completed when the block tag is closed (eg: {% endif %}, {% endfor %}, {% endcapture %}, etc...) or the partial template is finished rendering.

The liquid developer documentation should indicate which block tags create child scopes.

Variable Scopes

While it is possible to create templates without a good understanding of scope, it may be helpful to understand how liquid variables are scoped in order to create truly reusable template or integrate smoothly with code written by other developers.

What is scope?

Put as simply as possible, scope is the context in which any particular object is accessible.

Scopes may be nested similarly to how partial templates may be nested - with a root scope which may contain child scopes, each of which may also contain child scopes.

Any object on a parent scope is accessible in all child scopes unless the child scope has another object by the same name.

Object on a child scope, however, are no longer accessible on the parent scope once the child scope is complete.

Additionally, if an object exists by the same name on both a parent and a child scope, only the object on the child scope will be accessible until the child scope is complete.

How are child scopes created and completed?

Child scopes are created every time you open up a new block tag (eg: {% if %}, {% for %}, {% capture %}, etc...) or include a partial template. The child scope is completed when the block tag is closed (eg: {% endif %}, {% endfor %}, {% endcapture %}, etc...) or the partial template is finished rendering.

The liquid developer documentation should indicate which tags create child scopes (eg: "The case tag creates a new child scope." in the {% case %} documentation).

Why should I care?

On the most basic level, if you save objects on a child scope, they will not be available when the child scope is completed. This means that if you attempt to retrieve a value inside an {% if %} statement, it will be unavailable after the matching {% endif %} tag unless you save that value on a parent scope.

On a more advanced level, if you are creating reusable templates that you want to allow other people to use without messing up their templates while still using readable variable names, all you need to do is be careful to save your objects on the child scope and when your template is done rendering it will not have disturbed any of the parent template objects or variables. For example: you could use the variable "start" even if the same variable is used by a parent template, and when your template is done rendering the "start" object would be unchanged inside its scope.

OK. So how do I choose where to save an object?

Ahh. Now we get to the point of this discussion. There are three different ways to save objects on the scope which are designed to give you full control over where objects are saved without becoming overly complicated:

There are three ways that variables may be defined and saved to a scope:

  • var stores the variable on the current scope. Once the current scope is completed, the variable will no longer be accessible. This is the default functionality for everything except for the {% assign %} and {% set %} methods.
  • set updates the variable on the closest scope which it is already defined on. The the variable has not been defined on any scope, it will be stored on the root scope.
  • assign removes the variable from all scopes and stores it on the root scope.

When writing templates, the best practice is to utilize var and set wherever possible and reserve assign for cases where it is specifically necessary. By restricting your variables to the scopes where they are used, you make your code more friendly and compatible with code written by other developers who may wish to use the same variable names.

Note that there is currently no way to unset a variable from a scope other than using the assign method to define the variable at the root scope. Setting a variable to null does not remove it from the scope.

Objects Overview

Objects contain and control all dynamic information that is used while processing a page. An object may be output directly to the template using output markup, may be modified prior to being output using filters, or may be used by methods to control their behavior and output. There are numerous types of objects, each with their own properties and characteristics.

Simple Object Types

Simple object types do not have any child properties. They are the most basic type of data, and in many cases complex objects must be converted to simple object types in order to be used inside methods and filters.

The simple object types are:

  • strings: Text. Includes everything from the empty string ("") or single characters ("a") to the results of a fully-rendered template.
  • integers: Whole numbers, including 0 (eg: -1, -2, 0, 1, 2, ...).
  • numbers: Whole, decimal, and fractional numbers. Integers are a type of number.
  • booleans: True or false

Symbols

There are several symbols that refer to an idea rather than to a specific object. Some of them may be used in place of an object (eg: null, true/false) while others may only be used in a conditional expression (eg: blank/empty) or as an object property. The main symobls are:

  • null or nil: Symbolizes an object with no value, similar to a placeholder. Analogous to how one might describe a missing chair at the table or the food on an empty plate. May be used anywhere an object is used, but may result in unexpected behavior when a method or filter requires arguments to have a value.
  • true or false: The boolean values true and false. May be used anywhere a boolean value is used.
  • empty: May be used in a conditional to test for the empty string or an empty list.
  • blank: May be used in a conditional to test for a null value, the boolean false value, an empty string, a string containing only whitespace, or an empty list.
  • key or value: May be used as a property to reference the key or value of dictionary objects, but is not commonly used.
  • first or last: May be used as a property on list objects to refer to the first or last item in the list, and which will be null if the list is empty. If the object already has another property (eg: a custom field) with the same name, this will reference that property instead.
  • size: May be used as a property on list objects to refer to the number of items in the list. If the object already has another property (eg: a custom field) with the same name, this will reference that property instead.

Lists

Lists are objects that may contain multiple values, and which can be iterated/enumerated/looped by the template. There are a number of methods and filters that are useful for creating and working with lists. Additionally, a list of integers may be created using the range markup: (start..end). For example: (1..5) creates a list of numbers from 1 to 5.

Most lists have a common type for all items in that list, although not all lists are limited to a specific type - particularly lists that have been manipulated using filters and methods.

It should be noted that there are numerous object types which may be treated as both a complex object and a list.

It should also be noted that strings are technically both a list and not a list. They are a list in that they can be enumerated using the {% for %} method, in which case each value in the loop will be a single character. However, for most other purposes they are treated as a single value rather than a collection of values - so {% if "string" is_list %} will evaluate to false.

Complex Objects

More complicated object types include page, request, entity, list, dictionary, date, custom field, and other types defined in the liquid reference documentation.

When output directly to the template, or when converted to a string as a parameter to a method or filter, each object type may produce a different string. For complicated object types it may be a specific property, the empty string, a JSON string containing multiple properties, a fully-rendered sub-template, or some other pre-defined output. Refer to the documentation for each object type to understand the default output for that object type.

Properties on objects may be accessed using either object.property or object['property'] syntax in most cases, though for some objects, the object['property'] syntax is required (such as for the request.query_params and request.cookies objects).

Filters Overview

Filters may be used to modify objects before either being stored in a variable or output to the template. There are also some methods which allow the use of filters to modify an input object before it is evaluated by the method.

The syntax for using a filter is "input | filter_name: comma_separated_filter_parameters". For example: {{request.date | time_diff:entity.post_date, 'd'}}. Not all filters require parameters, in which case only the filter name is required. For example: {% var show_buttons = request.query_params['show_buttons'] | to_boolean %}.

Multiple filters may be chained together for more complex functionality. For example: {% var one_rooms = houses | where: "rooms", "1" | sort: "price" %}.

Conditions Overview

There are a few methods that evaluate objects based on conditions. The most basic and common of these is the {% if %} method. Each condition has 3 components:

  1. The subject: The object which you are checking the condition against. This is the only part of the condition that is required.
  2. The operator: Describes the type of conditional check you would like to perform. If there is no operator, the condition will return true if the left object exists and is non-null.
  3. The operand: The final argument for the condition. For comparison operators (==, !=, < >, <=, >=, contains, etc...) the operand is required and should be a second object to check against. For type operators (is_valid, is_boolean, is_date, etc...) the operand is not required. Some of the type operators (is_boolean, is_date, is_list, and is_number) may return a different result if you supply true as the operand.