Sign in

    moonpug

    Templates for MoonBit: text with braces, indented markup or Markdown over one expression language; compiled once, rendered many times.

    template
    html
    pug
    markdown
    render
    moonbit
    Download zip
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    6 hours ago
    Downloads
    7

    #moonpug

    Templates for MoonBit. One expression language, three ways to write a page: text with braces, indented markup, or Markdown. A template compiles once and renders as many times as you like; nothing touches a file, a clock or a socket unless you hand it a function that does.

    let out = @moonpug.render("Hello {{ name|title }}!", { "name": "ada" })
    // Hello Ada!

    #Install

    moon add moonbitstack/moonpug

    import {
    "moonbitstack/moonpug",
    }

    #A first page

    Data goes in as anything that converts to Json: a derive(ToJson) struct, a map, a parsed document.

    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: [...] })

    Output is escaped by default: Reading <list> is written as Reading &lt;list&gt;. |safe marks text you trust.

    #Three languages

    NameAlsoReadsPreset
    htmltext with {{ }}, {% %} and {# #}html (escaped), text (not escaped)
    pugpgelements by indentation, < lines passed through, {% %} lines as statementspug
    mdmarkdownthe brace syntax, with a tag on a line of its own leaving no blank linemarkdown

    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>

    A named template picks its format from the extension: .pug and .pg read as pug, .md as Markdown, .html .htm .xml .svg as escaped HTML, anything else as text.

    #Language blocks

    Any part of a template can switch language, and blocks nest:

    <section> {% block lang=pug %} ul each i in items li= i {% endblock %} </section>

    A whole template is one big block in its own language. With force=true a stretch reads only its own language: inside pug and md, blocks in another language stay as text and HTML still reads; inside html, pug and md blocks stay as text. A block's own force= wins over the one around it; Settings.force sets it for the whole template.

    #What a template can say

    Statements: if/elif/else, for/else with loop, break, continue, set (with a block and a filter), with, macro and call, filter, include (ignore missing, with context), import and from … import, extends and block (super(), scoped, required, append, prepend), raw, autoescape, do, trans/pluralize, once, show, debug.

    Expressions: literals, lists, tuples and maps, attributes and subscripts, slices, arithmetic with //, % and **, comparisons that chain, in, and/or/not, ~ to join as text, inline if/else, filters with | and tests with is. Strings, lists and maps carry their methods: 'a,b'.split(','), '{:>8.2f}'.format(x), '%s=%d' % (k, v), d.items().

    Built in: 79 filters (among them date, timesince, floatformat, pluralize, urlize, wordwrap, tojson, json_script, groupby, dictsort, xmlattr), 39 tests, and the globals range, dict, lipsum, cycler, joiner, namespace and the gettext family.

    In the indentation syntax: tags with #id and .class, attributes in ( ), or [ ] and { } when a long list wraps, &attributes, #{} and !{} interpolation, #[tag] inline, | and . text, if/unless/each/case, mixins with blocks and attributes, include with a block that lands at yield, extends with block append and prepend, doctype, comments, and :name content filters.

    #Data, functions and objects

    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)

    A host value joins a template by implementing Object. Every method has a default; implement the ones it needs:

    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) })

    call, callable and show complete the set.

    #Templates by name

    let env = @moonpug.Env::new(load=name => sources.get(name))
    let page = env.template("page.html") // read, compiled and kept
    page.render(data)

    load serves extends, include, import and Env::template. translate answers {% trans %} and gettext: given a message, its context and the plural count, the translation or None. cache stores what {% once key %} renders. All three are functions you pass in.

    #Your own language

    A language is a name, its aliases, how its brace tags treat whitespace, and a reader that turns source into the tree. The built-in three are registered the same way:

    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 })

    Then {% block lang=csv %} and Format.lang = "csv" both work. moonbitstack/moonpug/syntax has the tree and the parsers for expressions and statements, so a reader can reuse them.

    #Settings

    RecordFieldsPreset
    Formatlang, autoescape, trim_blocks, lstrip_blocks, keep_trailing_newlinehtml, pug, markdown, text
    Settingsformat, formats, force, undefined, line_statement, line_comment, limitssettings
    Limitsdepth (1000), range (100 000)limits

    undefined is Strict by default: a missing name is refused, except where a template asks is defined or gives a default. Lenient prints it as nothing; Chainable also lets its attributes be missing.

    #Configuration precedence

    library preset < the record you pass < the same name passed to this call

    render(source, data, settings=s, format=f, force=b): format and force override the ones in s, and s overrides the preset settings.

    #Errors

    Every refusal is a Refused and names the template and line:

    VariantWhen
    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

    #Examples

    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

    #Licence

    Apache-2.0. See LICENSE.

    Call

    What an extension can reach of the render in flight: whether output is being escaped, the undefined policy, the other filters and tests, the translation seam, and the bound on range.

    Func

    What a function receives: what the engine knows about the render in flight, positional arguments, named arguments.

    Lang

    A language a template or a {% block lang=… %} is read in: its name, the other names it answers to, how its brace tags treat whitespace, and the reader that turns its source into the tree. The three built in are registered the same way a user's own is.

    Object

    A host value a template can use. Each method is optional: an object that only has attributes implements attr, a function only call. Cyclers, macros, namespaces and the loop variable are objects too.

    Reading

    Where a reader stands: the language and whether it is forced, the template and the line the source starts on, the brace options in effect, and the languages a block may name.

    Refused

    Why a template could not be read or rendered. Every variant names the template and the one-based line it stopped at; a fault raised by an extension carries an empty name and line 0 until the engine fills in where it was called from.

    Undefined

    What happens when an undefined value is used, Jinja's three answers: Lenient prints nothing and iterates as empty but refuses attribute access; Chainable also lets attributes of it be undefined in turn; Strict refuses every use except asking whether it is defined.

    Value

    A value in a template. The context arrives as data; functions and host objects enter as Object. Markup is text already safe to write as it stands. Undefined is a name that resolved to nothing, carrying the name for the message a later use may need.

    Env

    pub struct Env {
    // private fields
    }

    Where templates come from and what they all see: the settings, the filters, tests and global values, the loader and the seams for translation and caching. Compiled templates are kept per environment.

    Env::add_global

    fn Env::add_global(self : Env, name : String, value :
    Value
    ) -> Unit

    Make a value visible to every template, replacing any of the same name; a function is Value::func(name, …).

    Env::add_lang

    fn Env::add_lang(self : Env, lang :
    Lang
    ) -> Unit

    Register a language under its name and aliases, replacing any that answer to the same; Format.lang and {% block lang=… %} find it.

    Env::add_test

    Register a test, replacing any of the same name.

    Env::compile

    fn Env::compile(self : Env, source : StringView, name? : String, format? : Format) -> Template raise
    Refused

    Read a source into a template. A name picks the format by its extension and appears in errors; format overrides either.

    Env::new

    fn Env::new(settings? : Settings, load? : (String) -> String?, translate? : (String, String?, Int?) -> String?, cache? : (String, () -> String raise
    Refused
    ) -> String raise
    Refused
    ) -> Env

    An environment with the built-in languages, filters, tests and globals.

    load turns a template name into its source for extends, include, import and Env::template; without it those are refused. translate answers gettext and {% trans %}: given a message, its context and the plural count, the translation or None. cache stores what {% once key %} renders: given the key and a way to render, the text.

    Env::template

    fn Env::template(self : Env, name : String) -> Template raise
    Refused

    The template the loader finds under name, compiled once and kept.

    Format

    pub(all) struct Format {
    lang : String
    autoescape : Bool
    trim_blocks : Bool
    lstrip_blocks : Bool
    keep_trailing_newline : Bool
    }

    What a template writes, and so how it reads: the name of its language, whether output is escaped, and how whitespace around tags is handled.

    Limits

    pub(all) struct Limits {
    depth : Int
    range : Int
    }

    Bounds on what one render may do.

    Settings

    pub(all) struct Settings {
    format : Format
    formats : (String) -> Format
    force : Bool
    undefined :
    Undefined

    line_statement : String?
    line_comment : String?
    limits : Limits
    }

    Everything an environment is configured with.

    Template

    pub struct Template {
    // private fields
    }

    A compiled template, ready to render any number of times.

    Template::name

    fn Template::name(self : Template) -> String

    The name the template was compiled under.

    Template::render

    fn[C : ToJson] Template::render(self : Template, context : C) -> String raise
    Refused

    Render with a context of data: a Json object, or any value whose to_json gives one.

    Template::render_values

    fn Template::render_values(self : Template, values : Map[String,
    Value
    ]) -> String raise
    Refused

    Render with values that may hold functions and objects as well as data.

    escape

    fn escape(text : String) -> String

    Text made safe for HTML and XML, element bodies and quoted attributes alike: the five characters markupsafe replaces, written the way it does.

    format_of

    fn format_of(name : String) -> Format

    The format a template's name implies: .pug and .pg are Pug, .html, .htm, .xml, .svg and .xhtml are escaped HTML, .md and .markdown are Markdown, anything else is text.

    html

    let html : Format

    HTML in the brace syntax, escaped.

    invalid

    fn invalid(reason : String) ->
    Refused

    A fault an extension raises; the engine adds the template and line.

    limits

    let limits : Limits

    A thousand levels, as Python allows; a hundred thousand items, Jinja's sandbox bound on range.

    markdown

    let markdown : Format

    Markdown in the brace syntax: nothing escaped, and a tag on a line of its own leaves no blank line behind.

    pug

    let pug : Format

    Pug, escaped.

    render

    fn[C : ToJson] render(source : StringView, context : C, settings? : Settings, format? : Format, force? : Bool, load? : (String) -> String?, translate? : (String, String?, Int?) -> String?, cache? : (String, () -> String raise
    Refused
    ) -> String raise
    Refused
    ) -> String raise
    Refused

    Compile and render source in one call. format and force override the settings' own; load, translate and cache are the seams Env::new takes.

    settings

    let settings : Settings

    HTML for an unnamed source, the format by extension for a named one, every language read, undefined names refused, no line prefixes.

    text

    let text : Format

    Plain text in the brace syntax: nothing escaped.

    unknown

    fn unknown(kind : String, name : String) ->
    Refused

    A name that resolves to nothing, raised from an extension.