Appearance
Liquid Specification Differences
AireGlu implements the Shopify Liquid specification as much as possible, but due to AireGlu-specific requirements and different underlying technologies, there are some differences. This page will focus on the differences between the Shopify Liquid specification and our own implementation. We will also highlight differences between our Liquid engine versions.
Differences Compared to Shopify Liquid Documentation
Basics
Operators
contains
The Shopify Liquid documentation mentions that contains allows you to also check for the presence of a string in an array of strings. However, this is not possible for either version of our Liquid engine. Therefore, instead of doing:
{% if product.tags contains "Hello" %}
This product has been tagged with "Hello".
{% endif %}You will need to do something like this instead:
{% assign has_hello = false %}
{% for tag in input.product.tags %}
{% if tag == "Hello" %}
{% assign has_hello = true %}
{% endif %}
{% endfor %}
{% if has_hello %}
This product has been tagged with "Hello".
{% endif %}Truthy and Falsy
The Shopify Liquid documentation states that arrays and empty arrays are truthy. However, this is not the case in AireGlu and you will not be able to use these in conditional checks. Given the following Liquid:
{% if [] or ["test"] %}
This is invalid AireGlu Liquid.
{% endif %}You will simply just get:
{% if [] or ["test"] %}
This is invalid AireGlu Liquid.
{% endif %}returned back to you instead of This is invalid AireGlu Liquid..
For more information on Truthy and Falsy, please see the Shopify Liquid documentation.
Types
Strings
The Shopify Liquid documentation states that Liquid does not convert escape sequences (such as \n) into special characters. However, AireGlu does convert escape sequences into special characters, for both Liquid engine versions. For example:
STRING:
{% assign my_string = "Hello World!" %}
{% assign my_string_escape = "Hello\n \"World\"!" %}
{{ my_string }}
{{ my_string_escape }}will give you the following result:
STRING:
Hello World!
Hello
"World"!Array
Accessing items in arrays
The Shopify Liquid documentation shows the following Liquid:
{% for user in site.users %}
{{ user }}
{% endfor %}Resulting in:
Tobi Laura Tetsuro AdamAlthough, in AireGlu these will each get a newline appended e.g. for V1 you will get:
Tobi
Laura
Tetsuro
AdamAnd for V2 you will get:
Tobi
Laura
Tetsuro
AdamTherefore, you will need to make use of hyphens which are documented in the Whitespace Control section in order to get the desired result of all of the users being on a single line.
Accessing specific items in arrays
The Shopify Liquid documentation mentions that a negative index will count from the end of the array. e.g. {{ site.users[-1] }} would give you the last item of the array. However, in AireGlu this is not the case and using a -1 index will give you an empty result.
For more information on Types, please see the Shopify Liquid documentation.
Tags
Template
The following do not work in either of our Liquid engine versions:
inline commentsLiquidrenderinclude
Filters
capitalize
In the Shopify Liquid documentation, the following:
{{ "my GREAT title" | capitalize }}claims to result in My great title, however, both of our Liquid engine versions give us My GREAT Title. In other words the first character is capitalised but the rest of the string remains the same.
size
Size works as expected for both of our Liquid engine versions, the only difference is when trying to do something like {{ "this is a string".size }}. This won't work as the Shopify Liquid documentation mentions, however you can work around this by doing {{ "this is a string" | size }}. However, if you were to access a string via the input for example (instead of using a direct string) then you would be able to use the size filter.
For example, given the input of:
xml
<nametwo>Fox Mulder</nametwo>{{ input.nametwo.size }} and {{ input.nametwo | size }} both correctly give you 10, the number of characters in Fox Mulder. This works in both Liquid engine versions.
first and last on plain strings
Given the same <nametwo>Fox Mulder</nametwo> example, {{ input.nametwo.first }} and {{ input.nametwo.last }} give you the first and last characters, F and r - but only in V2. V1 does not support .first/.last on strings at all - this returns nothing for any string, not just ones that came from XML. .size above is unaffected by this and works correctly in both versions.
An XML element with an attribute
If the XML element you are accessing has an attribute on it, it is not treated as a plain string internally even if its only content is text. Given:
xml
<name id="1">Fox Mulder</name>{{ input.name }} still gives you Fox Mulder, and {{ input.name.id }} gives you 1, in both Liquid engine versions.
V2 also correctly treats the value as a string for everything else, provided the element has no child elements of its own (an attribute alone does not stop this working) - exactly matching the plain-string behaviour described above:
{{ input.name.size }}gives you10, the number of characters inFox Mulder.{{ input.name.first }}and{{ input.name.last }}give youFandr, the first and last characters.{% if input.name == "Fox Mulder" %}correctly evaluates to true.
V1 does not have this - .size, .first, .last all give you nothing, and == always evaluates to false, even though input.name prints as Fox Mulder. Note this is on top of the .first/.last limitation above that affects V1 regardless of attributes - even once you work around the attribute, .first/.last still won't work in V1. If you need this to work the same way in V1, force the value into a plain string first e.g.
{% assign name_text = input.name | append: "" %}
{{ name_text.size }}
{% if name_text == "Fox Mulder" %}"yes"{% else %}"no"{% endif %}would give you 10 and "yes" respectively (name_text.first/.last would still give you nothing in V1, per the limitation above).
Note that if the element also has child elements of its own (not just an attribute), V2 keeps treating it as a one-item array rather than a string, since that matches the intent of XML single vs multiple elements below - the string fallback above only applies to a leaf element (no child elements) that has an attribute.
V1 and V2 differences
Basics
Operators
Order of operations
Given the following Liquid:
{% if true or false and false %}
This evaluates to true, since the `and` condition is checked first.
{% endif %}
{% if true and false and false or true %}
This evaluates to false, since the tags are checked like this:
true and (false and (false or true))
true and (false and true)
true and false
false
{% endif %}You will get a different output depending on whether your task has the v1 Liquid engine selected or the v2 Liquid engine.
V1 Output:
ORDER OF OPERATIONS:
This evaluates to true, since the `and` condition is checked first.
This evaluates to false, since the tags are checked like this:
true and (false and (false or true))
true and (false and true)
true and false
falseV2 Output:
ORDER OF OPERATIONS:
This evaluates to true, since the `and` condition is checked first.The V2 version is now inline with what is expected from the Shopify Liquid documentation. In tags with more than one and or or operator, operators are checked in order from right to left.
Truthy and Falsy
Array
One thing to note with arrays in V1 compared to V2 is how they handle being compared against non-array values. If you had the following:
result=[{% if data.myArray == 'test' %}EQUAL{% else %}NOT-EQUAL{% endif %}]V1 would return
result=[EQUAL]Whereas V2 would return
result=[NOT-EQUAL]In other words, V1 would always return true when comparing an array against a non-array value, whereas V2 correctly returns false instead.
Whitespace Control
Given the following Liquid:
WHITESPACE CONTROL:
{% assign my_variable = "tomato" %}
{{ my_variable }}
{% assign my_variable = "tomato" -%}
{{ my_variable }}
{% assign username = "John G. Chalmers-Smith" %}
{% if username and username.size > 10 %}
Wow, {{ username }} , you have a long name!
{% else %}
Hello there!
{% endif %}
{% assign username = "John G. Chalmers-Smith" -%}
{%- if username and username.size > 10 -%}
Wow, {{ username -}} , you have a long name!
{%- else -%}
Hello there!
{%- endif %}You will get a different output depending on whether your task has the v1 Liquid engine selected or the v2 Liquid engine.
V1 Output:
WHITESPACE CONTROL:
tomato
tomato
Wow, John G. Chalmers-Smith , you have a long name!
Wow, John G. Chalmers-Smith, you have a long name!V2 Output:
WHITESPACE CONTROL:
tomato
tomato
Wow, John G. Chalmers-Smith , you have a long name!
Wow, John G. Chalmers-Smith, you have a long name!The V2 version is now inline with what is expected from the Shopify Liquid documentation. Normally, even if it doesn’t print text, any line of Liquid in your template will still print a blank line. Therefore, you will now need to use a hyphen in your tag syntax {{-, -}}, {%-, and -%} to strip whitespace from the left or right side of a rendered tag.
So to get the same output as V1 in V2 you would need to do something like this (notice the additional hyphens):
WHITESPACE CONTROL:
{% assign my_variable = "tomato" -%}
{{ my_variable }}
{% assign my_variable = "tomato" -%}
{{ my_variable -}}
{% assign username = "John G. Chalmers-Smith" %}
{% if username and username.size > 10 %}
Wow, {{ username }} , you have a long name!
{%- else -%}
Hello there!
{% endif -%}
{% assign username = "John G. Chalmers-Smith" -%}
{%- if username and username.size > 10 %}
Wow, {{ username -}} , you have a long name!
{% else %}
Hello there!
{% endif %}Capture
As the capture tag is typically used in a multiline fashion, take care to strip any unwanted new lines within a capture tag using the hyphen as described above. If this is not done, the new lines will be included in the result of your capture. For example:
{% assign animal = "frog" %}
{% capture colour %}
{% if animal == "frog" %}
green
{% else %}
unknown
{% endif %}
{% endcapture %}
result="{{colour}}"Output in text format would be:
result="
green
"In some cases the whitespace characters may also present as their escaped sequences such as \n within strings.
For more information on whitespace control, please see the Shopify Liquid documentation.
Tags
Control Flow
case/when
The Shopify Liquid documentation says that you can provide the values as a comma-separated list, or separate them using an or operator. This is true with our V2 Liquid engine, however V1 does not allow a comma-separated list and must instead make use of the or operator.
For more information on control flow, please see the Shopify Liquid documentation.
Iteration
There are some parameters that do not work in a for for our V1 Liquid engine. The following parameters will only work in V2:
offsetrange
You are also unable to use the following in either Liquid engine version:
tablerowtablerowloop
Whitespace considerations
The following forloop example from the Shopify Liquid documentation:
{% assign smoothie_flavors = "orange, strawberry, banana" | split: ", " %}
{% for flavor in smoothie_flavors -%}
{%- if forloop.length > 0 -%}
{{ flavor }}{% unless forloop.last %}-{% endunless -%}
{%- endif -%}
{% endfor %}Results in:
orange-strawberry-bananaHowever, our V1 engine actually gives us:
orange- strawberry- bananaThe following cycle example from the Shopify Liquid documentation:
{% cycle "one", "two", "three" %}
{% cycle "one", "two", "three" %}
{% cycle "one", "two", "three" %}
{% cycle "one", "two", "three" %}Gives us:
one
two
three
oneHowever, again our V1 engine instead gives us something slightly different:
onetwothreeoneThe same formatting can also be seen for increment and decrement for variables where V1 places the results on a single line instead of a new line for each.
Iterating over strings
Unlike the examples above, this is not just a whitespace formatting difference - our two Liquid engines actually run the for loop a different number of times when the thing being iterated is a string, because our V1 and V2 Liquid engines use different versions of the underlying Fluid library, and this behaviour changed between those versions.
Given the following Liquid, where input.interests[0] is the string "test":
{% for letter in input.interests[0] %}
{{ letter }}
{% endfor %}V1 runs the loop once per character in the string (4 times for "test"), giving you:
t
e
s
tV2 runs the loop only once, treating the whole string as a single item, giving you:
testEven though V1 is genuinely running 4 loop iterations to V2's 1, adding hyphens to strip the whitespace between each of V1's iterations happens to produce the same end result, since concatenating the 4 individually-printed characters with no whitespace between them gives you the same string as printing it all at once:
{% for letter in input.interests[0] %}
{{- letter -}}
{% endfor %}Results in test for both Liquid engines.
If you wanted the behaviour of V1 (iterating character by character) in V2, you can split the string into individual characters first:
{% assign chars = input.interests[0] | split: "" %}
{% for letter in chars %}
{{ letter -}}
{% endfor %}For more information on iteration, please see the Shopify Liquid documentation.
Filters
newline_to_br
The following example from the Shopify Liquid documentation:
{% capture string_with_newlines %}
Hello
there
{% endcapture %}
{{ string_with_newlines | newline_to_br }}Results in:
<br />
Hello<br />
there<br />However, for V1 we get:
Hello<br />there<br />And for V2 we get:
<br />Hello<br />there<br />So for both versions, the result ends up on a single line instead of multiple lines. Also, for V1 the first <br /> is not present at all.
The following filters do not work in our V1 Liquid engine:
remove_firstremove_lastreplace_firstreplace_lastsum
e.g. remove_first for the following:
{{ "I strained to see the train through the rain" | remove_first: "rain" }}results in I strained to see the train through the rain instead of the expected I sted to see the train through the rain. These filters are essentially ignored and will just print out whatever is defined before the filter e.g.
{% assign total_quantity = input.collection.products | sum: "quantity" %}Gives us:
[{"title":"hat","quantity":1},{"title":"shirts","quantity":3},{"title":"pants","quantity":2}]Instead of an expected sum value of 6
These filters work as expected in the Shopify Liquid documentation for our V2 Liquid engine.
default
When setting a value to an empty string and using default you will get different behaviour depending on the Liquid engine version used. If you had:
{{ product_price | default: 2.99 }}
{% assign product_price = 4.99 %}
{{ product_price | default: 2.99 }}
{% assign product_price = "" %}
{{ product_price | default: 2.99 }}You would see this in V1:
2.99
4.99Whereas for V2, you would get a more accurate result of:
2.99
4.99
2.99Therefore, in V1 an empty string would be considered a valid value which didn't fall back to the default, whereas V2 now closely resembles the Shopify Liquid documentation and does fall back to the default value when the value is an empty string.
forloop Indexing
This behaves slightly differently depending on the Liquid engine used. In V1 you can do forloop.index - 1 like so:
{% for val in instances[0].values %}
{% assign index = forloop.index - 1 %}
...
{% unless forloop.last %},{% endunless %}However, this will not work in V2. You must instead use forloop.index0 to achieve the same result:
{% for val in instances[0].values %}
{% assign index = forloop.index0 %}
...
{% unless forloop.last %},{% endunless %}forloop.index0 can be used in V1 too, therefore it is recommended to use this approach throughout.
Strict types
Our V2 Liquid engine is stricter than V1 when it comes to property types. A common example is when comparing true with "true". If you had the following:
{% if testStringBool == true %}Where testStringBool is "true", then V1 would return a successful match. However, for V2 because "true" is a string, this would not match the boolean value of true. To get the same result in V2 you could do:
{% if testStringBool == "true" %}or even
{% if testStringBool == true or testStringBool == "true" %}to have both scenarios covered.
Precomputing values
A common approach in V1 would be to compute a value as part of an if statement, like so:
{% if resource.id == resource | split: "/" | last %}However, due to the order of operations in our V2 Liquid engine, this will not be possible and you should instead precompute your value and then use this in the if statement instead, like so:
{% assign resource_id = resource | split: "/" | last %}
{% if resource.id == resource_id %}XML single vs multiple elements
When mapping XML input (see XML input mapping), if there is more than one element with the same name at the same level, they are combined into an array e.g. two <Item> elements under the same parent let you do {{ input.Item.size }}, {{ input.Item.first }}, or {% for item in input.Item %}.
If there is only a single occurrence of that element, our two Liquid engines behave differently:
- V1 - the single element is not treated as an array, so
{{ input.Item.size }},{{ input.Item.first }},{{ input.Item.last }}and{% for item in input.Item %}will all return nothing. - V2 - the single element is still treated as if it were a one-item array, so
{{ input.Item.size }}returns1, and{{ input.Item.first }}/{{ input.Item.last }}/{% for item in input.Item %}all behave the same as they would for multiple elements.
If you need Liquid that works the same way regardless of whether there is one element or several, and you need to support V1, you can use a workaround like this:
{% assign items = input.Item %}
{% if items.first %}
{% for item in items %}...{% endfor %}
{% else %}
...access `items` directly here instead of `items.first`, as there is only a single item...
{% endif %}For V2 this workaround is unnecessary (though it will still work, since items.first is always truthy there) as the single-item case already behaves the same as the multiple-item case.
Looping over a single element while also accessing its nested fields
Because V2 treats a single element as a one-item array, you can loop over a container even when it only occurs once, while still accessing its own nested array and sibling fields from inside that loop - this is not possible in V1 at all. Given the following XML:
xml
<?xml version="1.0" encoding="utf-8"?>
<agent>
<name id="1">Fox Mulder</name>
<interests>
<interest>paranormal</interest>
<interest>paranormal 2</interest>
<different-thing>this is different</different-thing>
</interests>
<interestsSingle>
hello
</interestsSingle>
</agent>Note that interests only occurs once, and the following Liquid loops over it while also accessing its interest array and different-thing sibling field from inside that loop:
{% for a in input.interests %}
{% for b in a.interest %}
{{ b }}
{% endfor %}
{{ a.different-thing }}
{% endfor %}V1 gives you nothing at all here - the outer {% for a in input.interests %} loop never runs, since interests only occurs once and V1 does not treat a single element as an array.
V2 gives you:
paranormal
paranormal 2
this is differentsince the outer loop runs once (treating the single interests element as a one-item array), letting you access its interest array and different-thing field from within the loop body, exactly as if there had been multiple interests elements to loop over.
Field names that clash with size, first or last
If the XML element itself has a child element or attribute literally named size, first or last, that real field always takes precedence over the array-style behaviour described above in V2 e.g. if a single <Item> element has a child <size> element, {{ input.Item.size }} would return the value of that <size> element rather than the count of Item elements. This matches how Liquid resolves properties generally - if the underlying object defines its own property with that name, it is used instead of any built-in behaviour.
AireGlu Defined Properties
There are some differences between V1 and V2 when it comes to casing. For example, for V1 you would access an endpoint task status code like so {{ endpoint.Tasks[0].StatusCode }}, however for V2 you must now use camelCase e.g. {{ endpoint.tasks[0].statusCode }}
For more information on the AireGlu data you can access, please see the Data Shape section of our Liquid docs. You can also find the receivers data shape here.