Documentation / Templates

Templates

Templates form Moth's string-building engine. They can stay tiny, or grow into a compile-time HTML and Markdown engine with slots, wrappers and control flow.

Square brackets take a moment to click. Then ordinary string formatting starts to feel limiting.

Choose the explanation level: Template basics

Template basics

Templates form Moth's string-building engine.


    name = "Priya"

    greeting = [:
        Hello, [name]
    ]
    

Square brackets create the template.

The colon separates its head from its body.

A nested template inserts a value from the surrounding scope.

Templates can build plain text, Markdown, HTML, CSS or another string-based format. They may be finished during compilation or built at runtime.

Templates create owned String values.

They use square brackets. Collections and maps use braces.


    name = "Priya"

    greeting = [:
        Hello, [name]
    ]
    

Head and body

A colon separates the template head from its body.


    ["prefix": body]
    

The head may contain:

  • a value or helper template
  • directives
  • slot contributions
  • final if or loop control-flow suffixes

The body contains authored text and nested templates.

Authored .moth templates must close with a closing square bracket.

Truncated heads, bodies, nested children and directive-argument templates are syntax errors.

Capture and text

A template may read visible values from its surrounding scope.

Inside a template body:

  • backticks are ordinary body text
  • backslashes are ordinary body text
  • formatter directives decide how authored text is transformed

Double-quoted string slices still use normal escaping in expression position.

Literal delimiters in output

To render a literal open or close square bracket inside a template body, insert a string slice:


    content = [: ["[literal]"]]
    

The current compiler does not support backtick raw string slices, so string insertion is the supported way to emit literal square brackets.

Compile-time and runtime forms

A template may:

  • fully fold to a compile-time string
  • remain as runtime string construction

The source-level result stays an owned String.

Templates do not become source-visible function values.

Runtime Float interpolation and compile-time folding use the same Moth formatter.

Page fragments

Only direct top-level template expressions in an active HTML module root contribute page fragments.

  • top-level runtime templates run through entry-selected start() in source order
  • direct top-level const templates fold into compile-time fragments
  • a template assigned to a binding or returned from a function does not become a page fragment by itself

Page assembly is owned by Project Structure. Const-template rules are owned by Const templates.

Choose the explanation level: Template directives

Template directives

Directives change how a template is interpreted.

They begin with $.


    content = [$md:
        # A heading

        This body is Markdown.
    ]
    

Common directives include:

  • $md for Moth Markdown
  • $raw for unformatted text
  • $note and $todo for ignored template notes
  • $html, $css and $code in HTML projects

Builders can add their own directives.

A formatter applies only to the template that declares it. Nested child templates choose their own formatter.

$ introduces a compiler-handled template directive.

Directive availability comes from one merged registry:

  • frontend directives are always available
  • a project builder may add project-specific directives
  • an unknown directive is a syntax or rule error

Frontend directives

$children(...):

  • applies one wrapper to each direct child
  • accepts a template or string-slice wrapper
  • the wrapper template must close before the directive closes

$fresh:

  • opts one child out of the immediate parent's $children(...) wrapper

$slot:

  • declares a default, named or positional slot

$insert(...):

  • contributes content to a named slot

$note and $todo:

  • discard their balanced bodies
  • are template-authored comments

$doc:

  • marks a documentation-comment template surface

$raw:

  • preserves authored body content and whitespace without a formatter

$md:

  • applies Moth Markdown formatting

HTML builder directives

The HTML project builder adds:

$html:

  • treats the body as raw HTML content

$css:

  • applies the HTML builder's CSS checking and formatting path

$escape_html:

  • escapes HTML-sensitive body content

$code(language):

  • highlights a code body
  • suppresses nested child-template interpretation inside the code content

Formatter scope

Formatting directives do not flow automatically into nested child templates.

A nested child that needs $md, $raw, $html, $css, $escape_html or $code declares that formatter itself.

Moth template files are the exception:

  • a nested template with no explicit directive defaults to $md
  • any explicit directive overrides that default

Incompatible formatter declarations in one template head are rejected.

Choose the explanation level: Template slots

Template slots

A slot is a place where another template supplies content.


    card = [:
        <h1>[$slot("title")]</h1>
        <section>[$slot]</section>
    ]

    [card:
        [$insert("title"): Welcome]
        This is the card body.
    ]
    

The named title slot receives Welcome.

The default slot receives the body text.

Slots that receive no content become empty strings.

Slots let one template receive content from another.

Default and named slots


    card = [:
        <h1>[$slot("title")]</h1>
        <section>[$slot]</section>
    ]

    [card:
        [$insert("title"): Hello]
        Body content
    ]
    

The default and named forms are:


        [$slot]                 -- default slot
        [$slot("name")]         -- named slot
        [$insert("name"): ...]  -- contribution to named slot
    

The default slot is declared with dollar-slot. A named slot is declared with dollar-slot and a string name. A named insert contributes with dollar-insert and the same name.

Positional slots

Positive integer slots receive loose head contributions.


    image = [:
        <img src="[$slot(1)]" alt="[$slot]">
    ]

    [image, "logo.png": Site logo]
    

The first loose contribution fills slot 1.

The remaining loose body content fills the default slot.

Routing rules

  • Loose contributions fill positional slots in ascending numeric order.
  • Remaining loose contributions go to the default slot when one exists.
  • Loose content that cannot be assigned is an error when no default slot exists.
  • Missing slots render as empty strings.
  • Repeated slots replay the same contribution.
  • Named inserts target only their named slot.
  • One application may combine named, positional and default contributions.

Stored named inserts

The language intends to support preparing a named insert in a binding and passing it as a loose contribution. The current compiler does not yet accept this form.

Runtime contributions

Runtime-capable templates support slot contributions from selected template if branches and template loop bodies.

AST prepares the slot routing before HIR.

Runtime lowering appends the routed contributions through normal string accumulators.

Unresolved slot or insert artifacts after routing are invalid template structure.

Choose the explanation level: Child wrappers

Child wrappers

$children(...) wraps every direct child in the same template.


    list #= [$children([:<li>[$slot]</li>]):
        <ul>
            [$slot]
        </ul>
    ]
    

This is useful for lists, table rows and repeated layout elements.

Use $fresh when one child should skip the immediate wrapper:


    [list:
        [: wrapped]
        [$fresh: [: not wrapped]]
    ]
    

$children(...) applies a wrapper to each direct child template.


    list #= [$children([:<li>[$slot]</li>]):
        <ul>
            [$slot]
        </ul>
    ]

    [list:
        [: one]
        [: two]
    ]
    

Each direct child becomes one list item.

Direct-child scope

The wrapper applies only to direct children.

A selected child becomes an opaque contribution to its parent.

The parent's wrapper does not descend into that child's nested templates.

A nested helper may declare its own $children(...) wrapper.

This keeps row, cell, list and layout helpers from leaking wrappers into deeper content.

$fresh

$fresh opts one child out of the immediate parent wrapper.


    [list:
        [: one]
        [$fresh: [: two]]
    ]
    

The first child receives the list-item wrapper.

The second child does not.

$fresh:

  • affects one child
  • affects only the immediate parent wrapper
  • does not change sibling behaviour
  • does not disable wrappers declared deeper inside the child
Choose the explanation level: Template control flow

Template control flow

A template can choose content with if.


    [if signed_in:
        Welcome back
    [else]
        Please sign in
    ]
    

A template can repeat content with loop.


    [loop names |name|:
        <p>Hello [name]</p>
    ]
    

Two structural controls are available inside a template loop:


        [break]
        [continue]
    

The first ends the loop. The second skips the rest of one iteration.

Template control flow builds output lazily at runtime unless a const template requires it to finish during compilation.

Templates support if and loop as final head suffixes.

If the head already contains a value, helper or directive, add a comma before the control-flow suffix.


    [if show:
        Visible
    ]

    [card, if show:
        Visible inside card
    ]
    

Template if

Template if supports:

  • Bool conditions
  • option-present capture with is |name|
  • standalone else if branches
  • one optional standalone else

    [if maybe_name is |name|:
        Hello [name]
    [else if use_fallback]
        Hello fallback
    [else]
        Hello guest
    ]
    

The selected branch is evaluated lazily at runtime.

Full pattern-match arm chains are not template-head syntax.

Use an ordinary statement or value-producing full match when pattern arms are needed.

Template loops

Template loops use the ordinary loop-header families:

  • conditional loops
  • collection loops
  • numeric ranges

    [loop items |item, index|:
        [index]: [item]
    ]

    [loop 0 to 10 |number|:
        [number]
    ]
    

Collection and range loops may bind the current value and optional zero-based index.

Iterations concatenate directly. There is no implicit separator.

Range and collection sources are evaluated once before iteration.

A conditional loop checks its condition before every iteration.

Structural loop control

Two standalone structural controls target the nearest active template loop.

They are signals rather than rendered strings.


    [loop items |item|:
        [if item.skip:
            [continue]
        ]

        [item]

        [if item.done:
            [break]
        ]
    ]
    

Output produced before a control signal is preserved.

An iteration with no output before break or continue does not count as emitted output.

Wrappers and no-output

A wrapper in the owning loop head wraps the complete aggregate once.

Place a per-iteration wrapper inside the loop body.

These forms produce structural no-output:

  • a false if with no else
  • a false conditional loop
  • a zero-iteration collection or range loop

Shared head wrappers and parent $children(...) wrappers are skipped when there is structural no-output.

A selected empty branch is still selected, so its own head wrapper may render an empty wrapper.

Fallible work and slots

Postfix ! inside selected runtime template content propagates from the enclosing function.

Catch blocks are not template expressions.

Runtime slot applications remain valid inside template control flow after normal slot routing.

Compile-time template control flow and iteration limits are specified by Const templates.

Choose the explanation level: Markdown formatting

Markdown formatting

Use $md when a template body should be formatted as Markdown.


    content = [$md:
        # Welcome

        This text becomes **formatted content**.
    ]
    

Small inline code spans use one backtick on each side.

Use $code("moth") or another language name for a full highlighted code block.

Moth Markdown stays small. It does not support fenced code blocks or pipe tables.

$md applies Moth's small Markdown flavour.

It is intentionally not a full CommonMark implementation.


    content = [$md:
        # Title

        Use **clear names** and @./other-page (helpful links).
    ]
    

Supported block forms

  • headings: # through ######
  • paragraphs separated by blank lines
  • unordered lists: - items
  • ordered lists: 1. items
  • links: @./relative-path (link text) or @https://... (link text)
  • emphasis: **bold** and *italic*

Inline code

Inline code uses paired isolated single backticks on one Markdown line.

It renders as a normal HTML code element.

Inside an inline code span:

  • authored whitespace is preserved
  • HTML-sensitive authored characters are escaped
  • emphasis is not parsed
  • links are not parsed

Not supported:

  • empty code spans
  • repeated backtick runs
  • unmatched backticks
  • multiline code spans
  • variable-length delimiters
  • fenced code blocks
  • Markdown-level backtick escaping

Use $code(language) for highlighted code blocks.

Dynamic content boundaries

A dynamic expression anchor may appear inside a parent-authored code span.

$md does not inspect that expression's rendered output.

A child template is an opaque formatting boundary.

A parent Markdown span cannot begin before a child and end after it.

Tables

Markdown pipe-table syntax is not part of Moth Markdown.

Inside .mtf content, use a structured list.

When a website page needs a visual table, build it in the owning .moth page with Moth table helpers.

Do not rely on pipe-table text rendering as a table.

Related content formats

Moth template .mtf files use an implicit compile-time $md template.

Plain .md files use the separate raw Markdown content path.

See Moth template files and Plain Markdown for those file contracts.