# Chapter 14: Template Fences — HTML and CSS

A *template fence* embeds a block of another language (HTML, CSS, SQL, plain
text) in Loup source and fills it with Loup values. It is the tool a web app
uses to produce markup.

This chapter teaches the part that works today and marks the part that does
not:

- **Works in Loup 1.25.** Fence syntax, `{$ expr $}` insertion, filters,
  the `if` / `for` / `while` / `let` directives, and `Template` values. Fences
  render the same way under `loup run` and in a Wasm build run by
  `loup-launcher`.
- **⚠ Planned.** The HTML safety layer of the Language Reference §1-16.6
  (context-aware auto-escaping, `SafeHtml`, `Url`, the `<script>`/`<style>`
  prohibition) and the CSS layer of §1-16.7 (`StyleSheet`, `ClassName`,
  scoped class names). Until they ship, **`html` fences do not escape
  anything**, and you must escape user data yourself with the `escape`
  filter. This chapter shows how.

Putting a rendered fence into a page (the `ui` fence, `mount`, components) is
Chapter 16, and is also planned.

## A first fence

````loup
@mode(web.compiled)

func card(name: string, role: string): string {
    let t = ```(html):(none)
    <div class="card">
      <h2>{$ name $}</h2>
      <p>{$ role | upper $}</p>
    </div>
    ```;
    return t.render({});
}

func main(): void {
    println(card("Ada Lovelace", "engineer"));
}
````

*Listing 14-1: A function that renders a card with an `html` fence*

```text
<div class="card">
  <h2>Ada Lovelace</h2>
  <p>ENGINEER</p>
</div>
```

Read it from the top:

- `` ```(html):(none) `` opens the fence. The descriptor is
  `(language):(format)`; `none` (no formatter) is the default, so
  `` ```(html) `` means the same thing.
- `{$ name $}` inserts a value. The expression is an ordinary Loup
  expression, resolved in the lexical scope around the fence: `name` is the
  function's parameter.
- `{$ role | upper $}` passes the value through the `upper` *filter*.
- `` ``` `` on a line of its own closes the fence, and `;` ends the `let`.
- A fence is an expression whose value is a `Template`. `t.render({})` turns
  it into a `string`.

The `{$ … $}` expressions are evaluated when the fence itself is evaluated,
not when `render` is called (Language Reference §1-16.4, ruling TPL-1). A
`Template` is a finished piece of text waiting to be collected.

## Fence layout: the anchor rule

The fence body is indented with the code around it, and Loup strips that
indentation for you. The rule (§1-16.1, shared with multiline strings) is:

- The *anchor* is the whitespace at the start of the line that holds the
  opener. In Listing 14-1 that line is `    let t = ```(html):(none)`, so the
  anchor is four spaces.
- Every content line must start with the anchor; the anchor is removed.
- The closer is the anchor followed by exactly as many backticks as the
  opener, alone on its line.

The mistake everyone makes once is to write the closer flush left, the way
Markdown fences look:

````loup,does_not_compile
@mode(web.compiled)

func badge(label: string): string {
    let t = ```(html):(none)
    <span class="badge">{$ label $}</span>
```;
    return t.render({});
}

func main(): void {
    println(badge("new"));
}
````

```text
main.lpweb:6:1: warning: line does not start with expected indentation
main.lpweb:8:1: warning: line does not start with expected indentation
main.lpweb:10:1: warning: line does not start with expected indentation
main.lpweb:12:1: warning: line does not start with expected indentation
main.lpweb:13:1: error[E0004]: unterminated template fence
main.lpweb:13:1: error[E0002]: expected closing ``` for template fence
main.lpweb:13:1: error[E0002]: expected `}`
```

A closer is only recognised at the anchor, so the flush-left `` ``` `` on line
6 is just content. The fence swallows the rest of the file, the compiler warns
about every line that lacks the four-space anchor, and it reports the fence as
unterminated at the end of the file (line 13). When you see `E0004` far from
the fence you just wrote, look at its closer. Indent it to match the `let`
line:

````loup
@mode(web.compiled)

func badge(label: string): string {
    let t = ```(html):(none)
    <span class="badge">{$ label $}</span>
    ```;
    return t.render({});
}

func main(): void {
    println(badge("new"));
}
````

```text
<span class="badge">new</span>
```

The language reference also allows a fence opened with four or more backticks,
so that the body can contain three-backtick lines (for example, Markdown that
shows code). Stage0 does not implement that yet: a four-backtick fence closes at
the first inner `` ``` `` line and the outer closer is a syntax error.

> ⚠ **Planned.** Fences of four or more backticks for nesting (§1-16.1) and
> the alternative delimiters (`expr_open`, `stmt_open`, …).

## Values in a fence

Every `{$ … $}` renders its value with the same rules `println` and `str`
use (§1-16.4):

````loup
@mode(web.compiled)

class Point {
    pub let x: int;
    pub let y: int;
}

enum Shape { Circle(r: float), Square }

func main(): void {
    let some: Option[string] = Some("x");
    let none: Option[string] = None;
    let ok: Result[int, string] = Ok(3);
    let p = Point(x = 1, y = 2);
    let s = Shape.Circle(r = 2.0);
    let t = ```(html):(none)
    <p>[{$ some $}] [{$ none $}] [{$ ok $}] [{$ 2.5 $}] [{$ true $}]</p>
    <p>[{$ p $}] [{$ s $}] [{$ Shape.Square $}]</p>
    ```;
    println(t.render({}));
}
````

*Listing 14-2: How values of different types render in a fence*

```text
<p>[x] [] [3] [2.5] [true]</p>
<p>[Point(x: 1, y: 2)] [Circle(2.0)] [Square]</p>
```

`Some(v)` and `Ok(v)` render as `v`, `None` as nothing, and class instances and
enum variants structurally. Three kinds of value are refused, each for a
reason:

- An `Err` throws `RenderError` where the fence is evaluated: an error should
  not quietly become page text.
- A `Task` must be awaited first: `cannot insert Task[T] into template: use
  'await' first`.
- `bytes` and lists have no single obvious text form, so you choose one with a
  filter.

Here is the `bytes` case:

````loup,does_not_compile
@mode(web.compiled)

func main(): void {
    let avatar = b"hi";
    let t = ```(html):(none)
    <img alt="{$ avatar $}">
    ```;
    println(t.render({}));
}
````

```text
main.lpweb:6:15: error[E0001]: cannot render bytes in template; use a filter (base64, hex, utf8) to convert first
```

The message names the fix. Choose the encoding the receiving end expects:

````loup
@mode(web.compiled)

func main(): void {
    let avatar = b"hi";
    let t = ```(html):(none)
    <img src="data:text/plain;base64,{$ avatar | base64 $}" title="{$ avatar | hex $}">
    ```;
    println(t.render({}));
}
````

```text
<img src="data:text/plain;base64,aGk=" title="6869">
```

A `list` gets the same treatment (`type 'list[int]' is not renderable in
template context`); write `{$ tags | join(", ") $}`.

### Fences see only their lexical scope

`render` takes a `dict` argument, and a reader coming from Jinja or Handlebars
expects it to supply the template's variables:

````loup,does_not_compile
@mode(web.compiled)

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

```text
main.lpweb:5:25: error[E0001]: undefined identifier `who`
```

A fence is checked like any other Loup expression, so `who` has to be in
scope where the fence is written. The `dict` passed to `render` does not
declare names. To reuse a fence with different data, put it in a function and
make the data parameters. A function can return the `Template` itself and let
the caller decide when to render it:

````loup
@mode(web.compiled)

func row(label: string, count: int): Template {
    return ```(html):(none)
    <tr><td>{$ label $}</td><td>{$ count $}</td></tr>
    ```;
}

func main(): void {
    let rows = [row("apples", 3), row("pears", 5)];
    var html = "<table>";
    for (r in rows) {
        html = html + r.render({});
    }
    println(html + "</table>");
}
````

*Listing 14-3: A function that returns a `Template`*

```text
<table><tr><td>apples</td><td>3</td></tr><tr><td>pears</td><td>5</td></tr></table>
```

## Filters

A filter post-processes the rendered text of an expression. Filters chain
left to right, and some take arguments:

````loup
@mode(web.compiled)

func main(): void {
    let tags = ["web", "wasm", "loup"];
    let t = ```(html):(none)
    <p>{$ tags | join(", ") $}</p>
    <p>[{$ "" | default("n/a") $}]</p>
    <p>{$ "A very long title indeed" | truncate(10) $}</p>
    <p>{$ "  pad  " | trim $}|</p>
    <p>{$ "hello world" | title $}</p>
    <p>{$ "Mixed" | lower $} {$ "abc" | reverse $} {$ "héllo" | length $}</p>
    <p>{$ "a-b-c" | replace("-", "+") $} {$ "x" | wrap("[", "]") $}</p>
    <p>{$ "<b>Tom & Jerry</b>" | escape $}</p>
    ```;
    println(t.render({}));
}
````

*Listing 14-4: The built-in filters*

```text
<p>web, wasm, loup</p>
<p>[n/a]</p>
<p>A very lon…</p>
<p>pad|</p>
<p>Hello World</p>
<p>mixed cba 5</p>
<p>a+b+c [x]</p>
<p>&lt;b&gt;Tom &amp; Jerry&lt;/b&gt;</p>
```

| Filter | Effect |
|---|---|
| `upper`, `lower`, `title` | Change case (ASCII) |
| `trim`, `trim_start`, `trim_end`, `strip` | Remove whitespace |
| `escape`, `escape_html` | Encode `&`, `<`, `>`, `"`, `'` as HTML entities |
| `join(sep)` | Join the items of a list |
| `default(fallback)` | Use `fallback` when the text is empty |
| `truncate(n)` | Keep `n` characters and add `…` |
| `replace(from, to)`, `wrap(open, close)` | Edit the text |
| `reverse`, `length` | Reverse it, or render its length |
| `base64`, `hex`, `utf8` | Turn `bytes` into text |
| `raw` | Insert unescaped (meaningful once auto-escaping ships) |

Any function whose first parameter is a `string` and that returns a `string`
is also a filter. Further arguments come from the call:

````loup
@mode(web.compiled)

func shout(value: string): string {
    return value + "!";
}

func stars(value: string, n: int): string {
    var out = value;
    var i = 0;
    while (i < n) {
        out = out + "*";
        i = i + 1;
    }
    return out;
}

func main(): void {
    let t = ```(html):(none)
    <p>{$ "hey" | shout $} {$ "rated" | stars(3) $} {$ "loud" | upper | shout $}</p>
    ```;
    println(t.render({}));
}
````

*Listing 14-5: User-defined filters*

```text
<p>hey! rated*** LOUD!</p>
```

A misspelled filter name is reported like any other unknown name, for
example ``error[E0001]: undefined identifier `shoutify` ``.

## Directives

`{% … %}` runs template-level control flow. The working directives are
`if` / `elif` / `else`, `for (x in xs)`, `while`, `do` (run an expression for
its side effect), and `let` / `var`; each block ends with `{% end %}`:

````loup
@mode(web.compiled)

func userList(users: list[string]): string {
    let t = ```(html):(none)
    {% if (users.len() == 0) %}
    <p>No users yet.</p>
    {% else %}
    <ul>
    {% for (u in users) %}
      <li>{$ u $}</li>
    {% end %}
    </ul>
    {% end %}
    ```;
    return t.render({});
}

func main(): void {
    println(userList(["ann", "bob"]).trim());
    println(userList([]).trim());
}
````

*Listing 14-6: `if` and `for` directives*

```text
<ul>

  <li>ann</li>

  <li>bob</li>

</ul>
<p>No users yet.</p>
```

(`trim()` removes the blank lines at the start and end.) The output is
correct HTML, but it has blank lines inside: a directive produces no text, and
the newline after it stays. Browsers ignore the extra whitespace.
If you are producing text where whitespace matters, put the directives on the
same line as the content:

````loup
@mode(web.compiled)

func main(): void {
    let xs = [1, 2, 3];
    var i = 0;
    let t = ```(html):(none)
    <p>{% for (x in xs) %}{% if (x == 2) %}<i>two</i>{% elif (x == 3) %}<i>three</i>{% else %}<i>{$ x $}</i>{% end %}{% end %}</p>
    <p>{% while (i < 3) %}<b>{$ i $}</b>{% do i = i + 1; %}{% end %}</p>
    <p>{% let total = xs.len() * 10; %}total: {$ total $}</p>
    ```;
    println(t.render({}));
}
````

*Listing 14-7: Inline directives with `elif`, `while`, `do` and `let`*

```text
<p><i>1</i><i>two</i><i>three</i></p>
<p><b>0</b><b>1</b><b>2</b></p>
<p>total: 30</p>
```

A bare expression is not allowed as a directive; write `{% do expr; %}`.

> ⚠ **Planned.** Three directive features from §1-16.2 do not work in Loup
> 1.25: the trim markers (`{$- x -$}`, `{%- … -%}`) are not recognised;
> `{% match %}` is not implemented in the template parser; and the buffered
> `{% try %} … {% catch %}` emitter parses but does not catch: an exception
> thrown inside it escapes the fence. Compute a value that might fail before
> the fence, and branch on the result with `{% if %}`.

## Interpolated strings

For a one-line string, use an *interpolated string* instead of a fence. It
uses the same `{$ … $}` syntax and the same filters, but it is a `string`
directly. The `i` prefix is what turns interpolation on, and leaving it off is
not an error:

```loup,not_desired_behavior
@mode(web.compiled)

func main(): void {
    let name = "Ann";
    println("Hello, {$ name $}!");
}
```

```text
Hello, {$ name $}!
```

A plain `"…"` string is literal text, so the braces are printed. Add the
prefix:

```loup
@mode(web.compiled)

func main(): void {
    let name = "Ann";
    let unread = 3;
    println(i"Hello, {$ name $}! You have {$ unread $} new messages.");
    println(i"Status: {$ "online" | upper $}");
}
```

```text
Hello, Ann! You have 3 new messages.
Status: ONLINE
```

Use `i"…"` for log lines, messages and short labels; use a fence for anything
with structure. One current difference matters for Wasm builds: in Loup
1.25.1 an `i"…"` string traps when the compiled module runs under
`loup-launcher` (`call_dynamic: no exported function RenderContext`), while
fences work there. Under `loup run` both work.

## HTML escaping: specified, not yet implemented

The Language Reference makes `html` fences escape every inserted value
according to where it lands (§1-16.6.1):

| Context | Example position | Specified escaping |
|---|---|---|
| text | `<p>{$ x $}</p>` | `&`, `<`, `>` become entities |
| attribute value | `class="{$ x $}"` | also `"` and `'` |
| URL attribute | `href="{$ x $}"` | value must be a `Url`; `javascript:` and `vbscript:` rejected |
| raw | `{$ x \| raw $}` | none |

It also makes interpolation inside `<script>` or `<style>` a compile-time
error (`E_SCRIPT_STYLE_INTERPOLATION`) and requires quoted attribute values
(`E_HTML_UNQUOTED_ATTR`).

**None of this is implemented in Loup 1.25.** An `html` fence inserts the
rendered text exactly as it is, in every position:

````loup,not_desired_behavior
@mode(web.compiled)

func main(): void {
    let comment = "<img src=x onerror=alert(1)>";
    let name = "x\"; alert(1); //";
    let t = ```(html):(none)
    <p>{$ comment $}</p>
    <div data-user={$ name $}></div>
    <a href="{$ "javascript:alert(1)" $}">profile</a>
    <script>var user = "{$ name $}";</script>
    ```;
    println(t.render({}));
}
````

*Listing 14-8: An `html` fence does not escape anything today*

```text
<p><img src=x onerror=alert(1)></p>
<div data-user=x"; alert(1); //></div>
<a href="javascript:alert(1)">profile</a>
<script>var user = "x"; alert(1); //";</script>
```

Every line of that output is a cross-site-scripting hole if `comment` or
`name` came from a user, and the compiler accepted all four positions. Until
auto-escaping ships, follow three rules:

1. Pipe every untrusted value through `escape`. It encodes `&`, `<`, `>`, `"`
   and `'`, which is enough for text and for **quoted** attribute values.
2. Never put an untrusted value in a URL attribute without checking its
   scheme yourself; `escape` does not make `javascript:` safe.
3. Never interpolate into `<script>` or `<style>`, and always quote
   attributes.

````loup
@mode(web.compiled)

func safeHref(raw: string): string {
    let url = raw.trim().to_lowercase();
    if (url.starts_with("https://") or url.starts_with("http://") or url.starts_with("/")) {
        return raw;
    }
    return "#";
}

func link(target: string, label: string): string {
    let t = ```(html):(none)
    <a href="{$ safeHref(target) | escape $}">{$ label | escape $}</a>
    ```;
    return t.render({});
}

func main(): void {
    println(link("/docs?a=1&b=2", "Docs"));
    println(link("JavaScript:alert(1)", "<b>win</b>"));
    println(link("https://example.com/\"x", "Example"));
}
````

*Listing 14-9: Escaping by hand until auto-escaping ships*

```text
<a href="/docs?a=1&amp;b=2">Docs</a>
<a href="#">&lt;b&gt;win&lt;/b&gt;</a>
<a href="https://example.com/&quot;x">Example</a>
```

`safeHref` allows only `http`, `https` and site-relative URLs, after trimming
and lower-casing so that `JavaScript:` and ` javascript:` are caught too. The
label and the URL are then escaped for their quoted positions. When §1-16.6
ships, the fence does this itself and `Url.of` replaces `safeHref`.

The other fence languages (`css`, `sql`, `text`, …) are never escaped, even in
the design: a `sql` fence is a string builder, not a parameterised query. Do
not build SQL from user input with it.

### ⚠ Planned: `SafeHtml`, `Url` and the `raw` filter

> ⚠ **Planned.** `SafeHtml` (`lang.template`), `Url` and `UrlError`
> (`core.text`), the scheme check `E_URL_SCHEME_UNSAFE`, the lint on a bare
> `string` in a URL attribute, and the lint on `raw` applied to anything but a
> `SafeHtml` or a constant (§1-16.6.4–§1-16.6.7) are not implemented. In Loup
> 1.25, `SafeHtml` is an undefined identifier and `core.text` is not a module.

The sketch below shows the designed shape of a blog-post renderer, for when
those types arrive:

````loup,ignore
@mode(web.compiled)

from core.text import (Url);
from lang.template import (SafeHtml);

struct Post {
    title: string;
    slug: string;
    author: string;
    body: SafeHtml;          # already-sanitised HTML from a Markdown renderer
    tags: list[string];
}

func renderPost(post: Post): Template {
    return ```(html):(none)
    <article class="post">
      <h1><a href="{$ Url.trusted(i"/posts/{$ post.slug $}") $}">{$ post.title $}</a></h1>
      <p class="meta">By {$ post.author $}</p>
      <ul class="tags">
      {% for (tag in post.tags) %}
        <li><a href="{$ Url.of(i"/tags/{$ tag $}") $}">#{$ tag $}</a></li>
      {% end %}
      </ul>
      <section class="body">{$ post.body | raw $}</section>
    </article>
    ```;
}
````

*Listing 14-10: (design sketch) a renderer using `Url` and `SafeHtml`*

In the design, `title`, `author` and `tag` are entity-escaped because they
land in text, `Url.of` validates a URL built at run time, `Url.trusted` marks
one the program built from constants, and `| raw` passes `SafeHtml` through
without a lint warning.

## CSS fences

A `css` fence works today as a text template: its value is a `Template`, and
`render` gives you the CSS with the values filled in.

````loup
@mode(web.compiled)

func cardStyles(spacing: int, brand: string): string {
    let styles = ```(css):(none)
    .card {
      padding: {$ spacing $}px;
      border-color: {$ brand $};
      border-width: {$ spacing // 4 $}px;
    }
    ```;
    return styles.render({});
}

func main(): void {
    println(cardStyles(16, "#0066cc"));
}
````

*Listing 14-11: A `css` fence used as a text template*

```text
.card {
  padding: 16px;
  border-color: #0066cc;
  border-width: 4px;
}
```

Numbers are inserted without units, so write the unit after the `$}`.

The Language Reference designs much more for `css` (§1-16.7): the fence's
value is a `StyleSheet`, each top-level class becomes a `ClassName` property
with a collision-free hashed name, and `toCSS()` returns the compiled sheet.
If you write code from that section today, it fails at the first property
access:

````loup,does_not_compile
@mode(web.compiled)

func main(): void {
    let styles = ```(css):(none)
    .container { padding: 16px; }
    .button { font-size: 14px; }
    ```;
    println(styles.container);
    println(styles.toCSS());
}
````

```text
main.lpweb:8:13: error[E0001]: type 'Template' does not support member access
main.lpweb:9:13: error[E0001]: type 'Template' has no method 'toCSS'
```

The value is a plain `Template`, which has `render` and nothing else. Until
`StyleSheet` ships, render the fence and choose your class names yourself, as
in Listing 14-11.

> ⚠ **Planned.** `StyleSheet` and `ClassName` (`web.css`), hash-scoped class
> names, `styles.get("gridItem")` for non-identifier selectors, collection of
> `ClassName`s during server-side rendering, and the shadow-root rule: a
> `css` fence evaluated in the `render()` method of a `@webcomponent` class is
> attached to the element's shadow root instead of being name-mangled
> (§1-16.7.1, Chapter 17).

## Summary

- A template fence `` ```(language):(format) `` … `` ``` `` is an expression
  whose value is a `Template`; `render({})` turns it into a `string`.
- The fence body and closer must start with the anchor, the indentation of
  the opener's line. A misplaced closer shows up as `E0004 unterminated
  template fence` at the end of the file.
- `{$ expr $}` takes any expression in lexical scope; `render`'s `dict` does
  not supply names. Pass data in as function parameters.
- `Option`, `Result`, numbers, classes and enums render as `println` renders
  them; `Err` throws `RenderError`; `bytes`, lists and `Task`s must be
  converted (`base64`, `join`, `await`) first.
- Built-in and user-defined filters work in fences and in `i"…"` strings.
  A plain `"…"` string does not interpolate.
- `if` / `elif` / `else`, `for`, `while`, `do` and `let` directives work;
  trim markers, `{% match %}` and the `{% try %}` emitter do not yet.
- `html` fences **do not escape** in Loup 1.25. Use `| escape` on untrusted
  values in text and quoted attributes, check URL schemes yourself, and never
  interpolate into `<script>` or `<style>`. Auto-escaping, `SafeHtml`, `Url`,
  `StyleSheet` and `ClassName` are planned.
