# Vascula documentation (v0.1.5)
Source: https://vascula.dev
---
# Vascula
URL: https://vascula.dev/
Vascula is a template language for HTML. You write markup with `{{ output }}`
and `{% tags %}`; an application compiles the template once and renders it
with data.
```vascula
```
The output is escaped for HTML automatically, so the `&` and the markup
inside the data cannot break the page.
## What makes it different
**Safe by default.** Everything you print is HTML-escaped. There is no `raw`
filter and no way for a template to switch escaping off; only the application
can hand over trusted HTML. JSON printed inside an HTML attribute is escaped
for that position too, and a link or other URL with printed parts cannot be a
`javascript:` URL.
**Explicit about data.** A template can only read the data roots the
application allows. Reading anything else is an error, not a silent blank, and
the application can list every name a template reads before it ever renders.
**Bounded.** Compilation and rendering have limits on source size, nesting,
work, output and value size. A template cannot loop forever or build a
gigabyte string.
**Fast.** A 48-product grid renders in about 100 microseconds, three times
faster than the popular Liquid implementations in Go and Ruby. See
[Performance](/performance).
**Familiar.** The syntax will look familiar if you know Liquid or Jinja, but
Vascula is its own language with documented behavior. See
[Coming from Liquid](/from-liquid).
## Where to start
| You want to | Read |
|---|---|
| Write templates | [Syntax](/syntax), then [the tutorial](/tutorial) |
| Look up a filter or tag | [Filters](/filters), [Conditionals](/conditionals), [Loops](/loops) |
| Embed Vascula in a Go program | [Getting started](/getting-started), then the [Go API](/go-api) |
| Extend it with your own filters and tags | [Extending Vascula](/extending) |
| Understand what it protects against | [Safety and limits](/safety) |
| Generate templates with an AI agent | [Vascula for agents](/agents) and [llms.txt](/llms.txt) |
| Build a theme for a Tringify store | [Tringify theme docs](https://dev-docs.tringify.com/themes/) |
## Made by Tringify
Vascula is made by [Tringify](https://tringify.com) and renders every Tringify
storefront. Building a theme for a Tringify store? See the
[Tringify theme docs](https://dev-docs.tringify.com/themes/).
## Versions
Vascula is at version 0.1. Until 1.0, a release can change the language or
the Go API; the [changelog](/changelog) lists every change.
Every page of this site is also available as Markdown: add `.md` to the
address, or read [llms-full.txt](/llms-full.txt) for the whole site in one file.
---
# Getting started
URL: https://vascula.dev/getting-started
Vascula is a Go library. This page takes you from nothing to a rendered page.
## Requirements
- Go 1.25 or later.
- Add the module: `go get vascula.dev/vascula@latest`.
The package has no third-party dependencies.
## Render your first template
```go run
package main
import (
"fmt"
"vascula.dev/vascula"
)
func main() {
tmpl, err := vascula.Compile(`
```
Two steps, always in this order:
1. **Compile** parses the source into a `*Template`. Do it once and keep the
template: compiling is the expensive part, and a compiled template is safe
to render from many goroutines at once.
2. **Render** evaluates the template with `Options` and returns the HTML.
`settings` is the template's own parameters. It is always readable, without
any permission.
## Give the template data
Application data goes in `Options.Data`, keyed by root name. A template can
only read the roots listed in `Options.Allow`.
```go run
package main
import (
"fmt"
"vascula.dev/vascula"
)
func main() {
tmpl, err := vascula.Compile(`{% for p in catalog.products %}{{ p.title }} costs {{ p.price }}.
{% endfor %}`)
if err != nil {
panic(err)
}
html, err := tmpl.Render(vascula.Options{
Data: map[string]interface{}{
"catalog": map[string]interface{}{
"products": []interface{}{
map[string]interface{}{"title": "Cup", "price": 12},
map[string]interface{}{"title": "Saucer", "price": 4.5},
},
},
"secrets": "not for templates",
},
Allow: []string{"catalog"},
})
if err != nil {
panic(err)
}
fmt.Print(html)
}
```
```output
Cup costs 12.
Saucer costs 4.5.
```
`secrets` is in `Data` but not in `Allow`, so the template cannot read it.
If it tried, rendering would fail with an `UndeclaredNameError`.
Use JSON-shaped values: `map[string]interface{}`, `[]interface{}`, strings,
numbers, booleans and `nil`. Data decoded with `encoding/json` already has this
shape.
## Handle errors
Compilation fails on malformed source; rendering fails on things that can only
be known with data, such as reading a name that is not allowed. Both errors say
where the problem is.
```go run
package main
import (
"errors"
"fmt"
"vascula.dev/vascula"
)
func main() {
_, err := vascula.Compile("
\n{% if settings.on %}open")
fmt.Println(err)
tmpl, _ := vascula.Compile("
{{ user.name }}
")
_, err = tmpl.Render(vascula.Options{})
fmt.Println(err)
var undeclared *vascula.UndeclaredNameError
fmt.Println(errors.As(err, &undeclared), undeclared.Name)
}
```
```output
{% if %} is never closed with {% endif %} (line 2, col 1)
undeclared name "user": it is not a variable or an allowed data root (line 1, col 7)
true user
```
Show these messages to the person who wrote the template. Do not show them, or
the template source, to the people visiting your site. See
[Errors](/errors) for every message and what to do about it.
## Next steps
- Learn the language: [Syntax](/syntax) and [the tutorial](/tutorial).
- Learn the Go API in depth: [Go API](/go-api) and [Embedding](/embedding).
- Add your own filters and tags: [Extending Vascula](/extending).
---
# Tutorial
URL: https://vascula.dev/tutorial
In this tutorial you build the product list of a small tea shop, one feature
at a time. Each step shows the template, the data it is rendered with, and
the output. You need nothing installed to follow along.
The data is the same throughout. The application gives the template one data
root, `shop`, and two settings:
```json
{
"data": {"shop": {
"name": "Leaf & Pot",
"products": [
{"title": "Sencha", "price": 1250, "stock": 12, "tags": ["green", "japan"]},
{"title": "Assam ", "price": 899, "stock": 0, "tags": ["black"]},
{"title": "Oolong", "price": 1575, "stock": 3, "tags": []}
]
}},
"allow": ["shop"],
"settings": {"heading": "Our teas", "show_tags": true}
}
```
## 1. Print a value
`{{ }}` prints a value. `shop.name` reads the field `name` of the root `shop`;
`settings.heading` reads a setting.
```vascula
```
The `&` was escaped automatically. Everything you print is.
## 2. Loop over the products
`{% for %}` repeats its body for each item. Inside, `product` is the current
item.
```vascula
```
A product title containing markup cannot break the page: it is printed as
text.
## 3. Format the price with filters
Prices are in cents. Filters, written after `|`, transform a value, and you
can chain them: divide by 100, then round to two decimal places. Notice that
`12.5` prints without a trailing zero: proper money formatting is a job for a
`money` filter that the application provides (see
[Extending](/extending#filters)).
```vascula
{% for product in shop.products %}{{ product.title }}: €{{ product.price | divided_by: 100.0 | round: 2 }}
{% endfor %}
```
```json data
{"data": {"shop": {"products": [{"title": "Sencha", "price": 1250}, {"title": "Oolong", "price": 1575}]}}, "allow": ["shop"]}
```
```output
Sencha: €12.5
Oolong: €15.75
```
## 4. Show stock with conditions
`{% if %}` chooses what to show. Conditions compare values with `==`, `>`
and the other operators.
```vascula
{% for product in shop.products %}{{ product.title }}: {% if product.stock > 5 %}in stock{% elsif product.stock > 0 %}only {{ product.stock }} left{% else %}sold out{% endif %}
{% endfor %}
```
```json data
{"data": {"shop": {"products": [{"title": "Sencha", "stock": 12}, {"title": "Assam", "stock": 0}, {"title": "Oolong", "stock": 3}]}}, "allow": ["shop"]}
```
```output
Sencha: in stock
Assam: sold out
Oolong: only 3 left
```
## 5. List the tags, or nothing
`join` turns an array into text. An empty array is still *truthy*, so test its
size rather than the array itself. The setting `show_tags` lets the shop
owner switch tags off.
```vascula
{% for product in shop.products %}{{ product.title }}{% if settings.show_tags and product.tags.size > 0 %} ({{ product.tags | join: ", " }}){% endif %}
{% endfor %}
```
```json data
{"data": {"shop": {"products": [{"title": "Sencha", "tags": ["green", "japan"]}, {"title": "Oolong", "tags": []}]}}, "allow": ["shop"], "settings": {"show_tags": true}}
```
```output
Sencha (green, japan)
Oolong
```
## 6. Count with variables
`assign` stores a value. Count the products in stock as you go, and print the
total after the loop.
```vascula
{% assign available = 0 %}{% for product in shop.products %}{% if product.stock > 0 %}{% assign available = available | plus: 1 %}{% endif %}{% endfor %}{{ available }} of {{ shop.products.size }} teas available
```
```json data
{"data": {"shop": {"products": [{"stock": 12}, {"stock": 0}, {"stock": 3}]}}, "allow": ["shop"]}
```
```output
2 of 3 teas available
```
The same in one line, with filters: `{{ shop.products | where: "stock" | size }}`
would not work here, because a stock of `0` is truthy. Variables are clearer.
## 7. Move the card into its own template
The product markup is getting long. `{% render %}` renders another template,
and passes it what it needs as arguments. The child sees only its arguments,
so it cannot accidentally depend on the page around it.
```vascula
{% for product in shop.products %}{% render "product-card", product: product %}{% endfor %}
```
The `-` inside `{%-` and `-%}` removes the whitespace around those tags, which
keeps the output tidy without cramming the template onto one line. See
[Whitespace control](/syntax#whitespace-control).
## Where next
- The rest of the language: [Variables](/variables), [Loops](/loops),
[Filters](/filters).
- Rendering this from Go: [Getting started](/getting-started).
---
# Syntax
URL: https://vascula.dev/syntax
A template is text with three kinds of markup in it:
| Markup | Meaning |
|---|---|
| `{{ expression }}` | **Output.** Evaluate the expression and print it, HTML-escaped. |
| `{% tag ... %}` | **Tag.** Logic: conditions, loops, variables, composition. Prints nothing itself. |
| `{# comment #}` | **Comment.** Removed from the output. |
Everything else is literal text and is printed exactly as written.
```vascula
Hello, {{ settings.name }}.
{% if settings.vip %} Welcome back!{% endif %}{# greeting #}
```
```json data
{"settings": {"name": "Ada", "vip": true}}
```
```output
Hello, Ada.
Welcome back!
```
## Output
`{{ }}` takes one expression: a value, optionally passed through filters with
`|`.
```vascula
{{ "vascula" }} {{ 42 }} {{ settings.name }} {{ settings.name | upcase }}
```
```json data
{"settings": {"name": "Ada"}}
```
```output
vascula 42 Ada ADA
```
Output is always escaped for HTML. To learn what that covers and what it does
not, see [Output and escaping](/output).
## Tags
Tags start with a keyword. Some stand alone (`assign`, `render`, `cycle`,
`break`), and some wrap a body that ends with a matching `end` tag (`if` …
`endif`, `for` … `endfor`, `case` … `endcase`, `capture` … `endcapture`).
```vascula
{% assign greeting = "Hi" %}{% for n in (1..3) %}{{ greeting }} {{ n }}. {% endfor %}
```
```output
Hi 1. Hi 2. Hi 3.
```
The full list is on the pages for [variables](/variables),
[conditionals](/conditionals), [loops](/loops) and
[composition](/composition). An unknown tag is a compilation error.
## Whitespace control
Tags leave the line breaks around them in the output. Put `-` just inside a
delimiter to remove all whitespace, including newlines, on that side.
```vascula
{%- for x in (1..2) %}
{{ x }}
{%- endfor %}
```
```output
1
2
```
`{%-` and `{{-` trim before the tag; `-%}` and `-}}` trim after it. Comments
accept `{#-` and `-#}` the same way.
## Comments
`{# ... #}` is removed from the output and can span lines. For a longer
comment, or to switch off a piece of template while you work, use the
`comment` block: everything up to `{% endcomment %}` is ignored, including
tags.
```vascula
Visible{# not visible #}{% comment %}{% if broken %}ignored{% endcomment %}.
```
```output
Visible.
```
An unterminated comment is a compilation error rather than a comment that
silently swallows the rest of the template.
## Names
Names are made of letters, digits and underscores and start with a letter or
underscore: `product`, `item_count`, `_draft`. Names are case-sensitive.
## Text is never evaluated
Text that comes from data is printed, never run. If a value contains
`{{ something }}`, the page shows those characters.
```vascula
{{ settings.note }}
```
```json data
{"settings": {"note": "{{ settings.secret }}"}}
```
```output
{{ settings.secret }}
```
## What is not supported
These are compilation errors, so a mistake never reaches a live page:
| You may know | In Vascula |
|---|---|
| `{% raw %}` and the `raw` filter | Not available. Output is always escaped. |
| `{% include %}` | Use `{% render %}`; see [Composition](/composition). |
| `{% liquid %}`, `{% schema %}`, `{% javascript %}`, `{% stylesheet %}`, `{% section %}` | Not part of the language. An application can add its own tags; see [Extending Vascula](/extending). |
| Arithmetic operators (`a + b`) | Use filters: `{{ a \| plus: b }}`. |
| Array literals (`[1, 2]`) | Use a range `(1..3)` or `split`. |
The next page covers the values you can write and how they behave:
[Values and truthiness](/values).
---
# Values and truthiness
URL: https://vascula.dev/values
## Kinds of values
| Kind | Written as | Notes |
|---|---|---|
| String | `"text"` or `'text'` | A backslash takes the next character literally: `"say \"hi\""`. There are no escape sequences such as `\n`. |
| Integer | `42`, `-7` | Exact 64-bit integers. |
| Decimal | `3.14`, `-0.5` | 64-bit floating point. Exponent notation is not accepted. |
| Boolean | `true`, `false` | |
| Nothing | `nil` or `null` | Prints nothing. |
| Empty | `empty` or `blank` | Only for comparisons; see [Empty](#empty-and-blank). |
| Range | `(1..5)` | A list of integers; see [Loops](/loops#ranges). |
| Array, map | from data only | There are no array or map literals. |
```vascula
{{ "double" }} {{ 'single' }} {{ "say \"hi\"" }} {{ 42 }} {{ -0.5 }} {{ true }} [{{ nil }}]
```
```output
double single say "hi" 42 -0.5 true []
```
### Integers stay exact
A number without a decimal point is an exact integer, and arithmetic between
two integers stays exact. This matters for large identifiers.
```vascula
{{ 9007199254740993 | plus: 1 }} {{ 10 | divided_by: 4 }} {{ 10 | divided_by: 5 }}
```
```output
9007199254740994 2.5 2
```
Division keeps a fractional result. A calculation that overflows 64 bits
falls back to floating point. Data from the application keeps its own type:
numbers decoded from JSON are floating point.
## Truthiness
Conditions such as `{% if %}` test whether a value is *truthy*.
| Value | In a condition |
|---|---|
| `false` | false |
| `nil`, a missing field | false |
| `true` | true |
| `0`, `0.0` | **true** |
| `""` (empty string) | **true** |
| `[]`, `{}` (empty array or map) | **true** |
| any other value | true |
Only `false` and nothing are false. To ask "is there anything here?", compare
with `blank`, or check `.size`.
```vascula
{% if settings.zero %}0 is true{% endif %}, {% if settings.text %}"" is true{% endif %}, {% if settings.missing %}{% else %}missing is false{% endif %}
```
```json data
{"settings": {"zero": 0, "text": ""}}
```
```output
0 is true, "" is true, missing is false
```
## Empty and blank
`empty` and `blank` are the same marker. A value equals it when the value is
nothing, an empty string, or a collection with no entries. A string of spaces
is **not** blank.
```vascula
{% if settings.title == blank %}No title{% endif %} / {% if settings.tags == empty %}No tags{% endif %} / {% if settings.space == blank %}blank{% else %}not blank{% endif %}
```
```json data
{"settings": {"title": "", "tags": [], "space": " "}}
```
```output
No title / No tags / not blank
```
## Comparing values
| Operator | Meaning |
|---|---|
| `==`, `!=` | Equal, not equal. |
| `<`, `>`, `<=`, `>=` | Order. Both sides must be numbers, or both strings. |
| `contains` | Substring of a string, or member of an array. |
Equality is forgiving: numbers compare as numbers, and a string that is a
number equals that number. Anything else compares by its printed text.
```vascula
{{ 1 == 1.0 }} {{ "5" == 5 }} {{ "a" == "a" }} {{ nil == nil }} {{ "1" != 2 }}
```
```output
true true true true true
```
Ordering is strict. Comparing a number with a string, or anything that is not
a number or string, is an error, never a silent `false`.
```vascula
{{ 2 > 1 }} {{ "b" > "a" }} {{ 10 >= 10 }}
```
```output
true true true
```
```vascula
{% if settings.count > "3" %}more{% endif %}
```
```json data
{"settings": {"count": 5}}
```
```error
cannot compare number and string with ">"
```
`contains` looks for a substring in a string, or an equal member in an array.
It is always false for maps and other values.
```vascula
{{ "teapot" contains "tea" }} {{ settings.tags contains "new" }} {{ settings.tags contains "old" }}
```
```json data
{"settings": {"tags": ["new", "sale"]}}
```
```output
true true false
```
## How values print
| Value | Prints as |
|---|---|
| String | itself, HTML-escaped |
| Integer | digits: `42` |
| Decimal | shortest exact form: `2.5`, `3` for `3.0` |
| Boolean | `true` or `false` |
| `nil` | nothing |
| Array | its items printed one after another, with no separator; use `join` |
| Map | a debugging form; print its fields instead |
```vascula
{{ settings.list }} | {{ settings.list | join: ", " }} | {{ 3.0 }}
```
```json data
{"settings": {"list": ["a", "b", "c"]}}
```
```output
abc | a, b, c | 3
```
Next: [Variables, data and scope](/variables).
---
# Variables, data and scope
URL: https://vascula.dev/variables
## Where names come from
When a template reads a name such as `product` in `{{ product.title }}`,
Vascula looks it up in this order:
1. **Variables** the template created with `assign` or `capture`, and loop
variables inside their loop.
2. **`settings`**: the template's own parameters. Always readable.
3. **Globals**: names the application makes readable everywhere, such as
`theme` in a theme system.
4. **Data roots** the application allowed for this template.
Anything else is an error, so a typo cannot quietly print nothing:
```vascula
{{ produt.title }}
```
```json data
{"data": {"product": {"title": "Cup"}}, "allow": ["product"]}
```
```error
undeclared name "produt"
```
The application decides which data roots a template may read (see
[Embedding](/embedding#data-and-allow)). A root in the data but not allowed is
just as unreadable as a name that does not exist.
## Reading fields
Use a dot for a field and brackets for a key or position.
```vascula
{{ product.title }} / {{ product["title"] }} / {{ product.tags[0] }} / {{ product.tags[-1] }} / {{ product.tags[settings.i] }}
```
```json data
{"data": {"product": {"title": "Cup", "tags": ["new", "sale", "blue"]}}, "allow": ["product"], "settings": {"i": 1}}
```
```output
Cup / Cup / new / blue / sale
```
A negative position counts from the end. A missing field or an out-of-range
position is `nil`, which prints nothing; only the first name of a path must
exist.
```vascula
[{{ product.missing }}] [{{ product.missing.deeper }}] [{{ product.tags[9] }}]
```
```json data
{"data": {"product": {"tags": ["new"]}}, "allow": ["product"]}
```
```output
[] [] []
```
### size, first and last
Every array, map and string has three built-in properties. A field of the same
name in a map wins.
| Property | Array | Map | String |
|---|---|---|---|
| `.size` | number of items | number of keys | number of characters |
| `.first` | first item | `nil` | `nil` |
| `.last` | last item | `nil` | `nil` |
```vascula
{{ product.tags.size }} {{ product.tags.first }} {{ product.tags.last }} {{ product.title.size }}
```
```json data
{"data": {"product": {"title": "Café", "tags": ["new", "sale"]}}, "allow": ["product"]}
```
```output
2 new sale 4
```
`.size` of something missing is `0`, so `{% if product.images.size > 0 %}` is
safe even when `images` is absent.
## assign
`assign` stores the value of an expression, filters included.
```vascula
{% assign title = product.title | upcase %}{% assign count = product.tags.size %}{{ title }} has {{ count }} tags.
```
```json data
{"data": {"product": {"title": "Cup", "tags": ["new", "sale"]}}, "allow": ["product"]}
```
```output
CUP has 2 tags.
```
## capture
`capture` renders its body and stores the result as a string. The stored
string is ordinary text: printing it escapes it again.
```vascula
{% capture label %}{{ product.title }} ({{ product.tags.size }}){% endcapture %}{{ label }}
```
```json data
{"data": {"product": {"title": "Cup", "tags": ["new"]}}, "allow": ["product"]}
```
```output
Cup (1)
```
For reusable markup, prefer [`render`](/composition) to `capture`.
## Scope
A template has one set of variables. A value assigned or captured anywhere,
including inside `if`, `case`, `for` or `capture`, is available after that
block ends.
```vascula
{% for n in (1..5) %}{% if n > 2 %}{% assign first_big = first_big | default: n %}{% endif %}{% endfor %}{{ first_big }}
```
```error
undeclared name "first_big"
```
Reading a variable before anything has assigned it is still an error, as the
example shows: the first `default` reads `first_big` before it exists. Assign
a starting value first:
```vascula
{% assign first_big = nil %}{% for n in (1..5) %}{% if n > 2 %}{% assign first_big = first_big | default: n %}{% endif %}{% endfor %}{{ first_big }}
```
```output
3
```
Three exceptions keep things predictable:
- **Loop variables** belong to their loop. After `{% endfor %}` the name is
gone, and assigning to it inside the loop changes only the current item.
- **`forloop`** exists only inside a loop.
- **A child template** rendered with `{% render %}` has its own variables. It
cannot see the caller's, and its assignments do not leak back. See
[Composition](/composition).
```vascula
{% assign total = 0 %}{% for price in settings.prices %}{% assign total = total | plus: price %}{% endfor %}Total: {{ total }}
```
```json data
{"settings": {"prices": [3, 4, 5]}}
```
```output
Total: 12
```
## Reserved names
`settings` and `forloop` cannot be assigned, captured, used as a loop variable
or passed to a child. Applications reserve their own globals the same way, so
a template cannot hide them.
```vascula
{% assign settings = "x" %}
```
```error
assignment name "settings" is reserved
```
Next: [Expressions and operators](/expressions).
---
# Expressions and operators
URL: https://vascula.dev/expressions
An expression is what goes inside `{{ }}`, after `if` or `elsif`, after
`assign name =`, and in a few other tag positions.
## Filters
A filter transforms the value on its left. Chain filters with `|`; they apply
left to right. Arguments follow a colon, separated by commas.
```vascula
{{ " hello world " | strip | capitalize | append: "!" }}
{{ "a,b,c" | split: "," | join: " + " }}
{{ settings.text | truncate: 8, "…" }}
```
```json data
{"settings": {"text": "Vascula templates"}}
```
```output
Hello world!
a + b + c
Vascula…
```
Some filters also take **named** arguments, written `name: value` after any
positional ones:
```vascula
{{ settings.on | default: true, allow_false: true }}
```
```json data
{"settings": {"on": false}}
```
```output
false
```
A filter argument is a single value: a literal, a variable path or a range.
To pass the result of another filter, assign it first.
```vascula
{% assign suffix = settings.unit | upcase %}{{ settings.amount | append: suffix }}
```
```json data
{"settings": {"amount": 3, "unit": "kg"}}
```
```output
3KG
```
Every built-in filter is described in [Filters](/filters). Applications add
their own, such as formatting money or building image URLs.
## Comparisons
`==`, `!=`, `<`, `>`, `<=`, `>=` and `contains` compare two values. The rules
for each are in [Values: comparing values](/values#comparing-values). Filters
bind tighter than comparisons, so each side is filtered first:
```vascula
{% if settings.name | downcase == "ada" %}hello Ada{% endif %}
```
```json data
{"settings": {"name": "ADA"}}
```
```output
hello Ada
```
A comparison takes exactly two sides: `a == b == c` is a compilation error.
## and, or
`and` and `or` combine conditions. They **return one of their operands**,
not a new `true` or `false`:
- `a or b` is `a` when `a` is truthy, otherwise `b`.
- `a and b` is `b` when `a` is truthy, otherwise `a`.
That makes `or` a convenient fallback:
```vascula
{{ settings.nickname or settings.name }} / {{ settings.missing or "Guest" }} / {{ settings.name and "has a name" }}
```
```json data
{"settings": {"name": "Ada"}}
```
```output
Ada / Guest / has a name
```
The right side is only evaluated when needed, so `customer and customer.name`
never reads a field of nothing.
## Precedence
From tightest to loosest:
1. A value with its filters: `x | upcase`
2. A comparison: `a == b`
3. `and`
4. `or`
There are no parentheses for grouping, so `a or b and c` means
`a or (b and c)`. For anything more involved, assign intermediate results to
variables. Parentheses are only used for [ranges](/loops#ranges).
```vascula
{% if false or true and false %}yes{% else %}no{% endif %}
```
```output
no
```
## There is no `not`
Use `unless`, `!=`, or `== false`:
```vascula
{% unless settings.hidden %}shown{% endunless %} {% if settings.count != 0 %}nonzero{% endif %}
```
```json data
{"settings": {"hidden": false, "count": 2}}
```
```output
shown nonzero
```
## Ranges
`(from..to)` is the list of integers from `from` to `to`, inclusive. Each end
is a number or a variable. A range whose end is below its start is empty.
```vascula
{{ (1..4) | join: "," }} | {% for i in (1..settings.n) %}{{ i }}{% endfor %} | [{{ (3..1) | join: "," }}]
```
```json data
{"settings": {"n": 3}}
```
```output
1,2,3,4 | 123 | []
```
Next: [Conditionals](/conditionals).
---
# Conditionals
URL: https://vascula.dev/conditionals
## if, elsif, else
`if` renders its body when the condition is [truthy](/values#truthiness).
`elsif` adds more conditions, tried in order; `else` renders when none matched.
```vascula
{% if settings.stock > 10 %}In stock{% elsif settings.stock > 0 %}Only {{ settings.stock }} left{% else %}Sold out{% endif %}
```
```json data
{"settings": {"stock": 3}}
```
```output
Only 3 left
```
Remember that `0` and `""` are truthy. To test for "has something", compare
with `blank` or check the size:
```vascula
{% if settings.title != blank %}
{{ settings.title }}
{% endif %}{% if settings.items.size > 0 %}{{ settings.items.size }} items{% endif %}
```
```json data
{"settings": {"title": "", "items": ["a"]}}
```
```output
1 items
```
## unless
`unless` is `if` with the condition reversed: its body renders when the
condition is **not** truthy. It may have an `else`, but no `elsif`.
```vascula
{% unless settings.signed_in %}Sign in{% else %}Welcome back{% endunless %}
```
```json data
{"settings": {"signed_in": false}}
```
```output
Sign in
```
## case, when
`case` compares one value against several, using [equality](/values#comparing-values).
The first matching `when` renders. A `when` can list several values, separated
by commas or `or`. `else` renders when nothing matched.
```vascula
{% case settings.size %}{% when "s", "xs" %}Small{% when "m" or "l" %}Regular{% else %}Large{% endcase %}
```
```json data
{"settings": {"size": "l"}}
```
```output
Regular
```
Numbers compare as numbers, so `{% when 2 %}` matches `2.0`.
```vascula
{% case settings.qty %}{% when 0 %}none{% when 1 %}one{% else %}many{% endcase %}
```
```json data
{"settings": {"qty": 1.0}}
```
```output
one
```
A `case` needs at least one `when`. Anything written between `{% case %}` and
the first `{% when %}` is ignored, so you can lay the tags out on separate
lines.
Next: [Loops](/loops).
---
# Loops
URL: https://vascula.dev/loops
## for
`for` renders its body once for each item of an array.
```vascula
```
The collection is a single value: a variable path or a range. It cannot take
filters; assign a filtered list to a variable first.
```vascula
{% assign sorted = settings.names | sort %}{% for name in sorted %}{{ name }} {% endfor %}
```
```json data
{"settings": {"names": ["Grace", "Ada"]}}
```
```output
Ada Grace
```
### An empty collection: else
An `else` inside a loop renders when there is nothing to loop over.
```vascula
{% for item in settings.items %}{{ item }}{% else %}Your basket is empty.{% endfor %}
```
```json data
{"settings": {"items": []}}
```
```output
Your basket is empty.
```
### What can be looped over
| Value | Iterations |
|---|---|
| Array | each item, in order |
| Range `(a..b)` | each integer |
| Map | `[key, value]` pairs, sorted by key; read `item[0]` and `item[1]` |
| `nil`, missing | none (renders `else`) |
| String, number, boolean | none (renders `else`) |
```vascula
{% for pair in settings.sizes %}{{ pair[0] }}={{ pair[1] }} {% endfor %}
```
```json data
{"settings": {"sizes": {"m": 10, "l": 4, "s": 0}}}
```
```output
l=4 m=10 s=0
```
## limit, offset, reversed
Modifiers come after the collection, in any order: `offset` skips items,
`limit` caps how many remain, and `reversed` reverses the selection.
```vascula
{% for n in (1..10) offset: 2 limit: 3 %}{{ n }}{% endfor %} {% for n in (1..10) limit: 3 reversed %}{{ n }}{% endfor %}
```
```output
345 321
```
`offset` is applied first, then `limit`, then `reversed`. A negative offset is
treated as zero; a negative limit means no limit. Both accept a variable or a
filter expression: `limit: settings.count | plus: 1`.
## Ranges
A range loops over integers. Each end is a number or a variable.
```vascula
{% for i in (1..settings.stars) %}★{% endfor %}{% for i in (settings.stars..4) %}☆{% endfor %}
```
```json data
{"settings": {"stars": 3}}
```
```output
★★★☆☆
```
Ranges are limited in size like any other value; see [limits](/safety).
## forloop
Inside a loop, `forloop` describes the current iteration.
| Property | Value |
|---|---|
| `forloop.index` | position, starting at 1 |
| `forloop.index0` | position, starting at 0 |
| `forloop.rindex` | positions remaining, ending at 1 |
| `forloop.rindex0` | positions remaining, ending at 0 |
| `forloop.first` | `true` on the first iteration |
| `forloop.last` | `true` on the last iteration |
| `forloop.length` | number of iterations (after `offset`, `limit`) |
| `forloop.parentloop` | the enclosing loop's `forloop`, or `nil` |
```vascula
{% for t in settings.tags %}{{ t }}{% unless forloop.last %}, {% endunless %}{% endfor %} ({{ settings.tags.size }})
```
```json data
{"settings": {"tags": ["red", "green", "blue"]}}
```
```output
red, green, blue (3)
```
In a nested loop, `forloop.parentloop` reaches the outer loop:
```vascula
{% for row in (1..2) %}{% for col in (1..3) %}{{ forloop.parentloop.index }}{{ forloop.index }} {% endfor %}{% endfor %}
```
```output
11 12 13 21 22 23
```
## break and continue
`break` stops the loop; `continue` skips to the next item. They act on the
innermost loop. Using them outside a loop is an error.
```vascula
{% for n in (1..10) %}{% if n == 3 %}{% continue %}{% endif %}{% if n > 5 %}{% break %}{% endif %}{{ n }}{% endfor %}
```
```output
1245
```
## cycle
`cycle` prints the next of its values each time it runs, starting over after
the last. Each `cycle` tag keeps its own position.
```vascula
{% for n in (1..5) %}
{% endfor %}
```
```output
```
## Loop variables
The loop variable exists only inside its loop. Variables you `assign` inside a
loop remain after it; see [Scope](/variables#scope).
Next: [Composition](/composition).
---
# Composition
URL: https://vascula.dev/composition
`{% render "name" %}` renders another template, a **child**, in place.
Use it for anything you would otherwise copy and paste: a product card, a
price, an icon.
```vascula
{% for p in settings.products %}{% render "card", product: p %}{% endfor %}
```
## Arguments
Everything after the name is `argument: value`. Each argument becomes a
variable in the child. Argument values are expressions, so filters work.
```vascula
{% render "price", currency: "EUR", amount: settings.cents | divided_by: 100 %}
```
```json data
{
"settings": {"cents": 1250},
"children": {"price": {"source": "{{ amount }} {{ currency }}"}}
}
```
```output
12.5 EUR
```
A filter's own arguments continue after a comma, so a filtered value
**must be the last argument**. Otherwise the filter takes the arguments that
follow as its own:
```vascula
{% render "price", amount: settings.cents | divided_by: 100, currency: "EUR" %}
```
```json data
{
"settings": {"cents": 1250},
"children": {"price": {"source": "{{ amount }} {{ currency }}"}}
}
```
```error
filter "divided_by" does not take a named argument "currency"
```
Put the filtered argument last, or `assign` it first and pass the variable.
An argument named `settings` or `forloop`, or a name the application
reserves, is a compilation error. Passing the same argument twice is too.
## A child has its own scope
A child starts fresh. It sees:
- its arguments,
- its **own** `settings`, supplied by the application when it resolves the child,
- the application's globals,
- the data roots the application allows **for the child**.
It does **not** see the caller's variables, `forloop`, `settings` or data
permissions. Whatever a child needs, pass it as an argument.
```vascula
{% assign secret = "caller only" %}{% render "child" %}
```
```json data
{"children": {"child": {"source": "{{ secret }}"}}}
```
```error
undeclared name "secret"
```
Nothing flows back either: a child's assignments stay in the child, and
`break` or `continue` in a child cannot end the caller's loop.
## Names are literal
The name after `render` must be a quoted string; a name computed from data is
a compilation error. [Inspection](/inspection) lists every template a template
renders.
```vascula
{% render settings.which %}
```
```error
requires a literal template name
```
## How children are found
The application supplies the children: it maps each name to a compiled
template, with that child's settings and data permissions (see
[Go API: children](/go-api#children)). An unknown name fails when the
`render` tag runs, with the application's reason in the message.
## Limits
A child shares its caller's render budget: work, output and value limits
cover the whole page, not each template. Children can render children, up to
32 levels deep, which stops a template that renders itself.
Next: [Output and escaping](/output).
---
# Output and escaping
URL: https://vascula.dev/output
## Everything printed is escaped
`{{ }}` escapes its value for HTML: `&`, `<`, `>`, `"` and `'` become
character references. This applies to every value, whatever its source.
```vascula
{{ settings.text }}
```
```json data
{"settings": {"text": " & 'more'"}}
```
```output
```
---
# Vascula for agents
URL: https://vascula.dev/agents
This page is written for AI agents and code generators, and for people who
want the whole language on one screen. It is complete for writing templates;
the linked pages have the details.
Machine-readable versions: [llms.txt](/llms.txt) (index),
[llms-full.txt](/llms-full.txt) (every page in one file), and any page as
Markdown by adding `.md` to its address, for example [/filters.md](/filters.md).
## Rules that prevent most mistakes
1. **Output is always HTML-escaped.** Never use `raw` or `{% raw %}`; they do
not exist. Do not call `escape` before printing; it is unnecessary. Only the
application can print trusted HTML.
2. **Only read names that exist.** A template may read `settings`, its own
variables, loop variables inside the loop, globals the application defines,
and the data roots the application allows. Any other name is an error, not
an empty string. If unsure, ask which roots are available.
3. **`0`, `""` and `[]` are truthy.** Only `false` and `nil` (or missing) are
false. Test presence with `!= blank` or `.size > 0`.
4. **There is no `not` and no grouping parentheses.** Use `unless` or `!=`.
`a or b and c` means `a or (b and c)`.
5. **There are no arithmetic operators or array literals.** Use filters
(`plus`, `minus`, `times`, `divided_by`, `modulo`) and ranges `(1..n)`.
6. **A filter argument is one value** (literal, variable or range). Assign
intermediate results to variables.
7. **A loop collection cannot take filters.** `{% assign s = list | sort %}`
then `{% for x in s %}`.
8. **`render` names are quoted strings**, and the child sees only its
arguments. Put a filtered argument last: `{% render "c", a: 1, b: x | plus: 1 %}`.
9. **Ordering comparisons need matching types.** `count > "3"` is an error;
`"5" == 5` is true.
10. **`divided_by` keeps fractions:** `7 | divided_by: 2` is `3.5`. Add
`| floor` for integer division.
11. **Use `json` to put data in `