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.

plang
{{ 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.

plang
{{ 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, .first and .last work on lists and text. .size also 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 nil and prints nothing. It is never an error.
  • __proto__, constructor and prototype can'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.

plang
{{ '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:

TagWhat it does
if, elsif, else, endifConditions
unless, else, endunlessThe opposite of if (no elsif)
case, when, else, endcaseCompare one value with several
for, else, endforLoops, with break and continue
assignStore a value in a variable
capture, endcaptureStore a piece of markup in a variable
echoOutput a value (like {{ }}, handy inside tags)
renderInclude a snippet
shellSet the section wrapper's padding and class (see snippets)
comment, endcommentText that is thrown away
raw, endrawText written exactly as typed, {{ included
schema, endschemaThe section's settings, as JSON (see sections)
style, endstyleThe 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

plang
{% 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

plang
{% 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

plang
{% 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: and offset: take numbers or variables, and reversed flips the order. They apply in that order: offset, then limit, then reverse. limit: 0 shows nothing.
  • else runs 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 assign first: {% assign lines = text | lines %}{% for l in lines %}.
  • A text value loops once with the text itself. An object loops over [key, value] pairs. nil loops zero times.
  • {% break %} and {% continue %} only work inside a for.

Inside the loop forloop has:

PropertyMeaning
forloop.index, forloop.index0Position, counting from 1 or from 0
forloop.rindex, forloop.rindex0Position from the end, counting from 1 or from 0
forloop.first, forloop.lasttrue on the first or last round
forloop.lengthNumber of rounds
forloop.parentloopThe 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.

plang
{% for i in (1..5) %}<span class="dot{% if i <= s.rating %} on{% endif %}"></span>{% endfor %}

assign, capture and echo

plang
{% 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:

OperatorMeaning
==, != (also <>)Equal, not equal
<, >, <=, >=Smaller or greater
containsText contains text, or a list contains an item
and, orCombine conditions
plang
{% if s.layout == 'grid' %}
{% if s.title contains 'Promo' %}
{% if s.tags contains 'baru' %}
{% if s.rating >= 4 or s.featured %}

Things to know:

  • and and or are evaluated from right to left, with no precedence between them: a or b and c means a or (b and c), and a and b or c means a and (b or c). There are no parentheses, so split complex logic into several if tags or assign the 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 == false is 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 is nil the result is false.
  • contains on 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.

plang
{% 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:

TestTrue when
x == emptyx is '', an empty list or an empty object
x == blankx is nil, false, text that is only spaces, an empty list or an empty object
x != empty, x != blankThe opposite

nil == empty is false (a missing value is blank, not empty). The built-in sections use != blank for almost everything:

plang
{% 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:

plang
<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

plang
{% # 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:

MessageCause
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:

LimitValueWhen exceeded
Rendering steps per section200,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 section1,500,000 charactersThis template writes too much HTML.
render nesting8 levelsSnippets are nested more than 8 deep (does “x” render itself?).
Numbers in a range2000A range can have at most 2000 numbers.
Items from split, paragraphs, lines, concat5000A list can have at most 5000 items here.
Characters in one file200,000 when parsing, and 120,000 when saving a themeThe 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, entry and editing, not product, cart, shop or request. See objects.
  • Truthiness. nil, false, empty text and empty lists are false; 0 is true. An empty list is false in Plang, which Liquid treats as true.
  • No filters in conditions. assign the filtered value first.
  • and and or run right to left, and there are no parentheses.
  • No include. Use render, whose snippets see only what you pass them (plus the platform objects). See snippets.
  • capture keeps markup. The captured value is not escaped again when written out.
  • Output is always escaped. There is no raw-style output filter, and no escape need.
  • case runs only the first matching when, like a switch (Liquid runs every match).
  • Link objects. A link setting is an object that prints its address ({{ s.link }}) and has .href, .kind and .external. See objects.
  • for takes no filters in its target, and render needs 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 use list settings instead of Shopify's blocks.
  • No JavaScript. Plang can't produce scripts or event handlers; see security.

Where next

  • Objects lists everything a template can read.
  • Filters lists every filter with examples.
  • Snippets covers render, parameters and the platform snippets.
  • Examples has complete, working section files.