
## 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](https://pebbletemplates.io/)-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.

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

```vary-snippet
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]`.

```vary-snippet
let result = engine.render("greeting.html", ctx).unwrap()
```

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

```vary-snippet
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:

```vary-snippet
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.

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

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

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

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

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

### Control flow

```text
{% if active %}
  Active user
{% else %}
  Inactive
{% endif %}
```

```text
{% for item in items %}
  {{ item }}
{% endfor %}
```

```text
{% for key, value in config.items() %}
  {{ key }}: {{ value }}
{% endfor %}
```

### Filters

Pipe a value through a filter to transform it:

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

### Comments

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

### Whitespace control

Add `-` to trim whitespace around tags:

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

## API reference

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

## Example: generating Markdown from JSON

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

```vary-snippet
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:

```text
| 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](https://pebbletemplates.io/). Auto-escaping and strict variable mode are both off.
