A template engine for MoonBit.
<ul>
{% for b in books %}
<li>{{ b.title }}</li>
{% endfor %}
</ul>ul
each b in books
li= b.title{% for b in books %}
- {{ b.title }}
{% endfor %}let out = @moonpug.render("Hello {{ name|title }}!", { "name": "ada" })
// Hello Ada!moon add moonbitstack/moonpugimport {
"moonbitstack/moonpug",
}struct Book {
title : String
year : Int
tags : Array[String]
} derive(ToJson)
struct Shelf {
shelf : String
books : Array[Book]
} derive(ToJson)
let page =
#|<h1>{{ shelf }}</h1>
#|<ul>{% for b in books|sort(attribute='year') %}
#| <li>{{ b.title }}, {{ b.year }}{% if b.tags %} ({{ b.tags|join(', ') }}){% endif %}</li>
#|{% endfor %}</ul>
let html = @moonpug.render(page, Shelf::{ shelf: "Reading <list>", books: [...] })| Name | Also | Reads | Preset |
|---|---|---|---|
| html | text with {{ }}, {% %} and {# #} | html (escaped), text (not escaped) | |
| pug | pg | elements by indentation, < lines passed through, {% %} lines as statements | pug |
| md | markdown | the brace syntax, with a tag on a line of its own leaving no blank line | markdown |
let data : Json = { "title": "Notes", "items": ["ink", "paper"] }
@moonpug.render("h1= title\neach i in items\n p= i", data, format=@moonpug.pug)
// <h1>Notes</h1><p>ink</p><p>paper</p><section>
{% block lang=pug %}
ul
each i in items
li= i
{% endblock %}
</section>let env = @moonpug.Env::new()
env.add_global("today", @moonpug.Value::func("today", (_, _, _) => Text("2026-10-25")))
env.add_filter("money", (call, value, _, _) => Text("¥" + call.text(value)))
env.add_test("short", (call, value, _) => call.text(value).length() <= 3)impl @moonpug.Object for Cart with fn kind(_) { "the cart" }
impl @moonpug.Object for Cart with fn attr(self, name) { ... } // cart.total
impl @moonpug.Object for Cart with fn items(self) { ... } // for x in cart
impl @moonpug.Object for Cart with fn assign(self, name, value) { ... } // {% set cart.coupon = … %}
env.compile(source).render_values({ "cart": Object(cart) })let env = @moonpug.Env::new(load=name => sources.get(name))
let page = env.template("page.html") // read, compiled and kept
page.render(data)let host : @moonpug.Host = {
url: Some((name, args, named) => routes.url(name, args, named)),
asset: Some(path => "/static/" + path),
asset_prefix: Some(() => "/static/"),
media_prefix: Some(() => "/media/"),
csrf_token: Some(() => forms.token()),
now: Some(() => clock.now_iso()),
}
@moonpug.Env::new().add_host(host~)| Global | Answers |
|---|---|
| url(name, …) | the address of a route, the rest of the arguments filling it |
| static(path), static_prefix() | where static files are served from |
| media_prefix() | where uploaded files are served from |
| csrf_token() | the token a form carries, as markup |
| now(format) | the moment, in the date-format letters |
fn read_csv(source : String, reading : @moonpug.Reading) -> Array[@syntax.Node] raise @moonpug.Refused {
...
nodes.append(reading.block(cell, "html", None, reading.line)) // cells read as HTML
...
}
env.add_lang({ name: "csv", aliases: ["table"], options: @syntax.options, read: read_csv })| Record | Fields | Preset |
|---|---|---|
| Format | lang, autoescape, trim_blocks, lstrip_blocks, keep_trailing_newline | html, pug, markdown, text |
| Settings | format, formats, force, undefined, line_statement, line_comment, delimiters, shortcuts, limits | settings |
| Delimiters | variable_start, variable_end, block_start, block_end, comment_start, comment_end | delimiters |
| Limits | depth (1000), range (100 000), write (8 MiB) | limits |
| Host | url, asset, asset_prefix, media_prefix, csrf_token, now | host (all None) |
library preset < the record you pass < the same name passed to this calllet square = { ..@moonpug.delimiters, variable_start: "[[", variable_end: "]]" }
@moonpug.render("Hi [[ name ]]! {{ stays }}", { "name": "Ada" }, delimiters=square)
// Hi Ada! {{ stays }}let settings = {
..@moonpug.settings,
shortcuts: Map([(".", "class"), ("#", "id"), ("@", "data-role")]),
}| Variant | When |
|---|---|
| Syntax(template, line, column, reason) | the source is not well formed |
| Unknown(template, line, kind, name) | a name, filter, test, template, language or attribute that resolves to nothing |
| Invalid(template, line, reason) | values that do not fit: wrong arguments, an operation a type does not take |
| Exceeded(template, line, limit, got) | a bound in Limits was crossed |
page.html:3: price is undefined| Program | Shows |
|---|---|
| 01-first-page | the smallest render: one template, one value |
| 02-three-languages | the same page in html, pug and md, and a language block inside html |
| 03-template-set | a layout, a page that extends it, a macro library and an include |
| 04-own-language | a reader for a language of your own (a small csv renderer) |
| 05-host-objects | a value the template can read, loop over and change, plus a filter and a global |
| 06-minimal-site | a minimal but complete generator: compile once, render many pages |
| 07-feature-tour | a breadth tour of the built-in filters, tests, statements and globals |
| 08-application | a small application: layout, components, a host object, translation and caching |
moon run src/examples/01-first-page
moon run src/examples/02-three-languages
moon run src/examples/03-template-set
moon run src/examples/04-own-language
moon run src/examples/05-host-objects
moon run src/examples/06-minimal-site
moon run src/examples/07-feature-tour
moon run src/examples/08-applicationpub struct Env {
// private fields
}pub(all) struct Format {
lang : String
autoescape : Bool
trim_blocks : Bool
lstrip_blocks : Bool
keep_trailing_newline : Bool
}pub(all) struct Limits {
depth : Int
range : Int
write : Int
}pub struct Template {
// private fields
}fn escape(text : String) -> Stringfn[C : ToJson] render(source : StringView, context : C, settings? : Settings, format? : Format, force? : Bool, delimiters? : Delimiters, shortcuts? : Map[String, String], host? : Host, load? : (String) -> String?, translate? : (String, String?, Int?) -> String?, cache? : (String, () -> String raise Refused) -> String raise Refused) -> String raise Refused