# Chapter 14: Template fences

Loup has two built-in ways to produce text from values. *Interpolated strings*, `i"…{$ expr $}…"`, build a `string` on the spot and work everywhere, including native binaries. *Template fences*, blocks opened with three backticks and a language descriptor, build a `Template` value with loops and conditions inside: an HTML report, a SQL script, a config file. This chapter covers both, and is precise about the limits of Loup 1.25.1: fences run under `loup run` and in WebAssembly builds but not yet in native binaries, and HTML fences do not escape their input.

## Interpolated strings and filters

An interpolated string evaluates each `{$ expr $}` and renders the value as text (Language Reference §1-15). A value can be passed through *filters* with `|`:

```loup
@mode(host.native)

func main(): void {
    let host = "  db.internal  ";
    let retries = 3;
    let note = "";
    println(i"connecting to {$ host | trim | upper $} ({$ retries $} retries)");
    println(i"note: {$ note | default("none") $}");
    println(i"summary: {$ "replica lag above threshold" | truncate(12) $}");
}
```

*Listing 14-1: Interpolation with filters*

```text
connecting to DB.INTERNAL (3 retries)
note: none
summary: replica lag …
```

The built-in filters include `upper`, `lower`, `trim`, `escape`, `default(x)`, `truncate(n)`, `replace(a, b)`, `join(sep)` and `title` (Language Reference §1-16.4). A plain `"…{$ x $}…"` string without the `i` prefix is never interpolated; it prints the braces literally.

## A first fence

A fence starts with three backticks and a descriptor `(language):(format)`, and ends with three backticks on a line of their own. Its value is a `Template`; `render(scope)` turns it into a string. Expressions inside refer to variables in the surrounding code, so the scope argument is usually an empty dictionary:

````loup
@mode(host.dual)

func main(): void {
    let service = "billing";
    let replicas = 3;
    let manifest = ```(yaml):(none)
    service: {$ service $}
    replicas: {$ replicas $}
    ```;
    println(manifest.render({}));
}
````

*Listing 14-2: A YAML fragment from a fence*

```text
service: billing
replicas: 3
```

The listings in this chapter use `@mode(host.dual)`: a `host.dual` program can be run by the interpreter or built as WebAssembly, both of which support fences today.

> ⚠ **Planned.** Native binaries cannot contain template fences yet. `loup build --target native` stops with `error[E-BUILD-NATIVE]: unsupported in managed native (libc foundation): an unsupported operation (async / templates / signals / advanced dispatch).` Interpolated strings (Listing 14-1) do build natively, except for the `join` filter on the macOS preview lane. Until fences land in the native code generator, a native program that needs templated output can build its text with interpolated strings, or move the templating into a `host.dual` tool.

### Mistake: unindented fence lines

The fence's lines are measured against the indentation of the code around it, and that indentation is removed from the output. Content written flush against the left margin, as you might in a Python triple-quoted string, breaks the fence:

````loup,does_not_compile
@mode(host.dual)

func main(): void {
    let who = "Ann";
    let t = ```(html):(none)
<p>{$ who $}</p>
```;
    println(t.render({}));
}
````

```text
main.loup:6:1: warning: line does not start with expected indentation
main.loup:6:1: warning: line does not start with expected indentation
main.loup:7:1: warning: line does not start with expected indentation
main.loup:9:1: warning: line does not start with expected indentation
main.loup:10:1: error[E0004]: unterminated template fence
main.loup:10:1: error[E0002]: expected closing ``` for template fence
main.loup:10:1: error[E0002]: expected `}`
```

The closing backticks at column 1 are not recognized as the closer, so the fence swallows the rest of the file. Indent the content and the closer to the level of the statement, as in Listing 14-2. A fence that must contain three backticks itself uses a longer run (four or more) for its own opener and closer.

## Directives

Inside a fence, `{% … %}` directives provide control flow: `{% if (cond) %} … {% else %} … {% end %}` and `{% for (x in xs) %} … {% end %}` (Language Reference §1-16.2). Here a function renders one table row, and the page loops over the results:

````loup
@mode(host.dual)

struct TestResult {
    name: string;
    millis: int;
    passed: bool;
    message: string;
}

func row(t: TestResult): string {
    let status = if (t.passed) { "PASS" } else { "FAIL" };
    let cell = ```(html):(none)
    <tr><td>{$ t.name | escape $}</td><td>{$ t.millis $} ms</td><td class="{$ status | lower $}">{$ status $}</td><td>{$ t.message | escape $}</td></tr>
    ```;
    return cell.render({});
}

func report(results: list[TestResult]): string {
    var failed = 0;
    for (r in results) {
        if (not r.passed) {
            failed = failed + 1;
        }
    }
    let page = ```(html):(none)
    <h1>Test report: {$ failed $} of {$ results.length() $} failed</h1>
    <table>
    {% for (r in results) %}
    {$ row(r) $}
    {% end %}
    </table>
    ```;
    return page.render({});
}

func main(): void {
    let results = [
        TestResult(name = "parse <empty>", millis = 2, passed = true, message = ""),
        TestResult(name = "checksum", millis = 1, passed = false, message = "expected 42, got 41"),
    ];
    println(report(results));
}
````

*Listing 14-3: An HTML test report*

```text
<h1>Test report: 1 of 2 failed</h1>
<table>

<tr><td>parse &lt;empty&gt;</td><td>2 ms</td><td class="pass">PASS</td><td></td></tr>

<tr><td>checksum</td><td>1 ms</td><td class="fail">FAIL</td><td>expected 42, got 41</td></tr>

</table>
```

The directive lines leave blank lines in the output. The specification's trim markers on directives (`{%- … -%}`) do not parse in 1.25.1; for whitespace-sensitive output, put the directives on the same line as the content they wrap.

Notice the `| escape` filters on the test name and message. They are not optional, as the next section shows.

## Known gap: HTML fences do not escape

The Language Reference specifies context-aware escaping for `html` fences: text placed in an element is HTML-escaped, text in an attribute is attribute-escaped, a URL attribute requires a validated `Url`, and interpolation inside `<script>` is refused (§1-16.6). Loup 1.25.1 implements none of this. An interpolated value is inserted exactly as it is:

````loup,not_desired_behavior
@mode(host.dual)

func main(): void {
    let comment = "<script>steal(document.cookie)</script>";
    let page = ```(html):(none)
    <p class="comment">{$ comment $}</p>
    ```;
    println(page.render({}));
}
````

```text
<p class="comment"><script>steal(document.cookie)</script></p>
```

Anyone who can put text into `comment` can put script into the page. Until context-aware escaping lands, pass every untrusted value through the `escape` filter, which encodes `&`, `<`, `>`, `"` and `'`:

````loup
#~ @mode(host.dual)
#~ func main(): void {
let comment = "<script>steal(document.cookie)</script>";
let page = ```(html):(none)
<p class="comment">{$ comment | escape $}</p>
```;
println(page.render({}));
#~ }
````

*Listing 14-4: Escaping by hand*

```text
<p class="comment">&lt;script&gt;steal(document.cookie)&lt;/script&gt;</p>
```

`escape` is right for element text and for quoted attribute values. It does not make a URL safe: a `href` built from user input still needs checking for schemes such as `javascript:`.

> ⚠ **Planned.** The types that carry these guarantees are specified but not defined in 1.25.1: `SafeHtml` (already-escaped markup that is not escaped again), `Url` (a validated URL), and the CSS fence's `StyleSheet` and `ClassName` (§1-16.6.7, §1-16.7.1). `let s: SafeHtml = …` is `error[E0001]: undefined type 'SafeHtml'`. A `css` fence works today as a plain `Template`.

## Templates as values

A fence is an expression, so functions can return templates and assemble them. A `Template` has `render(scope)` and `renderWith(ctx, scope)`; composing two templates means rendering the inner one and interpolating its text:

````loup
#~ @mode(host.dual)
func header(title: string): Template {
    return ```(html):(none)
    <header><h1>{$ title | escape $}</h1></header>
    ```;
}

func main(): void {
    let top = header("Q3 <draft>");
    let page = ```(html):(none)
    {$ top.render({}) $}
    <main>quarterly numbers</main>
    ```;
    println(page.render({}));
}
````

*Listing 14-5: Composing templates*

```text
<header><h1>Q3 &lt;draft&gt;</h1></header>
<main>quarterly numbers</main>
```

The fence's expressions are evaluated when the fence itself is evaluated, not when `render` is called (Language Reference §1-16.4), so the values captured are those at the point the `Template` was built.

The GPU shader fences (`wgsl`, `metal`, `glsl`) and the `ui` layout fence are part of the planned GPU and desktop UI support (chapters 23 and 22).

## Summary

- `i"…{$ expr | filter $}…"` builds a string and works in every mode, including native binaries.
- A fence `` ```(lang):(format) … ``` `` builds a `Template`; `render({})` produces the text. Its lines and closer must be indented to the surrounding code (`E0004 unterminated template fence` otherwise).
- `{% if %}` and `{% for (x in xs) %}` directives work; directive trim markers do not.
- HTML fences do not escape in 1.25.1: use `| escape` on every untrusted value. `SafeHtml`, `Url`, `StyleSheet` and `ClassName` are ⚠ planned.
- Fences run under `loup run` and in WebAssembly builds; native builds refuse them (⚠ planned), so templated code belongs in `host.dual` for now.
- Chapter 15 begins the ownership chapters with parameter verbs.
