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.
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.
import "std:template"
string text = "Hello {{name}}, you have {{count}} new messages."
log template.render(text, {name: "Ada", count: 3}).unwrap()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}}.
import "std:template"
string text = "[{{n}}] [{{ok}}] [{{nothing}}] [{{absent}}]"
log template.render(text, {n: 3.5, ok: true, nothing: null}).unwrap()Try it: add a {{city}} placeholder and don't provide it.
Nested values#
Use dots to reach into objects.
import "std:template"
auto data = {user: {name: "Grace", address: {city: "Arlington"}}}
log template.render("{{user.name}} lives in {{user.address.city}}.", data).unwrap()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.
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()Blocks nest as deep as you need.
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() ++ "|"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.
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()For an array of plain values, {{.}} is the element itself:
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()An empty array renders nothing, so pair a loop with an {{#if}} when you want a message for the empty case:
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()Conditions work inside loops, looking at the current element:
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()Try it: add a third item and run it again.
Comments#
{{! ... }} is dropped from the output, for notes to whoever edits the template.
import "std:template"
log template.render("{{! greeting line }}Hi {{name}}", {name: "Bo"}).unwrap()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.
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()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:
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
)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:
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()Templates in files#
Keep long templates in their own file and read them with fs. That lets someone edit the wording without touching code.
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()What does {{#if items}} do when items is an empty array?
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.
Shopping for Ada - milk - tea
Catch the typo
Render Hello {{nmae}} with {name: "Ada"} in strict mode and print the error kind.
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) stringDeprecated. 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 "".