Docs/Data & text

Templates

Render HTML or text from templates.

On this page

valk.template renders HTML or text from templates at runtime. Create an engine, register templates, and render one with a data value.

Render a template#

Valk
use valk.template

class Page {
    title: String
}

fn main() {
    let views = template.Engine.new()
    views.set("page.html", "<h1>{{ title }}</h1>")
    let html = views.render("page.html", Page { title: "Notes & ideas" }) ! {
        println("Could not render: " + E.code)
        return
    }
    println(html) // <h1>Notes &amp; ideas</h1>
}

{{ title }} prints a field of the data, HTML-escaped. Names reach deeper with . and [index], such as {{ user.name }} or {{ items[0] }}. A name that does not exist is an error. render_content(text, data) renders template text without registering it, and render_into(name, data, writer) writes to a file or response instead of returning a string.

Syntax#

Text
{{ expr }}                         Print escaped
{! expr !}                         Print raw, without escaping
{# comment #}
@if(a > 1 && !b) @elif(c) @else @end
@each(items as item) ... @empty ... @end    @empty renders when the list is empty
@each(map as value, key, index) @end
@set(name = expr)
@include("header.html")            or @include("card.html", item) with other data
@extend("layout.html") @block("content") ... @end
@yield("content")                  In a layout: where a block goes
@isset(x)  @len(x)                 In expressions

Expressions support ==, !=, <, <=, >, >=, &&, ||, !, parentheses, and + - * / %; + with text joins it. A directive alone on its line takes the line with it, so loops leave no blank lines. Write \@if or \{{ for literal text.

Loops, conditions and includes#

Valk
use valk.template

class Note {
    title: String
    done: bool
}

class Page {
    notes: Array[Note]
}

fn main() {
    let views = template.Engine.new()
    views.set("item.html", "<li>{{ title }}@if(done) (done)@end</li>")
    views.set("list.html", "<ul>\n@each(notes as note)\n@include(\"item.html\", note)\n@empty\n<li>Nothing yet</li>\n@end\n</ul>")
    let data = Page { notes: Array[Note]{ Note { title: "Write docs", done: true } } }
    println(views.render("list.html", data) ! return)
}

Layouts#

A page fills the blocks of a layout with @extend and @block; the layout prints them with @yield:

Valk
use valk.template

class Page {
    title: String
}

fn main() {
    let views = template.Engine.new()
    views.set("layout.html", "<title>{{ title }}</title>\n<main>@yield(\"content\")</main>")
    views.set("home.html", "@extend(\"layout.html\")\n@block(\"content\")Welcome@end")
    println(views.render("home.html", Page { title: "Home" }) ! return)
}

Filters#

A filter changes a value on its way into the page, left to right: {{ summary | trim | truncate(80) }}.

Filters
Text upper, lower, capitalize, trim, truncate(n, suffix), replace(from, to), escape, urlencode
Lists length, join(sep), first, last, reverse, keys
Numbers round(decimals), fixed(decimals)
Other default("..."), json

Register your own with set_filter. It gets the value and the arguments as json.Value:

Valk
use valk.template
use valk.json

class Item {
    name: String
    price: float
}

fn main() {
    let views = template.Engine.new()
    views.set_filter("money", fn(value: json.Value, args: Array[json.Value]) json.Value {
        return json.from("$" + value.float.to_string(2))
    })
    let text = views.render_content("{{ name | upper }}: {{ price | money }}", Item { name: "Pen", price: 2.5 }) ! return
    println(text) // PEN: $2.50
}

Templates in files#

Keep templates in a directory and embed them in the executable with views.set_many(#embed_dir("views")). The names are the paths relative to that directory, such as "pages/home.html". Changing a file then needs a rebuild. See Embedding files.

An engine belongs to one thread. In a server with several worker threads, make it a global with an initializer, which runs once per thread:

Valk
use valk.template
use valk.http

class Page {
    title: String
}

global views: template.Engine (load_views())

fn load_views() template.Engine {
    let engine = template.Engine.new()
    engine.set("index.html", "<h1>{{ title }}</h1>") // or set_many(#embed_dir("views"))
    return engine
}

fn handle(req: http.Request) http.Response {
    let html = views.render("index.html", Page { title: "Home" }) ! return http.Response.text("Render failed", 500)
    return http.Response.html(html)
}

fn main() {
    http.serve("127.0.0.1", 9000, handle) ! return
}

Escaping#

{{ }} escapes for HTML; {! !} prints raw output, only for HTML your program made and trusts. Pass user content as data and never build template text from it.

Templates are not tied to HTML: set views.escape = null for plain text such as an e-mail body, or to your own function for another format. Template errors only show up when a template renders, not at compile time.