Skip to content

Latest commit

 

History

History
278 lines (210 loc) · 7.72 KB

SYNTAX.md

File metadata and controls

278 lines (210 loc) · 7.72 KB

syntax

Documents the template syntax.

Table of Contents

An upon template is simply a piece of UTF-8 text. It can be embedded in the binary or provided at runtime (e.g. read from a file). A template contains expressions for rendering values and blocks for controlling logic. These require you to use specific syntax delimiters in the template. Because upon allows you to configure these delimiters, this document will only refer to the default configuration.

Expressions

Expressions are available everywhere in templates and they can be emitted by wrapping them in {{ ... }}.

Literals

The simplest form of expressions are literals. The following types are available in templates.

  • Booleans: true, false
  • Integers: 42, 0o52, -0x2a
  • Floats: 0.123, -3.14, 5.23e10
  • Strings: "Hello World!", escape characters are supported: \r, \n, \t, \\, \"

Values

You can lookup up existing values in the current scope by name. The following would lookup the field “name” in the current scope and insert it into the rendered output.

Hello {{ name }}!

You can access nested fields using a dotted path. The following would first lookup the field “user” and then lookup the field “name” within it.

Hello {{ user.name }}!

You can also use this syntax to lookup a particular index of a list. For each of the expressions in the following code, first the field “users” is looked up, then a particular user is selected from the list by index. Finally, the field “name” is looked up from the selected user.

Hello {{ users.0.name }}!
And hello {{ users.1.name }}!
And also hello {{ users.2.name }}!

The dotted path syntax will raise an error when the field or index is not found. If you want to try lookup a field and return Value::None when it is not found then you can use the optional dotted path syntax. The following would try lookup the field “surname” from “user” and return Value::None if it is not found.

Hello {{ user.name }} {{ user?.surname }}!

Filters

Filters can be applied to existing expressions using the | (pipe) operator. The simplest filters take no extra arguments and are just specified by name. For example, assuming a filter called lower is registered in the engine the following would produce an expression with the user.name value transformed to lowercase.

{{ user.name | lower }}

Filters can also take arguments which must be a sequence of comma separated values or literals. In the following we lookup the value page.path and append a suffix to it.

{{ page.path | append: ".html" }}

See the filters module documentation for more information on filters.

Blocks

Blocks are marked with an opening {% ... %} and a closing {% ... %}.

Conditionals

Conditionals are marked using an opening if block and a closing endif block. It can also have zero or more optional else if clauses and an optional else clause. A conditional renders the contents of the block based on the specified condition which can be any expression. An expression can be negated by applying the prefix not. The conditional evaluates the expression based on it’s truthiness. The following values are considered falsy, every other value is truthy and will pass the condition:

  • None
  • Boolean false
  • An integer with value 0
  • A float with value 0.0
  • An empty string
  • An empty list
  • An empty map

Consider the following template. If the nested field user.is_enabled is returns false then the first paragraph would be rendered. Otherwise if user.has_permission returns true then the second paragraph would be rendered. If neither condition is satisfied then the HTML table would be rendered.

{% if not user.is_enabled %}
    <p>User is disabled</p>
{% else if user.has_permission %}
    <p>User has insufficient permissions</p>
{% else %}
    <table>...</table>
{% endif %}

Loops

Loops are marked using an opening for block and a closing endfor block. A loop renders the contents of the block once for each item in the specified sequence. This is done by unpacking each item into one or two variables. These variables are added to the scope within the loop block and shadow any variables with the same name in the outer scope. The specified sequence can be any expression but it must resolve to a list or map. Additionally, for lists there must a single loop variable and for maps there must be key and value loop variables.

Consider the following template. This would render an HTML paragraph for each user in the list.

{% for user in users %}
    <p>{{ user.name }}</p>
{% endfor %}

Here is an example where users is a map.

{% for id, user in users %}
    <div>
        <p>ID: {{ id }}</p>
        <p>Name: {{ user.name }}</p>
    </div>
{% endfor %}

Additionally, there are three special values available within loops.

  • loop.index: a zero-based index of the current value in the iterable
  • loop.first: true if this is the first iteration of the loop
  • loop.last: true if this is the last iteration of the loop
<ul>
{% for user in users %}
    <li>{{ loop.index }}. {{ user.name }}</li>
{% endfor %}
</ul>

With

“With” blocks can be used to create a variable from an expression. The variable is only valid within the block and it shadows any outer variables with the same name.

{% with user.names | join: " " as fullname %}
    Hello {{ fullname }}!
{% endwith %}

Include

“Include” blocks can be used to render nested templates. The nested template must have been registered in the engine before rendering. For example, assuming a template “footer” has been registered in the engine the following would render the template “footer” in the place of the include block. All variables in the current template will be available to the nested template.

<body>
    ...

    {% include "footer" %}

</body>

You can also include the nested using a specific context. In this case the nested template would not have any access to the current template’s variables and path.to.footer.info would form the global context for the nested template.

<body>
    ...

    {% include "footer" with path.to.footer.info %}

</body>

Self-referential templates and include cycles are allowed but the maximum include depth is restricted by the engine setting set_max_include_depth.

Whitespace control

If an expression or block includes a hyphen - character, like {{-, -}}, {%-, and -%} then any whitespace in the template adjacent to the tag will be skipped when the template is rendered.

Consider the following template.

Hello,
    {% if user.is_welcome %} and welcome, {% endif %}
    {{ user.name }}!

This doesn’t use any whitespace trimming so it would be rendered something like this:

Hello,
     and welcome,
    John!

We can add whitespace trimming like this.

Hello,
    {%- if user.is_welcome %} and welcome, {% endif -%}
    {{ user.name }}!

Now it will be rendered without the newlines or extra spaces.

Hello, and welcome, John!