Composition¶
Quill offers Twig-class composition: single-parent inheritance, overridable
blocks, parameterized macros, imports and traits, embeds, includes, and
accumulating slots. This is real template inheritance and reuse, not string
concatenation. Composition is built on the Template contract; inheritance,
embed, and trait reuse all reduce to building and merging block tables and
walking a parent chain, while macros are a separate, isolated function namespace.
The statement keywords below are written in the @-default spelling
(Templates); under pragma bare drop the @ and close on a lone
}.
Inheritance¶
@extends "base.quill" // single parent; content outside blocks in a child is rejected
@block body { // define + render in place; long form
...
@}
@block title "Default Title" // shortcut value form
@block outer { // nested, independently overridable
@block inner { ... @}
@}
@extends "<expr>" takes a string-coerced expression, so a candidate list
@extends ["a.quill", "b.quill"] selects the first that exists. Inside an overriding
block, parent() renders the parent's version. block("name") and
block("name", "other.quill") render a named block of this or another template;
block("name") is defined tests existence.
Macros¶
@macro greet(name, greeting: string = "Hello", ...rest) {
{{ greeting }} {{ name | default("guest") }}
@}
Declared params with constant defaults, optional type annotations, and a variadic
capture ...rest. A macro sees only its params, defaults, variadics, and host
globals; the caller's local context is invisible. A macro returns its captured
output (a Str, or Safe under escaping).
Tail captures. A macro may take two optional tail parameters in a fixed
order: a positional variadic ...rest that collects excess positional arguments
into a list, then a keyword variadic **opts that collects excess named
arguments into a map<string, any>. **opts must be the last parameter:
@macro render_field(name, **opts) {
<input name="{{ name }}"{{ opts | keys | join(" ") }}>
@}
{{ render_field("email", id: "e1", class: "big") }} // opts == { id: "e1", class: "big" }
Without a **opts tail an unmatched named argument is a typo error; with one it
is absorbed. Because **opts is an ordinary mapping, it forwards to a nested call
by spread: inner(...opts).
The macro namespace. A macro body sees the names of all macros visible to the
template (its own, sibling macros in the same template, and macros brought in by
import/from), so a macro may call itself or a sibling directly by name.
Recursion and mutual recursion are reachable both by bare name and by the _self
import path (import _self as me; me.tree(...)).
Call blocks. A @call name(args) { body } invokes macro name and binds a
caller() callable inside the macro body that renders the block. It factors a
wrapping macro (header/footer, table row, section shell) from the content it
wraps:
@macro section(title) {
## {{ title }}
{{ caller() }}
@}
@call section("Overview") {
This body is rendered where the macro calls caller().
@}
@call(p1, p2) name(args) { body } declares caller parameters that caller(v1,
v2) inside the macro binds positionally in the block scope.
Imports and traits¶
@import "forms.quill" as forms // namespace; call forms.input(...)
@from "forms.quill" import input, label as lbl // selective; call input(...), lbl(...)
@use "buttons.quill" // import all blocks of a traitable template
@use "buttons.quill" with { submit: ok } // block aliasing / rename
Top-level import is global; in-block import is block-local. A trait has no parent, no macros, and no free body; trait-then-own precedence means the importing template's own block definitions win over imported ones.
Embed¶
Inline an anonymous child of the embedded template: include plus block override
in one construct. Supports with, only, and ignore missing.
Includes¶
Statement form:
@include "header.quill"
@include "row.quill" with { user: u }
@include "row.quill" with { user: u } only
@include "maybe.quill" ignore missing
@include ["a.quill", "b.quill"] // first that exists
with map adds vars to the current context; only renders with just those vars;
ignore missing tolerates absence, rendering nothing; a sequence is a candidate
list, first existing wins.
Function form, returning rendered output as an expression value:
{{ include("snippet.quill", { x: 1 }, with_context: false, ignore_missing: true, sandboxed: true) }}
Accumulating content slots¶
@provide and @yield collect rendered content from many sites into a named
buffer and emit it once. This is the complement of @block: where a block overrides,
a slot accumulates.
@provide <label> { body }appends the rendered body to the slot named<label>and emits nothing at its own position. Every contribution appends in execution order.@yield <label>emits the slot's accumulated content once. It is deferred: a@yieldplaced before the@providesites that feed it is backfilled with the complete accumulation after the render, so a shell can reserve a region at the top and have body partials feed it further down.slot(label)is the expression form: it returns the slot's content as of the call as a value, capturing only what accumulated before the call.
Slots span the whole render, including sub-renders: a @provide inside an
@included or @embedded partial appends to the enclosing render's slot buffer,
and a @yield in the shell is backfilled from those contributions. The canonical
use is collecting import lines or a symbol table from many partials and emitting
the assembled block once in a shell.
Next¶
- Standard Library: the built-in filters, functions, and tests.
- Language Reference: the exhaustive treatment of every construct on this page.