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.
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 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 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.
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.
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.
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.
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).
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.
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:
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 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 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:
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:
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.
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 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" %}.
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: