Plang reference
Language
The syntax of Plang, the template language of Plangku themes: output, filters, tags, operators, truthiness, errors and limits.
Plang is the template language of Plangku themes. Section files (sections/*.plang) and snippet files (snippets/*.plang) are HTML with Plang inside. If you have written a Shopify theme the syntax will feel familiar ({{ output | filter }} and {% tags %}), but Plang is Plangku's own engine. It has its own objects, filters and rules, and it does not run Liquid. Differences from Liquid are called out where they matter and collected at the end of this page.
One engine runs everywhere: on the server for the live site, previews and theme saves, and in the browser for the editor canvas and the code editor's checks. Whatever the editor shows is what visitors get.
Output and escaping
{{ ... }} writes a value. Output is always HTML-escaped, so <, >, & and quotes in a value become harmless text.
{{ section.settings.heading }}
{{ site.name | upcase }}
{{ 'Pesan lewat WhatsApp' }}Only a few filters return markup that is written as-is: icon, markdown and newline_to_br. Their output is built by the platform, with every value inside it already escaped. Everything else is escaped. escape exists for Liquid habits; its result isn't escaped a second time, but you never need it.
Whatever your template produces is also cleaned afterwards by the security allowlist, so escaping is about correct text, not about protecting the site.
Strings use single or double quotes. There are no escape sequences inside a string. Numbers can be written 5, -2 or 1.5. true, false, nil (or null), empty and blank are keywords.
Values and paths
Names are letters, digits, _ and - (starting with a letter or _). A hyphen is part of a name, so a-1 is one name, not a subtraction: use the minus filter.
{{ section.settings.title }}
{{ s.items[0].title }}
{{ s.items.first.title }}
{{ s.items[-1].title }}
{{ s.items.size }}
{{ product["title"] }}
{{ s.items[i].title }}.size,.firstand.lastwork on lists and text..sizealso works on an object (its number of keys).- A negative index counts from the end:
items[-1]is the last item. ["key"]reads a property by a quoted name or by a variable.- A missing variable or property is
niland prints nothing. It is never an error. __proto__,constructorandprototypecan't be used as names.
The objects a template can read are listed on the objects page.
Filters
A filter changes a value. Chain them with |. Arguments follow a colon, separated by commas. Some filters take named arguments written name: value; positional and named arguments can be mixed.
{{ 'kopi susu' | capitalize | append: ' gula aren' }}
{{ 'Menu' | icon: class: 'size-5', stroke: 1.5 }}
{{ price | default: 'Hubungi kami' }}Filters are checked when the file is saved: an unknown name is an error (There is no filter called “xyz”.). The full list is on the filters page.
Tags
Tags use {% ... %}. These exist, and no others:
| Tag | What it does |
|---|---|
if, elsif, else, endif | Conditions |
unless, else, endunless | The opposite of if (no elsif) |
case, when, else, endcase | Compare one value with several |
for, else, endfor | Loops, with break and continue |
assign | Store a value in a variable |
capture, endcapture | Store a piece of markup in a variable |
echo | Output a value (like {{ }}, handy inside tags) |
render | Include a snippet |
shell | Set the section wrapper's padding and class (see snippets) |
comment, endcomment | Text that is thrown away |
raw, endraw | Text written exactly as typed, {{ included |
schema, endschema | The section's settings, as JSON (see sections) |
style, endstyle | The section's CSS (see styling) |
{% schema %} and {% style %} must be at the top level of the file, not inside an if or for. A section has one schema and may have several style blocks. Both are removed from the output.
Anything else is an error: There is no {% foo %} tag. In particular there is no include (use render), no liquid, increment, tablerow, paginate, layout or form tag.
if and unless
{% if s.price > 100000 and s.badge != blank %}
<span class="badge">{{ s.badge }}</span>
{% elsif s.price > 0 %}
{{ s.price | money }}
{% else %}
Hubungi kami
{% endif %}
{% unless s.items.size == 0 %}<ul>...</ul>{% endunless %}case and when
{% case s.layout %}
{% when 'grid' %}<div class="grid">...</div>
{% when 'list', 'rows' %}<ul>...</ul>
{% else %}<div>...</div>
{% endcase %}when takes several values separated by commas or or. Every when that matches runs, not only the first, and else runs only when none matched. Text between case and the first when is ignored.
for loops
{% for item in s.items limit: 6 offset: 2 reversed %}
<li class="{% if forloop.first %}first{% endif %}">{{ forloop.index }}. {{ item.title }}</li>
{% else %}
<li>Belum ada produk.</li>
{% endfor %}limit:andoffset:take numbers or variables, andreversedflips the order. They apply in that order: offset, then limit, then reverse.limit: 0shows nothing.elseruns when there is nothing to loop over.- The loop target is a plain value or a range. You can't put a filter in it, so use
assignfirst:{% assign lines = text | lines %}{% for l in lines %}. - A text value loops once with the text itself. An object loops over
[key, value]pairs.nilloops zero times. {% break %}and{% continue %}only work inside afor.
Inside the loop forloop has:
| Property | Meaning |
|---|---|
forloop.index, forloop.index0 | Position, counting from 1 or from 0 |
forloop.rindex, forloop.rindex0 | Position from the end, counting from 1 or from 0 |
forloop.first, forloop.last | true on the first or last round |
forloop.length | Number of rounds |
forloop.parentloop | The forloop of the loop around this one, or nil |
Ranges
(1..5) is the list 1, 2, 3, 4, 5. Both ends can be variables: (1..s.count). A range that ends below its start is empty. At most 2000 numbers.
{% for i in (1..5) %}<span class="dot{% if i <= s.rating %} on{% endif %}"></span>{% endfor %}assign, capture and echo
{% assign s = section.settings %}
{% assign label = s.label | default: 'Pesan' | upcase %}
{% capture whatsapp_icon %}{{ 'whatsapp' | icon: class: 'size-5' }}{% endcapture %}
{{ whatsapp_icon }}{{ label }}
{% echo s.title | truncate: 30 %}assign always writes to the variable scope of the file, even inside a for, so the value is still there after the loop. Variables don't leak into snippets or out of them.
capture stores what the body writes. Unlike Liquid's plain strings, a captured value keeps its markup: the body was written by your template (values inside were escaped as they were written), so writing the variable out later does not escape it again. Running a filter on the captured value turns it back into plain text, which is escaped on output.
Operators
Conditions (in if, unless, elsif) can use:
| Operator | Meaning |
|---|---|
==, != (also <>) | Equal, not equal |
<, >, <=, >= | Smaller or greater |
contains | Text contains text, or a list contains an item |
and, or | Combine conditions |
{% if s.layout == 'grid' %}
{% if s.title contains 'Promo' %}
{% if s.tags contains 'baru' %}
{% if s.rating >= 4 or s.featured %}Things to know:
andandorare evaluated from right to left, with no precedence between them:a or b and cmeansa or (b and c), anda and b or cmeansa and (b or c). There are no parentheses, so split complex logic into severaliftags orassignthe parts.- Filters are not allowed in a condition.
{% if title | size > 3 %}is an error (Filters can’t be used in conditions. {% assign %} the filtered value first.). Write{% assign n = title | size %}{% if n > 3 %}. ==is forgiving:1 == '1'is true, because a number and a numeric text compare as numbers.nil == falseis false.<and>compare numbers when both sides look like numbers (even as text:'10' > '9'is true), otherwise they compare text alphabetically. If either side isnilthe result is false.containson a text is a substring check. On a list it compares items with==. On anything else it is false.
Truthiness
In an if, a value is false when it is false, nil, an empty text or an empty list. Everything else is true. In particular 0 is true, and so is the text '0', as in Liquid. An empty link (a link setting you left blank) is false too, so {% if s.link %} is a good test.
{% assign zero = 0 %}
{% if zero %}0 is true{% endif %}
{% if '' %}never{% else %}empty text is false{% endif %}This is why default fills in only for nil, false, empty text and empty lists, never for 0.
empty and blank
Compare with the keywords empty and blank to test emptiness:
| Test | True when |
|---|---|
x == empty | x is '', an empty list or an empty object |
x == blank | x is nil, false, text that is only spaces, an empty list or an empty object |
x != empty, x != blank | The opposite |
nil == empty is false (a missing value is blank, not empty). The built-in sections use != blank for almost everything:
{% if s.eyebrow != blank %}<p class="eyebrow">{{ s.eyebrow }}</p>{% endif %}
{% if s.items == empty %}Belum ada data{% endif %}Whitespace control
A dash inside the delimiters removes all whitespace (spaces and line breaks) on that side:
<ul>
{%- for item in s.items -%}
<li>{{ item.title }}</li>
{%- endfor -%}
</ul>{{-, -}}, {%- and -%} all work. Whitespace in your HTML rarely matters, but it does inside inline elements and white-space: pre-line text.
Comments
{% # A one-line comment, a tag that starts with # %}
{% comment %}
Anything in here is ignored, even {{ broken syntax }}.
{% endcomment %}
{% raw %}{{ this is written exactly as typed }}{% endraw %}Comments don't nest, and neither do raw blocks.
Errors and line numbers
Plang checks a file when you save it. A syntax problem is an error with a file name and a line number, shown in the code editor's problem list, and saving is refused while any file has an error. Warnings (for example from CSS) don't block saving.
Typical messages:
| Message | Cause |
|---|---|
This {{ is never closed with }}. | A missing closing delimiter |
{% if %} on line 3 needs a matching {% endif %}. | An unclosed tag |
There is no filter called “xyz”. | Unknown filter name |
There is no {% xyz %} tag. | Unknown tag |
Use {% render 'snippet-name' %}; {% include %} isn’t supported. | include was used |
Filters can’t be used in conditions. ... | A filter inside if |
{% unless %} can’t have {% elsif %}. Use {% if %} instead. | elsif inside unless |
{% break %} only works inside {% for %}. | break or continue outside a loop |
The snippet “card” doesn’t exist. Create snippets/card.plang. | render of an unknown snippet |
Empty {{ }}: write a value inside, like {{ section.settings.heading }}. | {{ }} with nothing inside |
Problems that only show up while rendering (a limit being hit, a snippet that renders itself) are caught per section. In the editor canvas and in previews the section shows a red box: Theme code problem. sections/hero.plang, line 12: .... On the live site the section writes nothing, so one broken section never takes down a page.
Limits
Theme code can't hang the server, so every render has hard limits:
| Limit | Value | When exceeded |
|---|---|---|
| Rendering steps per section | 200,000 (every node, loop round and filter call counts) | This template does too much work (too many loop rounds). Show fewer items or simplify the loops. |
| Output per section | 1,500,000 characters | This template writes too much HTML. |
render nesting | 8 levels | Snippets are nested more than 8 deep (does “x” render itself?). |
| Numbers in a range | 2000 | A range can have at most 2000 numbers. |
Items from split, paragraphs, lines, concat | 5000 | A list can have at most 5000 items here. |
| Characters in one file | 200,000 when parsing, and 120,000 when saving a theme | The file is refused |
The steps and output budgets are shared by a section and every snippet it renders. Limits on a whole theme (at most 150 files, 80 sections, 120 snippets, 12 CSS files, 60 images, and 1,500,000 characters of code in total) are enforced when you save.
Nested loops multiply: two nested loops over 2000 numbers each is 4 million rounds and will stop with the "too much work" error. Loop over the items you actually show (limit:).
Differences from Shopify Liquid
- Plang's own objects. There is
section,settings,theme,site,page,entryandediting, notproduct,cart,shoporrequest. See objects. - Truthiness.
nil,false, empty text and empty lists are false;0is true. An empty list is false in Plang, which Liquid treats as true. - No filters in conditions.
assignthe filtered value first. andandorrun right to left, and there are no parentheses.- No
include. Userender, whose snippets see only what you pass them (plus the platform objects). See snippets. capturekeeps markup. The captured value is not escaped again when written out.- Output is always escaped. There is no
raw-style output filter, and noescapeneed. caseruns only the first matchingwhen, like a switch (Liquid runs every match).- Link objects. A link setting is an object that prints its address (
{{ s.link }}) and has.href,.kindand.external. See objects. fortakes no filters in its target, andrenderneeds a literal snippet name.{% style %}and{% schema %}are Plangku's own: the CSS is scoped to the section automatically, and the schema has its own setting types. Sections uselistsettings instead of Shopify's blocks.- No JavaScript. Plang can't produce scripts or event handlers; see security.