Alpha. Vary is under active development and not ready for production use. Syntax, APIs, performance, and behaviour may change between releases.

Templates

How it works

Vary has a built-in template engine for rendering text files with dynamic data. Template.create(dir) creates an engine that loads template files from a directory. engine.render(name, ctx) renders a template with a context object and returns Result[Str, TemplateError].

The engine supports Pebble-style templates. Use {{ variable }} for output and {% tag %} for control flow. Vary converts its own types (data classes, JSON values, YAML values) into template-compatible maps before rendering.

import template

def main() {
    let engine = template.create("templates").unwrap()
    let ctx = Yaml.parse("title: Hello")
    let html = engine.render("page.html", ctx).unwrap()
    print(html)
}

There is no configuration beyond the template directory path. Auto-escaping is off by default.

Creating an engine

Template.create(dir) takes a directory path and returns a TemplateEngine. All template names passed to render are resolved relative to this directory.

let engine = Template.create("/path/to/templates")

The directory must exist and contain the template files you reference. Template files can have any extension (.html, .md.peb, .txt).

Rendering

engine.render(name, ctx) loads the named template, evaluates it with the given context, and returns Result[Str, TemplateError].

let result = engine.render("greeting.html", ctx).unwrap()

Use the ? propagation operator to return errors from the enclosing function:

def render_page(engine: TemplateEngine, ctx: Json) -> Result[Str, TemplateError] {
    let html = engine.render("page.html", ctx)?
    return Ok(html)
}

Or match on the result for explicit error handling:

match engine.render("page.html", ctx) {
    case Ok(html) { print(html) }
    case Err(e) { print(f"Error ({e.kind()}): {e.message()}") }
}

TemplateError has two methods: .kind() returns the error category ("not_found", "syntax", "render", or "io") and .message() returns a human-readable description.

The context object provides the variables available inside the template. The engine accepts three context types.

Context types

YAML or JSON

Parse a YAML or JSON string and pass it directly. Fields become top-level template variables.

def main() {
    let ctx = Yaml.parse("name: World\ncount: 3")
    print(str(ctx))
}
import json

def main() {
    let ctx = Json.parse("{\"name\": \"World\"}").unwrap()
    print(str(ctx))
}

Nested objects become nested maps that you can traverse with dot notation in templates: {{ suites.kotlin_unit_tests.files }}.

Data classes

A data class works as context. Each field becomes a template variable.

data PageInfo {
    title: Str = ""
    count: Int = 0
}

let page = PageInfo("Hello", 42)
let result = engine.render("page.html", page).unwrap()

Inside the template, {{ title }} resolves to "Hello" and {{ count }} to 42.

Dicts

A Dict[Str, Any] is passed through as-is. Keys become template variables.

Template syntax

Templates use Pebble syntax. If you know Jinja2, most of it carries over.

Variables

{{ name }}
{{ user.email }}
{{ items[0] }}

Dot notation traverses nested objects. Missing variables produce empty strings (strict mode is off).

Control flow

{% if active %}
  Active user
{% else %}
  Inactive
{% endif %}
{% for item in items %}
  {{ item }}
{% endfor %}
{% for key, value in config.items() %}
  {{ key }}: {{ value }}
{% endfor %}

Filters

Pipe a value through a filter to transform it:

{{ name | upper }}
{{ name | lower }}
{{ name | capitalize }}
{{ name | trim }}
{{ items | length }}
{{ value | default("none") }}

Two extra filters are available for Jinja2 compatibility:

FilterPurpose
safeNo-op pass-through (auto-escaping is already off)
itemsConverts a map to a list of [key, value] pairs for iteration

Comments

{# This is a comment and produces no output #}

Whitespace control

Add - to trim whitespace around tags:

{%- if active -%}
  no surrounding whitespace
{%- endif -%}

API reference

ExpressionReturnsDescription
Template.create(dir)TemplateEngineCreate an engine that loads templates from dir
engine.render(name, ctx)Result[Str, TemplateError]Render template name with context ctx

Example: generating Markdown from JSON

The programs/generate-test-docs/ program renders test documentation from the CI test inventory:

import system
import fs
import json
import path
import template

def main() {
    let ctx = resolve_context()
    let inv = load_inventory(ctx.inventory_json)?
    let plan = build_plan()
    let engine = template.create(ctx.template_dir)

    for target in plan.targets {
        let rendered = engine.render(target.template_name, inv)?
        let out = path.join(ctx.repo_root, target.output_rel_path)
        fs.write_text(fs.write_path(out).unwrap(), rendered).unwrap()
    }
}

The corresponding template accesses nested JSON fields directly:

| Compiler unit tests | Unit | {{ suites.kotlin_unit_tests.test_cases }} |
| Vary language tests | Integration | {{ suites.vary_test_blocks.test_cases }} |
| **Total** | | **{{ totals.total_test_cases }}** |

Pebble documentation

The full Pebble template language reference is at pebbletemplates.io. Auto-escaping and strict variable mode are both off.