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.
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).
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.
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 }}.
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.
A Dict[Str, Any] is passed through as-is. Keys become template variables.
Templates use Pebble syntax. If you know Jinja2, most of it carries over.
{{ name }}
{{ user.email }}
{{ items[0] }}
Dot notation traverses nested objects. Missing variables produce empty strings (strict mode is off).
{% if active %}
Active user
{% else %}
Inactive
{% endif %}
{% for item in items %}
{{ item }}
{% endfor %}
{% for key, value in config.items() %}
{{ key }}: {{ value }}
{% endfor %}
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:
| Filter | Purpose |
|---|---|
safe | No-op pass-through (auto-escaping is already off) |
items | Converts a map to a list of [key, value] pairs for iteration |
{# This is a comment and produces no output #}
Add - to trim whitespace around tags:
{%- if active -%}
no surrounding whitespace
{%- endif -%}
| Expression | Returns | Description |
|---|---|---|
Template.create(dir) | TemplateEngine | Create an engine that loads templates from dir |
engine.render(name, ctx) | Result[Str, TemplateError] | Render template name with context ctx |
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 }}** |
The full Pebble template language reference is at pebbletemplates.io. Auto-escaping and strict variable mode are both off.