Packages

template

Fill text templates with values using {{ }} placeholders, conditions and loops, for emails, reports and generated files.

import "std:template"

template fills in text. You write the fixed parts once, mark the gaps with {{ }}, and give it an object with the values. Use it for emails, reports, config files, HTML pages and anything else where the shape stays the same and the data changes. It is logic-light on purpose: values, conditions and loops, and nothing else.

osl
import "std:template"

Fill in values#

template.render(text, data) returns a result, because the template might be malformed. {{name}} is replaced by the value of name in the data.

main.osl
import "std:template"

string text = "Hello {{name}}, you have {{count}} new messages."
log template.render(text, {name: "Ada", count: 3}).unwrap()
Click the code to edit it

Numbers and booleans are written as OSL prints them, and null or a missing key gives nothing. Spaces inside the braces don't matter: {{ name }} is the same as {{name}}.

main.osl
import "std:template"

string text = "[{{n}}] [{{ok}}] [{{nothing}}] [{{absent}}]"
log template.render(text, {n: 3.5, ok: true, nothing: null}).unwrap()
Click the code to edit it

Try it: add a {{city}} placeholder and don't provide it.

Nested values#

Use dots to reach into objects.

main.osl
import "std:template"

auto data = {user: {name: "Grace", address: {city: "Arlington"}}}
log template.render("{{user.name}} lives in {{user.address.city}}.", data).unwrap()
Click the code to edit it

Conditions#

{{#if key}} ... {{/if}} keeps its body when the value is truthy: not false, null, 0, an empty string or an empty array. {{#unless key}} is the opposite, and {{else}} gives an alternative.

main.osl
import "std:template"

string text = [
  "{{#if admin}}Welcome back, admin.{{else}}Welcome.{{/if}}",
  "{{#unless verified}}Please verify your email.{{/unless}}"
].join(" ")
log template.render(text, {admin: true, verified: false}).unwrap()
log template.render(text, {admin: false, verified: true}).unwrap()
Click the code to edit it

Blocks nest as deep as you need.

main.osl
import "std:template"

string text = "{{#if a}}a{{#if b}}+b{{/if}}{{/if}}"
log template.render(text, {a: 1, b: 1}).unwrap()
log template.render(text, {a: 1, b: 0}).unwrap()
log template.render(text, {a: 0, b: 1}).unwrap() ++ "|"
Click the code to edit it

Loops#

{{#each items}} ... {{/each}} repeats its body for every element. Inside, keys come from the current element, and keys that the element doesn't have are found in the data around it.

main.osl
import "std:template"

string text = "Order for {{customer}}:\n{{#each items}}- {{qty}} x {{name}}\n{{/each}}"
auto data = {
  customer: "Ada",
  items: [{name: "bolt", qty: 12}, {name: "nut", qty: 30}]
}
log template.render(text, data).unwrap()
Click the code to edit it

For an array of plain values, {{.}} is the element itself:

main.osl
import "std:template"

string text = "Tags:{{#each tags}} [{{.}}]{{/each}}"
log template.render(text, {tags: ["go", "fast", "small"]}).unwrap()
log template.render("{{#each tags}}x{{/each}}none", {tags: []}).unwrap()
Click the code to edit it

An empty array renders nothing, so pair a loop with an {{#if}} when you want a message for the empty case:

main.osl
import "std:template"

string text = "{{#if items}}{{#each items}}{{.}} {{/each}}{{else}}nothing to show{{/if}}"
log template.render(text, {items: ["a", "b"]}).unwrap()
log template.render(text, {items: []}).unwrap()
Click the code to edit it

Conditions work inside loops, looking at the current element:

main.osl
import "std:template"

string text = "{{#each tasks}}{{#if done}}[x]{{else}}[ ]{{/if}} {{title}}\n{{/each}}"
auto tasks = [{title: "write", done: true}, {title: "test", done: false}]
log template.render(text, {tasks: tasks}).unwrap()
Click the code to edit it

Try it: add a third item and run it again.

Comments#

{{! ... }} is dropped from the output, for notes to whoever edits the template.

main.osl
import "std:template"

log template.render("{{! greeting line }}Hi {{name}}", {name: "Bo"}).unwrap()
Click the code to edit it

Escaping for HTML#

By default values are written as they are. When the output is HTML, pass {html: true} and every {{value}} is escaped, so user input can't inject markup. Write {{& value}} for a value that is already safe HTML.

main.osl
import "std:template"

string text = "<p>{{comment}}</p>{{& badge}}"
auto data = {comment: "<script>alert(1)</script> & more", badge: "<b>new</b>"}
log template.render(text, data, {html: true}).unwrap()
log template.render(text, data).unwrap()
Click the code to edit it

Errors#

A malformed template is a ParseError with its position. Missing keys are silent by default, which hides typos. Pass {strict: true} to make them a ValueError:

main.osl
import "std:template"

string[] bad = ["Hello {{name", "{{#if x}}never closed", "stray {{/if}}"]
for t in bad (
  log template.render(t, {x: 1}).unwrapErr().message
)

switch template.render("Hello {{nmae}}", {name: "Ada"}, {strict: true}) (
  case ok(s)
    log s
  case err(e)
    log e.kind ":" e.message
)
Click the code to edit it

Strict mode is a good default while you develop, and for templates that must not be left half filled.

Structs as data#

The data can be an object or a struct. A struct's fields are read by name, and structs inside arrays and objects work the same way, so you can render your own types without converting them:

main.osl
import "std:template"

struct Item (
  string name = ""
  number price = 0
)

struct Order (
  string id = ""
  Item[] items = []
)

auto order = Order("a1", [Item("pen", 1.5), Item("ink", 12)])
log template.render("Order {{id}}:{{#each items}} {{name}} ({{price}}){{/each}}", order).unwrap()
Click the code to edit it

Templates in files#

Keep long templates in their own file and read them with fs. That lets someone edit the wording without touching code.

main.osl
import "std:template"
import "std:fs"

fs.remove("invoice.tpl", {force: true}).unwrap()
string invoice = [
  "Invoice {{id}}",
  "{{#each lines}}{{desc}}: {{amount}}",
  "{{/each}}Total: {{total}}",
  ""
].join("\n")
fs.writeFile("invoice.tpl", invoice).unwrap()

string tpl = fs.readFile("invoice.tpl").unwrap()
auto data = {id: 42, lines: [{desc: "Widget", amount: 10}, {desc: "Gadget", amount: 15}], total: 25}
fs.writeFile("invoice.txt", template.render(tpl, data).unwrap()).unwrap()
log fs.readFile("invoice.txt").unwrap()
Click the code to edit it
Quick check

What does {{#if items}} do when items is an empty array?

Your turn

A shopping list

Render the data as a bullet list: a heading line Shopping for NAME, then one line - ITEM for each item that is not bought, and All done! if there are no items left. Use a single template.

main.osl
Ctrl Enter runs
Expected output
Shopping for Ada
- milk
- tea
Your turn

Catch the typo

Render Hello {{nmae}} with {name: "Ada"} in strict mode and print the error kind.

main.osl
Ctrl Enter runs
Expected output
ValueError

API reference#

2 members, generated from the std:template source of OSL 9.3.0.

Functions

#template.render(string text, object data, object? options) result<string, error>

render fills in text from data. It supports {{key}}, {{& key}}, {{#if}}, {{#unless}}, {{#each}} and {{! comments}}. Options: html (escape values) and strict (fail with ValueError for a missing key). Fails with ParseError for a malformed template.

#template.renderHTML(any tmpl, object data) string

Deprecated. Deprecated: use template.render(text, data, {html: true}) instead, which fails with ParseError for a malformed template.

renderHTML fills in a template, HTML-escaping inserted values. It never fails: unclosed blocks render to the end of the template, stray closing and else tags are written out as they are, and missing keys render as "".