Expressions¶
An expression is the fundamental unit of computation within a template. Every syntactically correct expression is guaranteed to resolve to a value.
Expressions appear, for example, within output delimiters to render data directly to the page ({{ user.name | upcase }}), inside conditional tags to govern template logic ({% if item.price > 100 %}), and as the data sources for iterations ({% for product in collections.frontpage %}).
Here we'll give a brief tour of the expression language used in Luoma templates. See also the formal expression language specification.
Literals¶
Literals represent fixed values directly inside an expression.
null (alias nil) represents a Null data value. It is not to be confused with Nothing, which indicates the absence of a data value.
Booleans true and false represent True and False data values, respectively.
Literal numbers can be integers or decimals, and use scientific notation.
String literals can be single-quoted or double-quoted. Both support JavaScript-style string interpolation and JSON-like escape sequences.
{%- assign
greeting = 'Hello',
name = "Sue",
emoji = "\uD83D\uDE00",
-%}
{{ "${greeting}, ${name}! ${emoji}" }}
Array literals are lists of comma separated expressions surrounded by square brackets. The spread operator (...) expands a collection into the array literal.
{%- assign
a = [1, 2, 3],
b = ["a", "b", ["c", "d"]],
c = [foo or 99, ...a, (b | flatten)],
-%}
{{ a }}
{{ b }}
{{ c }}
Object literals use JavaScript-style braces ({ and }) and colons (:). Quotes around keys are optional if they are simple identifiers.
{%- assign
obj = {
a: 1,
"b": 2,
'c': [3,4,5],
d: {"foo": "bar"}
}
-%}
{{ obj }}
{{ obj | json: pretty=true }}
{"a":1,"b":2,"c":[3,4,5],"d":{"foo":"bar"}}
{
"a": 1,
"b": 2,
"c": [
3,
4,
5
],
"d": {
"foo": "bar"
}
}
A range literal represents a lazy sequence of increasing integers. The start and end (inclusive) integers are separated by two dots (..) and surrounded by parentheses, (1..5).
Variables and Property Access¶
Access variables by name. If a variable name contains reserved characters, use bracket notation.
Object properties are accessed using dot notation, or brackets when the property name contains reserved characters.
Array items are accessed by their zero-based index and bracket notation. Dotted indexes are not allowed.
A trailing path segment ending with a question mark (?) is a predicate. A predicate "queries" the value obtained by resolving preceding path segments and returns true or false. See the predicate reference for details of all built-in predicate functions.
Operators & Precedence¶
| Priority | Operator Category | Operators / Syntax | Examples |
|---|---|---|---|
| 1 (Highest) | Primary & Grouping | Literals, Variables, (...), Lambdas |
(a + b), x -> x.id |
| 2 | Unary | -, + |
-count |
| 3 | Multiplicative | *, /, % |
total * tax_rate |
| 4 | Additive | +, - |
price + shipping |
| 5 | Filter Pipe | | |
name | upcase |
| 6 | Comparison & Tests | ==, !=, <, >, <=, >=, in, contains |
item.qty > 0, tag in tags |
| 7 | Logical NOT | not |
not user.logged_in |
| 8 | Logical AND | and |
has_stock and visible |
| 9 | Logical OR | or |
is_admin or is_owner |
| 10 | Null Coalescing | orElse |
title orElse fallback_title |
| 11 (Lowest) | Inline Conditional | ... if ... else ... |
a if condition else b |
Filters and arguments¶
Filters transform values using the pipe (|) operator. Because filters exist directly within the expression hierarchy, they can be chained and used anywhere an expression is expected.
Positional arguments are separated by commas, e.g., value | slice: 0, 5.
Keyword arguments are specified using : or =, e.g., font | font_face: font_display = 'swap'.
TODO: finish me
Lambda expressions¶
Lambdas allow us to pass expressions a callbacks to filters, or save an expression to a variable for later evaluation.
A lambda expression starts with an arguments list, followed by an arrow symbol (-> or =>), then an expression. When evaluated - either as a filter callback or explicitly with the pipe (|) operator - the lambda's arguments are in scope, along with other variables from the scope in which the expression is "called". Lambdas capture nothing about the scope in which they are defined. They are not closures.
{{ items | map: (item) -> (item.stock_count > 0) }}
{% assign is_valid = (item) -> (item.price > 0 and item.in_stock) %}
{{ items | map: is_valid }}
When passed to a filter that expects an array input, lambdas will be evaluated at least once for each item in the input array, with the array item being passed as the first argument. The optional second argument to the lambda expression is the item's index.
TODO: finish me
Ternary expressions¶
If the else branch is omitted and the condition evaluates to a falsy value, the entire ternary expression evaluates to the special value Nothing (This is a deviation from the spec. In practice it can be quite inconvenient to be forced to write else null after every inline condition).
TODO: finish me
Short circuiting operators¶
TODO: