# 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

{{ settings.title }}

{% if settings.items.size > 0 %} {% else %}

Nothing yet.

{% endif %} ``` ```json data {"settings": {"title": "Tea & coffee", "items": ["sencha", "espresso"]}} ``` ```output

Tea & coffee

``` 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(`

Hello, {{ settings.name }}!

`) if err != nil { panic(err) } html, err := tmpl.Render(vascula.Options{ Settings: map[string]interface{}{"name": "Ada & Grace"}, }) if err != nil { panic(err) } fmt.Println(html) } ``` ```output

Hello, Ada & Grace!

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

{{ settings.heading }} at {{ shop.name }}

``` ```json data {"data": {"shop": {"name": "Leaf & Pot"}}, "allow": ["shop"], "settings": {"heading": "Our teas"}} ``` ```output

Our teas at Leaf & Pot

``` 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 ``` ```json data {"data": {"shop": {"products": [{"title": "Sencha"}, {"title": "Assam "}, {"title": "Oolong"}]}}, "allow": ["shop"]} ``` ```output ``` 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 ``` ```json data { "data": {"shop": {"products": [{"title": "Sencha", "stock": 12}, {"title": "Assam", "stock": 0}]}}, "allow": ["shop"], "children": {"product-card": {"source": "
  • {{ product.title }}
  • "}} } ``` ```output ``` ## 8. Put it together ```vascula

    {{ settings.heading }} at {{ shop.name }}

    {%- 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": { "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} } ``` ```output

    Our teas at Leaf & Pot

    2 of 3 teas available

    • Sencha: €12.5 [green, japan]
    • Assam <strong>: €8.99 (sold out) [black]
    • Oolong: €15.75
    ``` 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
      {% for tag in product.tags %}
    • {{ tag }}
    • {% endfor %}
    ``` ```json data {"data": {"product": {"tags": ["new", "sale"]}}, "allow": ["product"]} ``` ```output
    • new
    • sale
    ``` 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 %}
    ``` ```json data { "settings": {"products": [{"title": "Cup"}, {"title": "Saucer"}]}, "children": {"card": {"source": "
    {{ product.title }}
    "}} } ``` ```output
    Cup
    Saucer
    ``` ## 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

    <script>alert("hi")</script> & 'more'

    ``` There is **no way for a template to turn escaping off**: no `raw` filter, no `{% raw %}` block, no "safe" marker. Literal text in the template itself is printed as written, because the author wrote it. ## Escaping never happens twice The `escape` filter returns text that is already escaped, so printing it does not escape it again. `escape_once` escapes text that may already contain character references, without doubling them. ```vascula {{ settings.text | escape }} | {{ settings.encoded | escape_once }} ``` ```json data {"settings": {"text": "a < b", "encoded": "a < b & c"}} ``` ```output a < b | a < b & c ``` `newline_to_br` escapes the text and turns each line break into `
    `: ```vascula {{ settings.address | newline_to_br }} ``` ```json data {"settings": {"address": "1 Tea Street\n"}} ``` ```output 1 Tea Street
    <Leeds> ``` ## JSON `json` turns a value into JSON. Its result is safe in two places: - **Inside `
    ``` ```json data {"settings": {"product": {"name": "Cup ", "price": 12}}} ``` ```output
    ``` Vascula decides which case applies by reading the literal markup of the template in source order. If a tag is opened in one branch of an `if` and closed in another, it reads them in the order written. ## Trusted HTML comes only from the application Rich text that has already been sanitized, such as a product description, needs to be printed as HTML. Only the **application** can do that, by handing the template a trusted value (`vascula.SafeHTML` in Go). A template can print it but cannot create one. Trusted HTML is markup for the page body. Inside a tag it is only text: in an attribute value its tags are removed and the rest is escaped like any string, and where an attribute goes it prints only as a plain attribute name. Markup that adds attributes is a separate trusted value (`vascula.HTMLAttr` in Go), which the application should only produce itself, such as a filter returning `loading="lazy"`; it prints as written only where an attribute goes. ## Links and other URLs Escaping keeps a value inside its attribute, but a value in a URL attribute can still choose the URL's scheme, and `javascript:` runs when followed. When anything printed stands in the value of a URL attribute, Vascula checks the **whole value** once the attribute is complete: the literal text and the printed parts together. It must be a relative URL (a path, `?query` or `#fragment`) or use `http`, `https`, `mailto` or `tel`. Otherwise the whole value prints as `#`. ```vascula Ours Theirs ``` ```json data {"settings": {"link": "/pages/about?from=footer", "bad": "javascript:alert(1)"}} ``` ```output Ours Theirs ``` The check reads the value as a browser would: character references are decoded, leading and trailing spaces and control characters are ignored, tabs and line breaks are removed, and case does not matter. So `href=" {{ x }}"` with `javascript:...`, or `href="java{{ x }}"` with `script:...`, prints `href="#"` too. A value after a fixed start is part of a path, query or fragment and prints as written: ```vascula Search ``` ```json data {"settings": {"q": "tea:green"}} ``` ```output Search ``` A URL attribute written entirely in the template is the author's and is not checked. The check applies to these attributes, in HTML and SVG: `href`, `src`, `srcset`, `action`, `formaction`, `poster`, `data`, `cite`, `background`, `ping`, `manifest`, `xmlns`, `xlink:href` and the obsolete `archive`, `classid`, `codebase`, `icon`, `longdesc`, `profile` and `usemap`. As in Go's `html/template`, a `data-` prefix is ignored (`data-src` is a source), and any other attribute whose name contains `src`, `uri` or `url` is treated as a URL. - **Images:** the `src` of `` and `` and `url()` in `style` also accept an image data URL (`data:image/...`). No other data URL is accepted. - **`srcset`:** each image candidate is checked, read as browsers read `srcset`: a URL runs to the next space, so a comma inside it (`/image?crop=1,2 1x`) is part of it. An unsafe candidate URL prints as `#` and keeps its width or density descriptor. - **`style`:** each `url(...)` argument in the value is checked. A value printed as the start of a `url()` argument also has its spaces, quotes, parentheses and backslashes percent-encoded, so it cannot end the `url()`. ## Where output can stand in a tag - **Quoted attribute values** (`class="{{ x }}"`) are the place for output. - **Unquoted attribute values** (`class={{ x }}`) work too: spaces, quotes, `` ` ``, `=`, `/`, `<` and `>` in the value print as character references, so the value cannot end early or add attributes. - **Where an attribute goes** (``), attribute markup the application vouched for (`HTMLAttr`) prints as written. Trusted HTML does not count. Any other value must be one attribute name that is not an event handler, `style` or a URL attribute (`hidden`), and otherwise prints nothing. - **Names** cannot be printed: ``, ``, ``, and two values printed with no space between them in a tag fail to compile. Write names in the template and print values. As in Go's `html/template`, compilation follows control flow. Every branch of an `if`, `unless` or `case` must end in the same place in the markup (an `if` without `else` must end where it started), a loop body must end where it started, and `break` and `continue` must stand where their loop starts. So a branch cannot open a tag or attribute value that another branch does not: ```vascula About ``` ```json data {"settings": {"local": true, "page": "about"}} ``` ```output About ``` A name is never split by control flow. A branch may end inside a name when the text after the branch ends it with a space, `/`, `=` or `>`: ```vascula ``` But text or output that would continue the name on one path only fails to compile, as does a loop body or `break`/`continue` ending inside a name, or a loop starting inside one: ```vascula ``` ```error would continue a name that control flow may end inside ``` A template, and a `capture` body, must not end inside a tag. ## What escaping does not cover HTML escaping makes text safe as text and as a quoted attribute value. It does not make a value safe as: - **a URL in other places.** The URL check above covers attribute values. A URL printed into a ` ``` ```json data {"settings": {"data": {"tags": ["a", ""], "count": 2}}} ``` ```output ``` Map keys are written in sorted order. --- # Go API URL: https://vascula.dev/go-api ```go import "vascula.dev/vascula" ``` The package has two phases. **Compile** turns source into a `*Template`; **Render** evaluates a template with `Options`. Everything else configures one of the two or inspects a compiled template. ## Compiling ### func Compile ```go func Compile(src string) (*Template, error) ``` Parses `src` with default options: no forms, no host tags, no extra reserved names, default limits. Equivalent to `CompileWithOptions(src, CompileOptions{})`. ### func CompileWithOptions ```go func CompileWithOptions(src string, opts CompileOptions) (*Template, error) ``` Parses `src`. Every compilation error carries a line and column. A successful compile does not guarantee a successful render: data-dependent problems such as reading a name that is not allowed are found when rendering. ### type CompileOptions | Field | Meaning | |---|---| | `Forms map[string]FormDefinition` | Form actions templates may use with `{% form %}`. See [Extending: forms](/extending#forms). Without it, `{% form %}` fails to compile. | | `Tags map[string]TagDefinition` | Host tags templates may use. See [Extending: host tags](/extending#host-tags). | | `Reserved []string` | Names templates cannot bind (assign, capture, loop variable, render argument), in addition to `settings` and `forloop`. Reserve the names of your globals. | | `MaxSourceBytes int` | Source size limit. Default 1 MiB. | | `MaxTokens int` | Token limit. Default 100,000. | | `MaxNesting int` | Nesting limit. Default 128. | A zero or negative limit means the default. Form definitions are copied when compiling, so changing the map afterwards does not affect a compiled template. ## Rendering ### func (*Template) Render ```go func (t *Template) Render(opts Options) (string, error) ``` Renders the template. Safe to call from many goroutines at once on the same template: a compiled template is never modified. ### type Options | Field | Meaning | |---|---| | `Data map[string]interface{}` | Application data by root name. | | `Allow []string` | The roots of `Data` the template may read. A dotted entry such as `"cart.items"` allows the whole root `cart`. | | `Settings map[string]interface{}` | The template's parameters, readable as `settings` without an allowance. | | `Globals map[string]interface{}` | Names readable everywhere without an allowance, in this template and every child. | | `Filters map[string]FilterFunc` | Application filters, added to the built-ins. They replace a built-in of the same name, except `escape`, `json` and `default`. | | `Resolve ResolveChild` | Finds child templates for `{% render %}`. Nil makes `render` fail. | | `Tags map[string]TagFunc` | Implementations of the host tags declared in `CompileOptions.Tags`. | | `MaxSteps int` | Work limit. Default 250,000. | | `MaxBytes int` | Output limit. Default 2 MiB. | | `MaxValueBytes int` | Size limit for any one value. Default 2 MiB. | | `MaxValueNodes int` | Item limit for any one value. Default 50,000. | Limits are shared by the template and all its children. See [Safety and limits](/safety). Use JSON-shaped values in `Data`, `Settings` and `Globals`: `map[string]interface{}`, `[]interface{}`, `string`, `float64` or integers, `bool`, `nil`, and `SafeHTML` for trusted HTML. `time.Time` works with the `date` filter. Other types work where they behave like these, but JSON shapes are what the engine is tuned for. Two interfaces let a value print as text of the application's choosing while behaving as a number or a date everywhere else: - **`Number`**: `Float64() (float64, error)` and `String() string`, the shape of `encoding/json`'s `Number`. Comparisons, arithmetic and the number filters use `Float64`; output uses `String`. Use it for a decimal that must print with its stored digits (`19.90`) or a locale's separators (`19,90`). - **`Instant`**: `Time() time.Time` and `String() string`. The `date` filter and comparisons use `Time`; output uses `String`. An `Instant` equals or orders against another date, or an ISO 8601 string (`"2026-10-01"`, `"2026-10-01T09:30:00Z"`), by the moment it names; a string without an offset is read at the `Instant`'s own clock. Give such types a `MarshalJSON` method so `json` writes the value rather than an empty object. ## Children ### type Child ```go type Child struct { Template *Template Allow []string Settings map[string]interface{} } ``` A child template for `{% render %}`, with its own data allowance and its own settings. It shares the caller's `Data`, `Globals`, `Filters`, `Tags`, resolver and limits. ### type ResolveChild ```go type ResolveChild func(name string) (Child, error) ``` Maps a name used in `{% render "name" %}` to a child. Return an error for an unknown or forbidden name; it fails the render with your message. Resolve is called every time a `render` tag runs, so cache compiled children. ```go run package main import ( "fmt" "vascula.dev/vascula" ) func main() { children := map[string]*vascula.Template{} for name, src := range map[string]string{ "card": `
    {{ product.title }} {% render "price", amount: product.price %}
    `, "price": `{{ amount }}`, } { t, err := vascula.Compile(src) if err != nil { panic(err) } children[name] = t } page, _ := vascula.Compile(`{% for p in settings.products %}{% render "card", product: p %}{% endfor %}`) html, err := page.Render(vascula.Options{ Settings: map[string]interface{}{"products": []interface{}{ map[string]interface{}{"title": "Cup", "price": 12}, }}, Resolve: func(name string) (vascula.Child, error) { t, ok := children[name] if !ok { return vascula.Child{}, fmt.Errorf("no template named %q", name) } return vascula.Child{Template: t}, nil }, }) fmt.Println(html, err) } ``` ```output
    Cup 12
    ``` ## Filters ### type FilterFunc ```go type FilterFunc func(input interface{}, args FilterArgs) (interface{}, error) ``` An application filter. Return an error to fail the render; it is reported with the filter's name and position. ### type FilterArgs ```go type FilterArgs struct { Pos []interface{} Named map[string]interface{} } func (a FilterArgs) Arg(i int) interface{} func (a FilterArgs) NamedArg(name string) interface{} func (a FilterArgs) HasNamed(name string) bool ``` Positional and named arguments, already evaluated. `Arg` and `NamedArg` return `nil` for an argument that was not given. Integers that a `float64` holds exactly arrive as `float64`; larger integers arrive as `int64`. ## Host tags ### type TagDefinition ```go type TagDefinition struct { Argument TagArgument } ``` Declares a host tag at compilation. `TagArgument` is `TagNoArgument`, `TagOptionalArgument` or `TagRequiredArgument`. Tag names are lower case letters, digits and underscores and cannot replace a built-in tag. ### type TagFunc ```go type TagFunc func(call TagCall) (SafeHTML, error) ``` Renders a host tag. The returned markup is printed as-is. ### type TagCall ```go type TagCall struct { Name string Arg interface{} HasArg bool } func (c TagCall) Lookup(name string) (interface{}, bool) ``` `Lookup` reads a name visible at the tag: a variable, `settings`, a global or an allowed data root. See [Extending: host tags](/extending#host-tags). ## Forms ### type FormDefinition ```go type FormDefinition struct { Path string Multipart bool TokenContextKey string TokenField string ActionAttribute string } ``` A form action for `{% form "kind" %}`. See [Extending: forms](/extending#forms). ## Trusted HTML ### type SafeHTML ```go type SafeHTML string ``` HTML the application vouches for. A `SafeHTML` value in data or settings, or returned by a filter or tag, is printed without escaping in the page body. Only use it for markup you produced or sanitized. Inside a tag it is text: in an attribute value its tags are removed and the rest escaped, and where an attribute goes it prints only as a plain attribute name. ### type HTMLAttr ```go type HTMLAttr string ``` Attribute markup the application vouches for, such as `loading="lazy"`. It prints as written only where an attribute goes (``), and is escaped text anywhere else. Produce it only in code whose output you fix, such as a filter returning one of a few constants; never wrap stored, translated or user-provided text in it. ## Lazy values ### type Lazy ```go type Lazy struct { // unexported fields } func NewLazy(load func() (interface{}, error)) *Lazy func (l *Lazy) Value() (interface{}, error) func (l *Lazy) MarshalJSON() ([]byte, error) ``` A value computed the first time a render uses it, anywhere in `Data`, `Settings` or `Globals`. Reading it (a variable, property, index, `first`, `last`, a loop item) computes it; consuming a value whole (output, filters other than `size`, comparisons, `case`, host filter and tag arguments, form attributes) computes every `Lazy` inside it. A load error fails the render. `load` runs at most once, so create `Lazy` values per request. It may return another `Lazy` or data holding them, but must not use itself. `Value` follows a returned `Lazy`, so it never returns one; host code reading a path through data calls it at each step. `MarshalJSON` encodes the computed value. See [Embedding: compute values only when read](/embedding#compute-values-only-when-read). ### func Resolve ```go func Resolve(v interface{}) (interface{}, error) ``` Returns `v` with every `Lazy` computed, at any depth, without changing `v`. Use it in host code that reads template data directly: a host tag's `TagCall.Lookup` and output observation return values as you supplied them. ## Inspection | Method | Returns | |---|---| | `RenderTargets() []string` | Child names rendered anywhere in the template, in source order, without duplicates. | | `RenderArguments() map[string][]string` | For each child name, the sorted argument names passed to it. | | `UsesForm() bool` | Whether the template contains `{% form %}`. | | `UsesTag(name string) bool` | Whether the template contains the host tag `name`. | | `ExternalNames(globals ...string) []NameUse` | Every root name the template reads without defining it, excluding the given globals, with the position of its first use. | All of them visit every branch, including ones that may not run. See [Inspection](/inspection) for how to use them together. ### type NameUse ```go type NameUse struct { Name string Line, Col int } ``` ## Errors ### type UndeclaredNameError ```go type UndeclaredNameError struct { Name string } ``` Returned (wrapped) when a template reads a name that is neither a variable nor an allowed data root. Use `errors.As` to find it, for example to tell the author which permission to request. Other errors are plain errors with a readable message ending in `(line L, col C)` when a position is known. Errors from filters, tags and the resolver wrap your original error, so `errors.Is` and `errors.As` work on them. See [Errors](/errors). ### Output observation `Options.ObserveOutput func(OutputSpan)` optionally receives direct variable outputs in HTML text. `OutputSpan.Start` and `End` are byte offsets in the returned string; `Path` identifies the variable and `RootValue` identifies its current root object. Treat the root as read-only and do not retain it after the render. The observer does not change rendered bytes. Outputs in attributes, scripts/styles, textarea and title elements, captures, child renders and filtered expressions are excluded. This is a host inspection facility, not an authorization decision. --- # Embedding URL: https://vascula.dev/embedding This page is about the decisions you make when you build Vascula into an application. The exact types are in the [Go API](/go-api) reference. ## Data and Allow Templates get data in three ways, and each has a purpose: | Option | Readable as | Use it for | |---|---|---| | `Settings` | `settings` | A template's own parameters: a heading, a number of columns, a chosen colour. | | `Globals` | the key's name | Things every template may read: site-wide configuration, the current locale. | | `Data` + `Allow` | the key's name, **if allowed** | Application data with access control: the signed-in user, the cart, an order. | `Allow` is the template's permission list. Build `Data` with everything the page might need, and give each template only the roots it should read. A template that is not allowed `customer` cannot read the customer, even though it is in `Data`. ```go html, err := tmpl.Render(vascula.Options{ Data: map[string]interface{}{ "product": product, // public "customer": customer, // personal }, Allow: []string{"product"}, // this template may not read customer Settings: map[string]interface{}{"columns": 3}, Globals: map[string]interface{}{"site": siteConfig}, }) ``` An allowance is for a whole root: `"cart.items"` allows all of `cart`. Do not put anything in a root that some template allowed to read it must not see. **Let templates declare what they need.** A good pattern is to store, with each template, the list of roots it reads, and to pass that list as `Allow`. Check the list when the template is saved with [`ExternalNames`](/inspection), so a template cannot be saved if it reads something it did not declare. **Build only what was asked for.** Since you know each template's roots before rendering, you can skip loading data no template on the page reads. ## Compute values only when read Some values are costly and most templates never read them: a count that needs a query, a related record. Put a `*vascula.Lazy` in their place. The render computes it the first time a template uses it, and never otherwise. ```go run package main import ( "fmt" "vascula.dev/vascula" ) func main() { queries := 0 count := vascula.NewLazy(func() (interface{}, error) { queries++ // one query, the first time a template uses the count return 12, nil }) brand := map[string]interface{}{"name": "Acme", "product_count": count} for _, src := range []string{`{{ brand.name }}`, `{{ brand.product_count }} products`, `{{ brand | json }}`} { tmpl, err := vascula.Compile(src) if err != nil { panic(err) } out, err := tmpl.Render(vascula.Options{Data: map[string]interface{}{"brand": brand}, Allow: []string{"brand"}}) if err != nil { panic(err) } fmt.Println(out, queries) } } ``` ```output Acme 0 12 products 1 {"name":"Acme","product_count":12} 1 ``` A template sees the computed value however it reaches it: a property, an index, a loop over the object, an alias, printing or serializing the whole object, a filter, a comparison. Nothing can bypass it, so no analysis of the template is needed. A load that fails fails the render. A `Lazy` computes once and keeps its result, so create them per request. To load many values in one query, have each `Lazy` ask a loader that fetches everything the request registered on its first call. ## Reserve your globals A template could `assign` a name that hides a global. Reserve each global's name at compilation so that is a compilation error: ```go tmpl, err := vascula.CompileWithOptions(src, vascula.CompileOptions{ Reserved: []string{"site"}, }) ``` ## Cache compiled templates Compile when a template is saved or first used, and keep the `*Template`. Rendering a compiled template is cheap; compiling it on every request is not. A compiled template depends on its source **and** on the `CompileOptions` you compiled it with (forms, tags, reserved names, limits). Key your cache on all of them, or on a version you change when you change the options. Never key it on `Settings` or `Data`: those are render inputs and change per request. A compiled template is immutable and safe to share between goroutines. ## Children `Resolve` turns a name in `{% render "name" %}` into a `Child`: a compiled template with its own `Allow` and `Settings`. Typical choices: - Look the name up in a registry of compiled templates. - Give each child the allowance stored with it, not the caller's. A child is a peer with its own permissions. - Return an error for names a template may not use. The render fails with your message. Before rendering, you can walk the whole tree with [`RenderTargets`](/inspection#the-composition-tree): every name is a literal, so the tree is known in advance, and you can reject cycles or missing children when a template is saved. ## Limits and isolation Every render is bounded (see [Safety and limits](/safety)). The limits stop a template from looping forever or producing huge output, and they apply to the page as a whole, children included. They do not limit time spent inside your own filters, tags and resolver: bound those yourself. If you render templates written by people you do not trust, run rendering with a timeout and, ideally, in a process with its own memory limit. ## Showing errors Errors are written for the template's author: they name the problem and the line and column. Show them in your editor or preview. Do not show them, or the template source, to the visitors of a live page; render a generic error page instead and log the message. To give authors better guidance, check for [`UndeclaredNameError`](/go-api#type-undeclarednameerror) and explain how they request access to a root in your system. ## Rendering into a page Vascula renders strings. Set the response's content type yourself (`text/html; charset=utf-8` for HTML), and write the result as-is: it is already escaped. Never feed rendered output back into `Compile`. Output is data, not template source. --- # Extending Vascula URL: https://vascula.dev/extending The language stays small; applications add what they need. There are four ways to extend it: | Extension | Template sees | Declared | |---|---|---| | Filters | `{{ price \| money }}` | at render, `Options.Filters` | | Globals | `{{ site.name }}` | at render, `Options.Globals` | | Forms | `{% form "subscribe" %}...{% endform %}` | at compile, `CompileOptions.Forms` | | Host tags | `{% badge product %}` | at compile, `CompileOptions.Tags`, implemented in `Options.Tags` | Extensions are trusted code: they run with your application's permissions and outside Vascula's limits, so keep them fast and bounded. ## Filters A filter is a function from the piped value and its arguments to a new value. ```go run package main import ( "fmt" "strings" "vascula.dev/vascula" ) func main() { money := func(in interface{}, args vascula.FilterArgs) (interface{}, error) { cents, ok := in.(float64) if !ok { return nil, fmt.Errorf("expected a number of cents, got %T", in) } symbol := "€" if s, ok := args.NamedArg("symbol").(string); ok { symbol = s } return fmt.Sprintf("%s%.2f", symbol, cents/100), nil } shout := func(in interface{}, args vascula.FilterArgs) (interface{}, error) { return strings.ToUpper(fmt.Sprint(in)) + strings.Repeat("!", int(args.Arg(0).(float64))), nil } tmpl, _ := vascula.Compile(`{{ 1250 | money }} {{ 999 | money: symbol: "£" }} {{ "hi" | shout: 3 }}`) html, err := tmpl.Render(vascula.Options{ Filters: map[string]vascula.FilterFunc{"money": money, "shout": shout}, }) fmt.Println(html, err) } ``` ```output €12.50 £9.99 HI!!! ``` Things to know: - **Numbers arrive as `float64`** when they fit exactly, so `1250` arrives as `float64(1250)`. Integers too large for a `float64` arrive as `int64`. - **Return a value, not markup.** What you return is escaped when printed. Return `vascula.SafeHTML` only for markup you built safely yourself. - **Errors fail the render** with your message, the filter's name and its position. Use them for misuse, not for missing data: return `nil` or `""` when a value is simply absent. - An application filter **replaces a built-in** of the same name, except `escape`, `json` and `default`, which cannot be replaced. - Unlike built-ins, application filters receive any named arguments the template passes; ignore the ones you do not use, or reject them. ## Globals `Options.Globals` makes values readable everywhere, children included, without an allowance. Reserve their names when compiling so templates cannot hide them. ```go run package main import ( "fmt" "vascula.dev/vascula" ) func main() { tmpl, err := vascula.CompileWithOptions(`{{ site.name }} — {{ settings.title }}`, vascula.CompileOptions{ Reserved: []string{"site"}, }) if err != nil { panic(err) } html, _ := tmpl.Render(vascula.Options{ Globals: map[string]interface{}{"site": map[string]interface{}{"name": "Teahouse"}}, Settings: map[string]interface{}{"title": "Menu"}, }) fmt.Println(html) _, err = vascula.CompileWithOptions(`{% assign site = "x" %}`, vascula.CompileOptions{Reserved: []string{"site"}}) fmt.Println(err) } ``` ```output Teahouse — Menu assignment name "site" is reserved (line 1, col 11) ``` ## Forms `{% form "kind" %}` writes a `
    ` that posts to an action the application registered. Templates cannot choose the URL; they choose one of the kinds you registered. Each form carries a token from the data, for your cross-site-request-forgery protection. ```go run package main import ( "fmt" "vascula.dev/vascula" ) func main() { tmpl, err := vascula.CompileWithOptions( `{% form "subscribe", class: "signup" %}{% endform %}`, vascula.CompileOptions{Forms: map[string]vascula.FormDefinition{ "subscribe": { Path: "/newsletter", TokenContextKey: "csrf_token", TokenField: "_csrf", ActionAttribute: "data-action", }, }}, ) if err != nil { panic(err) } html, err := tmpl.Render(vascula.Options{ Data: map[string]interface{}{"csrf_token": "t0k3n"}, }) fmt.Println(html, err) } ``` ```output
    ``` | Field | Meaning | |---|---| | `Path` | Where the form posts. Must be a local path starting with `/`. | | `Multipart` | Adds `enctype="multipart/form-data"` for file uploads. | | `TokenContextKey` | The `Data` root holding the token. The template does not need to be allowed to read it; the tag reads it directly. A missing token fails the render. | | `TokenField` | The name of the hidden token field. | | `ActionAttribute` | Optional. A `data-` attribute that receives the kind, for your scripts. | A template may add other attributes (`class`, `id`, `data-*`), written in alphabetical order. It cannot set `action`, `method`, `enctype` or the action attribute. An unknown kind fails compilation. Your server must still verify the token and validate everything posted. ## Host tags A host tag adds a tag of your own: `{% name %}` or `{% name expression %}`. Declare it when compiling, so a template using an unknown tag fails to compile, and implement it when rendering. ```go run package main import ( "fmt" "html" "strings" "vascula.dev/vascula" ) func main() { tmpl, err := vascula.CompileWithOptions( `{% stars settings.rating %} {% byline %}`, vascula.CompileOptions{Tags: map[string]vascula.TagDefinition{ "stars": {Argument: vascula.TagRequiredArgument}, "byline": {Argument: vascula.TagNoArgument}, }}, ) if err != nil { panic(err) } out, err := tmpl.Render(vascula.Options{ Settings: map[string]interface{}{"rating": 3, "author": "Ada & co"}, Tags: map[string]vascula.TagFunc{ "stars": func(call vascula.TagCall) (vascula.SafeHTML, error) { n, _ := call.Arg.(int) return vascula.SafeHTML(`` + strings.Repeat("★", n) + ``), nil }, "byline": func(call vascula.TagCall) (vascula.SafeHTML, error) { settings, _ := call.Lookup("settings") author := settings.(map[string]interface{})["author"] return vascula.SafeHTML("by " + html.EscapeString(fmt.Sprint(author)) + ""), nil }, }, }) fmt.Println(out, err) } ``` ```output ★★★ by Ada & co ``` | Argument rule | Accepts | |---|---| | `TagNoArgument` | `{% name %}` only | | `TagOptionalArgument` | `{% name %}` or `{% name expression %}` | | `TagRequiredArgument` | `{% name expression %}` only | What the function receives: - `call.Arg`: the evaluated argument, and `call.HasArg` whether one was written. A value from `Settings` keeps its Go type (`3` above is an `int`); values from `Data` decoded from JSON are `float64`. - `call.Lookup(name)`: any name the template could read at that point, such as a loop variable or `settings`. The returned `SafeHTML` is printed as-is: **escape everything you insert into it**, as `byline` does. Tag names are lower case and cannot replace a built-in tag. Children inherit the implementations, and `UsesTag(name)` tells you whether a template uses a tag. ## Trusted HTML Sometimes a template must print HTML: a sanitized product description, a rendered Markdown page. Wrap such values in `vascula.SafeHTML` before putting them in `Data`, `Settings` or `Globals`. Templates print them unescaped but cannot create them; every other value is escaped. ```go run package main import ( "fmt" "vascula.dev/vascula" ) func main() { tmpl, _ := vascula.Compile(`{{ settings.description }} {{ settings.note }}`) html, _ := tmpl.Render(vascula.Options{Settings: map[string]interface{}{ "description": vascula.SafeHTML("

    Hand-thrown porcelain.

    "), "note": "escaped", }}) fmt.Println(html) } ``` ```output

    Hand-thrown porcelain.

    <em>escaped</em> ``` Mark a value as `SafeHTML` only when you produced it or ran it through an HTML sanitizer. Vascula does not sanitize. --- # Inspection URL: https://vascula.dev/inspection A compiled template can tell you, without rendering it, what it will need. Use this when a template is saved or published, so mistakes are reported to its author instead of failing a live page. All inspection methods look at every branch, including ones that may never run, and none of them needs data. ## The names a template reads `ExternalNames` lists every root name the template reads without defining it itself, with where it is first used. Variables it assigns or captures, loop variables inside their loops, `forloop` inside loops and `settings` are not external. Pass the names of your globals to exclude them too. ```go run package main import ( "fmt" "vascula.dev/vascula" ) func main() { tmpl, err := vascula.Compile(`{% assign n = cart.items.size %} {{ site.name }}: {{ n }} items{% if customer %} for {{ customer.name }}{% endif %} {% for item in cart.items %}{{ item.title }}{{ settings.separator }}{% endfor %}`) if err != nil { panic(err) } for _, use := range tmpl.ExternalNames("site") { fmt.Printf("%s (line %d, col %d)\n", use.Name, use.Line, use.Col) } } ``` ```output cart (line 1, col 15) customer (line 2, col 37) ``` Compare the result with what the template will be given. Anything left over will fail the first render that reaches it: ```go allowed := map[string]bool{"cart": true} // the roots this template declared for _, use := range tmpl.ExternalNames("site") { if !allowed[use.Name] { return fmt.Errorf("line %d: %q is not available here", use.Line, use.Name) } } ``` One case it cannot catch: reading a variable before the `assign` that creates it has run. That is still found when rendering. ## The composition tree `RenderTargets` lists every child name used with `{% render %}`, in order, without duplicates. Because names must be literal strings, this is the complete list. `RenderArguments` lists, for each child, the argument names passed to it. A child's `ExternalNames` must be covered by its own allowance plus the arguments its callers pass. ```go run package main import ( "fmt" "vascula.dev/vascula" ) func main() { page, _ := vascula.Compile(`{% render "header" %}{% for p in catalog.products %}{% render "card", product: p, compact: true %}{% endfor %}`) card, _ := vascula.Compile(`

    {{ product.title }}

    {% unless compact %}{{ product.text }}{% endunless %}{{ shop.currency }}`) fmt.Println(page.RenderTargets()) args := page.RenderArguments() fmt.Println(args["card"]) passed := map[string]bool{} for _, a := range args["card"] { passed[a] = true } for _, use := range card.ExternalNames() { if !passed[use.Name] { fmt.Println("card needs an allowance for", use.Name) } } } ``` ```output [header card] [compact product] card needs an allowance for shop ``` Walk the tree from a page through `RenderTargets` of each child to find missing children and cycles before anything renders. ## Features used `UsesForm` reports whether a template contains `{% form %}`, and `UsesTag(name)` whether it uses a host tag. Use them to check a template only uses what its context supports, or to prepare work only when needed (for example, issue a form token only for pages with forms). --- # Safety and limits URL: https://vascula.dev/safety An application can render templates written by other people with Vascula. This page says exactly what that covers. ## The model A **template** is untrusted code. It can be wrong, careless or hostile. An **application** (the Go program embedding Vascula) is trusted. It chooses the data, the permissions, the extensions and the limits. **Data** is untrusted text, whoever wrote it. Given that, Vascula guarantees: | Guarantee | How | |---|---| | Data cannot inject markup | Every printed value is HTML-escaped. Templates cannot switch it off. See [Output](/output). | | Data cannot become code | Output is never evaluated again. A value containing `{{ }}` prints those characters. | | Data cannot choose a script URL | A link, source or other URL attribute with printed parts must, as a whole, be relative or use `http`, `https`, `mailto` or `tel`; anything else prints as `#`. See [Output](/output#links-and-other-urls). | | Data cannot add attributes | Output in unquoted values is fully escaped, names cannot be printed, and control flow cannot leave the markup ambiguous. See [Output](/output#where-output-can-stand-in-a-tag). | | A template reads only what it is given | Only allowed data roots, `settings`, globals and its own variables. Anything else is an error. | | A template uses only registered extensions | Unknown filters fail the render; unknown tags and form kinds fail compilation. | | A template cannot exhaust the machine | Compilation and rendering are bounded; see below. | | A child cannot escape its scope | Children get only their own permissions and the arguments passed to them. | | Mistakes are reported, not hidden | Undeclared names, bad comparisons, division by zero and broken dates are errors, never silent blanks. | ## Compilation limits | Limit | Default | Option | |---|---|---| | Source size | 1 MiB | `CompileOptions.MaxSourceBytes` | | Tokens | 100,000 | `CompileOptions.MaxTokens` | | Nesting of blocks and expressions | 128 | `CompileOptions.MaxNesting` | ## Rendering limits These cover the whole render: the template and every child it renders. | Limit | Default | Option | |---|---|---| | Work (steps) | 250,000 | `Options.MaxSteps` | | Output | 2 MiB | `Options.MaxBytes` | | Size of any one value | 2 MiB of text | `Options.MaxValueBytes` | | Items in any one value | 50,000 | `Options.MaxValueNodes` | | Value nesting | 64 levels | fixed | | Expression depth | 256 | fixed | | Child render depth | 32 | fixed | **Work** counts what a render does: each tag, expression, filter call, loop iteration and every part of a value a filter processes. A render that would exceed a limit stops with an error that names it: ```vascula {% for i in (1..40000) %}{% for j in (1..40000) %}{% endfor %}{% endfor %} ``` ```error render budget exceeded: too many steps ``` **Values** are checked against the size limits when they are used: printed, passed to a filter, compared or serialized. Fields a template never uses are not checked. Lower the limits only after testing your real templates; raise them only if real templates need it. ## What remains your responsibility - **Permissions.** `Allow` says which roots a template may read; deciding that correctly is yours. An allowance covers a whole root. - **Extensions.** Filters, tags, the resolver and form handling run as your code, outside the limits. Keep them fast and bounded, and escape anything you put into `SafeHTML`. - **Trusted HTML.** Only wrap sanitized or self-built markup in `SafeHTML`, and only fixed attribute markup you build yourself in `HTMLAttr`. - **CSS and JavaScript.** Escaping does not make a value safe as a script or style, and URLs are only checked in attributes; see [Output: what escaping does not cover](/output#what-escaping-does-not-cover). - **Forms.** The form tag only writes the token; verify it, and validate every posted value, on the server. - **Time and memory.** The limits bound work, not wall-clock time or process memory. For templates from untrusted authors, render with a timeout, in a process with a memory limit. - **Error messages.** Show them to authors, not to visitors. ## Reporting a security problem Send a minimal reproducer, the Vascula version and the options involved to **developers@tringify.com**. Please do not open a public issue for a vulnerability. --- # Errors URL: https://vascula.dev/errors Vascula reports mistakes instead of rendering something wrong. Errors come from two phases: - **Compile errors** mean the source is malformed. They are found as soon as the template is saved, before any data is involved. - **Render errors** depend on the data: a name that is not allowed, a division by zero, a value that is too large. Messages end with the position, `(line L, col C)`, counting characters from 1, when one is known. In a child template the message starts with `{% render %} "name":` and the position is in the child's source. ## Compile errors ### {% x %} is never closed with {% endx %} A block tag (`if`, `unless`, `for`, `case`, `capture`, `form`, `comment`) has no matching end tag. The position is where the block opens. ```vascula
      {% for item in settings.items %}
    • {{ item }}
    ``` ```error {% for %} is never closed with {% endfor %} (line 1, col 5) ``` ### unexpected {% endx %} with no opening tag An end tag, `else`, `elsif` or `when` appears where no block is open, often because of a typo in the opening tag or an extra end tag. ```vascula {% if settings.a %}a{% endif %}{% endif %} ``` ```error unexpected {% endif %} with no opening tag ``` ### unknown tag "x" The tag is not part of Vascula and the application did not declare a host tag with that name. ```vascula {% includes "footer" %} ``` ```error unknown tag "includes" ``` ### expected X, found Y The tag or expression is malformed at that point. The message names what was expected and what was found. ```vascula {% assign total 5 %} ``` ```error expected "=", found number "5" (line 1, col 17) ``` ```vascula {{ product.title | }} ``` ```error expected name, found "}}" ``` ### unterminated ... A string, `{{`, `{%` or `{#` is never closed. Vascula refuses rather than treating the rest of the file as part of it. ```vascula

    {{ "unclosed }}

    ``` ```error unterminated string ``` ### ... is reserved A name that cannot be bound was assigned, captured, used as a loop variable or passed to a child: `settings`, `forloop`, or a name the application reserved. Choose another name. ```vascula {% for forloop in (1..2) %}{% endfor %} ``` ```error for variable "forloop" is reserved ``` ### duplicate render argument ```vascula {% render "card", title: 1, title: 2 %} ``` ```error duplicate render argument "title" ``` ### {% render %} requires a literal template name The name after `render` must be a quoted string. ```vascula {% render settings.name %} ``` ```error requires a literal template name ``` ### the {% raw %} block is not available There is no way to print unescaped text from a template. See [Output](/output). ```vascula {% raw %}{% endraw %} ``` ```error the {% raw %} block is not available ``` ### {% form %} errors `{% form %}` needs a kind the application registered, and cannot set attributes the application controls (`action`, `method`, `enctype`). See [Extending: forms](/extending#forms). ### compile budget exceeded The source is larger, or nests deeper, than the compilation limits allow. See [Safety and limits](/safety#compilation-limits). ## Render errors ### undeclared name "x" The template read a name that is not a variable, not `settings`, not a global and not an allowed data root. Usually a typo, a missing `assign`, or data the template must ask the application to allow. ```vascula {{ customer.name }} ``` ```error undeclared name "customer": it is not a variable or an allowed data root (line 1, col 4) ``` The Go error is a `*vascula.UndeclaredNameError` with the name; see [Go API](/go-api#type-undeclarednameerror). ### unknown filter "x" No built-in or application filter has that name. ```vascula {{ "x" | upcse }} ``` ```error unknown filter "upcse" ``` ### filter "x": ... A filter rejected its input or arguments. The message says why. ```vascula {{ 10 | divided_by: 0 }} ``` ```error filter "divided_by": divided_by: division by zero ``` ```vascula {{ "tomorrow" | date: "%Y" }} ``` ```error date: cannot parse "tomorrow" as a date ``` ### filter "x" does not take a named argument "y" Built-in filters accept only the named arguments they document (only `default` has one: `allow_false`). This usually means a filter's arguments swallowed the next render argument; put the filtered argument last. See [Composition: arguments](/composition#arguments). ### cannot compare A and B with "op" `<`, `>`, `<=` and `>=` need two numbers or two strings. ```vascula {% if settings.count > "3" %}{% endif %} ``` ```json data {"settings": {"count": 5}} ``` ```error cannot compare number and string with ">" ``` ### {% break %} outside a for loop `break` and `continue` only work inside a loop. A child template cannot break its caller's loop. ```vascula {% break %} ``` ```error {% break %} outside a for loop ``` ### {% render %} ... A child could not be rendered. The message continues with the reason: the application's resolver refused the name, the child failed (with its own message and position), or children nested more than 32 levels deep, which usually means a template renders itself. ### {% form %}: host token missing The data root that holds the form token is missing or empty for this render. The application must supply it. ### render budget exceeded: ... The render reached one of the [rendering limits](/safety#rendering-limits): | Message ends with | Meaning | |---|---| | `too many steps` | Too much work: usually nested loops over large collections. | | `output too large` | The page would exceed the output limit. | | `value too large` | One value has more text than allowed. | | `too many value nodes` | One value has too many items. | | `value nesting too deep` | A value is nested more than 64 levels. | | `range too large` | A range has more items than allowed. | | `expression too deep` | An expression nests more than 256 levels. | Reduce the data or the work; raise a limit only if a real template needs it. ### non-finite numbers are not supported A value was infinite or not a number. Vascula refuses rather than printing `NaN`. --- # Coming from Liquid URL: https://vascula.dev/from-liquid Vascula's syntax is close to Liquid's, so most templates read the same. It is its own language, stricter than Liquid in a few places. This page lists every difference. ## Behavior | | Liquid | Vascula | |---|---|---| | Output escaping | Off; use `escape` | Always on; no way to turn it off | | `raw` filter, `{% raw %}` | Available | Not available | | Reading an unknown name | Prints nothing | Error, unless the application allows that root | | Unknown filter | Ignored or error, depending on mode | Error | | `"5" > 3` | Compares somehow | Error: ordering needs two numbers or two strings | | `"5" == 5` | False | True: a numeric string equals its number | | `10 \| divided_by: 4` | `2` (integer division) | `2.5` | | Integers | Arbitrary size | Exact 64-bit integers | | Variables assigned in a block | Global | Global (the same) | | `include` | Shares the caller's variables | Removed; `render` only, with its own scope | | `render` with a variable name | Not allowed | Not allowed (the same) | | `and` / `or` | Evaluate right to left | Evaluate left to right, short-circuit, and return an operand | | JSON inside an attribute | Breaks the attribute | Escaped for the attribute automatically | | Errors | Often rendered inline | Returned to the application with line and column | | Resource use | Depends on the host | Bounded: work, output, value size, depth | ## Tags | Liquid tag | Vascula | |---|---| | `if`, `elsif`, `else`, `unless` | Same. `unless` has no `elsif`. | | `case`, `when` | Same, including `when a, b` and `when a or b`. | | `for` with `limit`, `offset`, `reversed`, `else` | Same. Ranges `(1..n)` work. | | `forloop.*` | Same properties, including `parentloop`. | | `break`, `continue`, `cycle` | Same. Cycle groups (`cycle "g": ...`) are not supported. | | `assign`, `capture` | Same. | | `comment`, `{% # %}` | `{% comment %}` and `{# ... #}`. | | `increment`, `decrement` | Not supported; use `assign` with `plus`. | | `tablerow` | Not supported; use `for` with `forloop`. | | `liquid`, `echo` | Not supported. | | `render` | Same idea; arguments with `name: value`. `with` and `for` forms are not supported. | | `include` | Not supported. | | `raw` | Not supported. | | Shopify `form`, `section`, `schema`, `style`, `javascript`, `paginate` | Not part of the language; an application can add its own tags. | ## Filters These Liquid filters exist in Vascula with the same meaning: `abs`, `append`, `at_least`, `at_most`, `capitalize`, `ceil`, `compact`, `concat`, `date`, `default`, `divided_by` (but see the table above), `downcase`, `escape`, `escape_once`, `first`, `floor`, `join`, `last`, `lstrip`, `map`, `minus`, `modulo`, `newline_to_br`, `plus`, `prepend`, `remove`, `remove_first`, `replace`, `replace_first`, `reverse`, `round`, `rstrip`, `size`, `slice`, `sort`, `sort_natural`, `split`, `strip`, `strip_html`, `strip_newlines`, `sum`, `times`, `truncate`, `truncatewords`, `uniq`, `upcase`, `url_decode`, `url_encode`, `where`. Vascula adds `json`. Not available: `raw`, `base64_*`, `url_escape`, `remove_last`, `replace_last`, `find`, `find_index`, `has`, `reject`, `group_by`, and Shopify's commerce filters (`money`, `img_url`, `t` and others). Applications provide their own filters for formatting money, building URLs and translating text. ## Porting a template 1. Compile it. Every unsupported tag or syntax error is reported with its line and column. 2. Remove `| raw` and `{% raw %}`. If something must print HTML, the application has to pass it as trusted HTML. 3. Replace `include` with `render`, passing what the child needs as arguments. 4. Render it with the data it needs and fix each `undeclared name` error: either it is a typo, or the application must allow that root. 5. Check divisions: unlike Liquid, `divided_by` does not truncate integer division; add `| floor` where you relied on that. --- # Recipes URL: https://vascula.dev/recipes Each recipe is a complete, tested example. ## Show something only if it is there `if` alone is not enough, because `""` and `[]` are truthy. Compare with `blank`: ```vascula {% if settings.subtitle != blank %}

    {{ settings.subtitle }}

    {% endif %} ``` ```json data {"settings": {"subtitle": ""}} ``` ```output ``` ## Fall back to a default `default` treats `nil`, `false`, `""` and empty collections as missing: ```vascula

    {{ settings.heading | default: "Featured" }}

    ``` ```json data {"settings": {"heading": ""}} ``` ```output

    Featured

    ``` ## Join a list with commas and "and" ```vascula {% for name in settings.names %}{% if forloop.last and forloop.length > 1 %} and {% elsif forloop.index > 1 %}, {% endif %}{{ name }}{% endfor %} ``` ```json data {"settings": {"names": ["Ada", "Grace", "Hedy"]}} ``` ```output Ada, Grace and Hedy ``` ## Plural words ```vascula {{ settings.count }} {% if settings.count == 1 %}item{% else %}items{% endif %} ``` ```json data {"settings": {"count": 1}} ``` ```output 1 item ``` ## A grid with a fixed number of columns `forloop.index0` with `modulo` finds the first item of each row: ```vascula {% for item in (1..5) %}{% assign column = forloop.index0 | modulo: 2 %}{% if column == 0 %}
    {% endif %}{{ item }}{% if column == 1 or forloop.last %}
    {% endif %}{% endfor %} ``` ```output
    12
    34
    5
    ``` ## The first few items, then "and more" ```vascula {% for tag in settings.tags limit: 2 %}{{ tag }} {% endfor %}{% if settings.tags.size > 2 %}and {{ settings.tags.size | minus: 2 }} more{% endif %} ``` ```json data {"settings": {"tags": ["new", "sale", "tea", "gift"]}} ``` ```output new sale and 2 more ``` ## Filter, sort and pick ```vascula {% assign cheapest = settings.products | where: "available" | sort: "price" | first %}Cheapest available: {{ cheapest.title }} ``` ```json data {"settings": {"products": [ {"title": "Pot", "price": 30, "available": true}, {"title": "Cup", "price": 8, "available": false}, {"title": "Jug", "price": 12, "available": true} ]}} ``` ```output Cheapest available: Jug ``` ## Put data in an attribute or a script `json` is safe in both places: ```vascula
    ``` ```json data {"settings": {"config": {"autoplay": true, "delay": 3}}} ``` ```output
    ``` ## A short excerpt from rich text ```vascula {{ settings.body | strip_html | truncatewords: 5 }} ``` ```json data {"settings": {"body": "

    Our first flush Darjeeling has arrived this week.

    "}} ``` ```output Our first flush Darjeeling has... ``` ## Mark the current item ```vascula {% for link in settings.links %}
    {{ link.title }}{% endfor %} ``` ```json data {"settings": {"current": "/teas", "links": [{"title": "Home", "url": "/"}, {"title": "Teas", "url": "/teas"}]}} ``` ```output HomeTeas ``` ## Group items under headings There is no `group_by`; sort by the group, then print a heading whenever it changes: ```vascula {% assign last = nil %}{% for t in settings.teas | sort: "kind" %}{% if t.kind != last %}

    {{ t.kind }}

    {% assign last = t.kind %}{% endif %}

    {{ t.name }}

    {% endfor %} ``` ```json data {"settings": {"teas": [{"name": "Sencha", "kind": "green"}, {"name": "Assam", "kind": "black"}, {"name": "Matcha", "kind": "green"}]}} ``` ```error expected "%}" ``` A loop's collection cannot take filters directly. Assign the sorted list first: ```vascula {% assign sorted = settings.teas | sort: "kind" %}{% assign last = nil %}{% for t in sorted %}{% if t.kind != last %}

    {{ t.kind }}

    {% assign last = t.kind %}{% endif %}

    {{ t.name }}

    {% endfor %} ``` ```json data {"settings": {"teas": [{"name": "Sencha", "kind": "green"}, {"name": "Assam", "kind": "black"}, {"name": "Matcha", "kind": "green"}]}} ``` ```output

    black

    Assam

    green

    Sencha

    Matcha

    ``` --- # 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 `