# TEMPLATES.md — Statum template and markup reference This document is the reference for every tag, attribute and expression form that is special to Statum: the `pstm-*` preprocessor syntax handled at build time by `statum-mkpstm`, and the `stm-*` attributes handled at runtime by the browser modules (`statum-core.js`, `statum-document.js`, `statum-markdown.js`). Audience: a developer writing Statum pages. For the wire protocol, directives and the Vala API see `model.md`; for a tutorial see `getting-started.md`. Two namespaces, two phases: - `pstm-*` — resolved at BUILD time by `statum-mkpstm`. Nothing with a `pstm-` prefix is supposed to reach the browser; the delivered page is a plain static document. If one leaks through anyway, the client logs a console warning listing them. - `stm-*` — resolved at RUNTIME by the client modules. These attributes stay in the delivered HTML and are re-evaluated on every render cycle. ## Part I — The pstm preprocessor (build time) `statum-mkpstm` turns one page file plus any templates and fragments it names into a Vala `StatumPage` subclass: a composed, precompressed HTML document with a baked-in route. Composition is: load templates → merge heads upward → inline fragments (repeat until none remain) → expand the legacy client reference → rewrite `pstm-res-*` → strip every remaining `pstm-*` → serialise as HTML → precompress (identity/gzip/zstd/brotli). CLI: `statum-mkpstm -o OUT.vala -n ClassName --ns=Namespace [-r ROUTE] [-t DIR ...] PAGE.html`. The route comes from the page's `` unless `-r` overrides it. ### `` — the page route (element, page head) Child element of the page's `` whose text is the route this page is served at (e.g. `/dashboard`). The generated class's `route` comes from here; registering a page whose composition dropped or omits the `pstm-uri` throws `StatumError.CONFIGURATION` at startup. Routes are read BEFORE head merging because merging consumes `pstm-uri` upward. ### `` — the page name (element, page head) Names the page for build diagnostics. Structural: consumed during composition, never delivered. ### `` — title contribution (element, page head) The page's title fragment. Titles CONCATENATE page-first: a page's `` text is prepended to its template's title. Merged like all head content, then consumed. ### `` — parent template (element, page head) Names the template file (without extension) this page composes into. The template is looked up in the page's directory or any `-t` template dir. A template is itself an HTML file whose `` contains a `` outlet. Chains (a template naming a `` of its own) compose recursively. An unknown template name is a build ERROR; a cyclic chain is not diagnosed — keep chains acyclic. ### `` — content outlet (element, template body) The element inside a template's `` that the child's `` content is spliced into, replacing the outlet. A template without an outlet is a build error (there is nowhere to splice). ### `` — fragment inclusion (element, any body) Includes a named fragment. Fragments live in their own files and are declared inside ` … ` wrapper elements; the include element itself carries `name="…"` naming the fragment to inline. Inclusion is recursive and runs to a fixed point: a fragment may include fragments. Nested fragment markup is inlined where the include element sits. An unknown fragment name is silently left as-is (and then stripped with all other `pstm-*` elements) — check your builds for missing content rather than errors. A fragment's `` content is merged into the document head like a template's. A fragment `` may carry metadata elements such as `` (consumed, not delivered). ### `pstm-materialise-as` — wrapper element (attribute) On a page's `` or a fragment's `` or a `` include: wraps the spliced/inlined content in a single element of the given tag (e.g. `pstm-materialise-as="section"`). Fragments that declare inputs are ALWAYS wrapped (a `div` by default) so the inputs have a stable container. When the fragment body supplied the tag (rather than the include site), the fragment body's non-`pstm-*` attributes are copied onto the wrapper. An explicit attribute at the include site wins over the fragment's own. ### `pstm-res-src` / `pstm-res-href` (and any `pstm-res-{attr}`) — resource URL (attribute) Rewrites to `{attr}="/_statum/resource/{value}"`. ` ``` One build warning per page names the page and the expansion. Pages that reference module files directly are untouched and unwarned. A page mixing the legacy reference with explicit module tags is deduplicated — each module appears exactly once, in canonical order — and the warning says so. Plain `src="…"` scripts are never touched. In new pages, write the four tags explicitly (see Part III for the load contract). ### What never reaches the browser Every `pstm-*` attribute and element is stripped after composition. The delivered page is plain HTML + `stm-*` runtime attributes. See `model.md` for the wire protocol and directives. ## Part II — The runtime modules Load contract: **core first, always**. `statum-core.js` owns the slot protocol, the directive pipeline, the realtime channel and the seams later modules register into (the internal bridge; the attribute registry itself is document-side). The canonical order is core → document → markdown → binding (markdown and binding are order-independent of each other and need not co-exist). Trailing modules may be cherry-picked after core (e.g. core + document for pages without markdown or variables). A module loaded without core logs `console.error('[statum] … requires statum-core.js')` and does nothing else. All modules are ES5, dependency-free, guarded IIFEs. ### Expression syntax (all modules) `stm-*` attribute values are JavaScript expressions evaluated against a scope built from state: `{ typeName: public }` for every held slot type, plus loop variables and `stm-local-*` locals in nested scopes. `statum.read('counter')` is the programmatic equivalent. Expression compilation is cached. An expression evaluating to `undefined` means "not resolved yet" and LEAVES THE ELEMENT UNTOUCHED (the server-rendered original DOM is preserved — this is the progressive-enhancement policy); `null` is stringified to `''`. The scope is a `Proxy` over the state object with `with`-semantics — slot type names are resolved first, then globals. Prefer distinct type names. ### Render cycles Every `set`/`clear` (directive, cross-tab broadcast, SSE push, or `statum.clear()`) triggers `renderAll()`: `beforeEvaluate` hooks → the document walk → `afterEvaluate` hooks → guard evaluation. The walk visits every element; each registered attribute handler runs for elements carrying a matching attribute. The walk is stateless — no binding remembers its previous value; anything your handler writes is rewritten on the next cycle unless the handler itself guards (see `stm-prefill`'s dirty tracking for the pattern). ### Section: statum-core.js The slot protocol and nothing else — core is HEADLESS and owns **no `stm-*` attributes at all**: no DOM scanning, no bindings. It keeps the state store and persistence, `X-Statum-Slot` assembly, entrypoint/action requests, directive dispatch, the SSE/SharedWorker realtime channel, the alert/confirm/notify override mechanism, and the internal bridge that later modules register presentation hooks into. A page can run core alone as a pure state layer under any other templating framework: point `statum.config` at your endpoints before `init()` and consume `read`/`getSnapshot`/`state`/`act` yourself. (The four `` endpoint overrides — `stm-entrypoint`, `stm-slots`, `stm-channel`, `stm-worker` — are read by the DOCUMENT module at load and written into core's mutable `statum.config`; see that section. They are inert on a core-only page, which sets `statum.config` directly.) Directives (server → client, `application/vnd.statum+json`): `set`, `clear`, `invalidate`, `subscribe`, `unsubscribe`, `notify`, `transmit`, `error`, `navigate`, `reenter`, `post`, `form-submit`, `form-reset`. `reenter` re-fetches the entrypoint in place (no page reload), after form directives and before navigate/post. See `model.md` for payloads. Public JS API (on `window.statum`): the essentials are `version`, `config`, `init`, `read`, `getSnapshot`, `getFrame`, `state`, `clear`, `act`, `addStateListener` / `removeStateListener`, `beforeEvaluate` / `afterEvaluate`, `Error`, and the `onConfirmHandler` / `onNotifyHandler` / `onErrorHandler` getter/setter pairs (the override mechanism for `stm-confirm` / `notify` / failures — frontend-demo installs toast handlers through it). `registerAttribute` is DOCUMENT API (next section) — it is absent on a core-only page. ### Section: statum-document.js The render walk and the structural/value bindings. Installs the render driver and owns EVERY `stm-*` attribute that touches the DOM, including the ones that used to be listed under core: - **`stm-entrypoint`**, **`stm-slots`**, **`stm-channel`**, **`stm-worker`** (on ``): endpoint overrides. Document reads them at load and writes them into core's mutable `statum.config` before init runs (defaults `/_statum/entrypoint`, `/_statum/slots`, `/_statum/channel`, `/_statum/resource/statum-worker.js`). - **`stm-preloader`** / **`stm-content`**: boot visibility. While the entrypoint loads, `stm-preloader` elements are shown and `stm-content` elements hidden; once ready, the reverse. Set `hidden` on `stm-content` yourself in the HTML to avoid a flash of unbound template. - **`stm-guard`** (a ``, plus the `pre-entrypoint` attribute on the link): a client-side UX gate — when `require` evaluates false, the page redirects to `href`. Re-checked every render cycle; guards without `pre-entrypoint` only evaluate after the entrypoint has loaded. This is UX sugar, NOT security. **`statum.registerAttribute(name, fn, {after, before})`** — the extension seam, provided by this module. `name` is an exact attribute name or, ending with `.`, a prefix matcher. `fn(el, scope)` runs once per element per render cycle for elements carrying a match; iterate `el.attributes` yourself when several match a prefix. `after`/`before` name another registered attribute and position the handler relative to it; an anchor that is not (yet) registered warns and keeps registration order — order is resolved at registration time, so a module that wants to be after `stm-html` should load after statum-document. Duplicate names warn and are ignored. Handler exceptions abort that render cycle (caught by renderAll's single try/catch) — same contract as the built-ins. Registering against core alone is safe: core queues the registration until document installs the registry. Walk bindings: - **`stm-if` / `stm-else-if` / `stm-else`**: conditional chains among siblings. Branches are toggled with the `hidden` attribute (they stay in the DOM and are re-evaluated every cycle). `stm-else-if`/`stm-else` are owned by their chain head's `stm-if`. - **`stm-for-{name}-in`** (with optional **`stm-key`**): loop over an array expression. The element is a HOLDER: its original server-rendered content is hidden once data arrives and clones of the holder are inserted after it, one per item, with `{name}` bound to the item in child scopes. Unresolved (`undefined`) source: instances are cleared and the holder's original content restored (progressive enhancement). Keyed loops (`stm-key`, an expression over the item, usually a field) REUSE DOM nodes for unchanged keys, preserving focus/selection/input state; unkeyed loops rebuild wholesale. `data-stm-active="1"` + `hidden` mark an active holder. - **`stm-local-{name}="expr"`**: local variable (statum-binding.js). The expression is evaluated LAZILY (at most once per render pass, only when read) against the parent scope; the variable is visible to this element and its descendants (a child scope) and shadows slot types/outer variables. Multiple locals per element; same-element locals cannot reference each other. Replaces the removed `stm-def-{name}-as` (mkpstm's generated fragment-parameter attribute included). - **`stm-text`** / **`stm-html`**: set text content / inner HTML from an expression. `undefined` leaves content untouched; otherwise stringified (`null` → `''`). Note (known issue C1): on editable controls `stm-text` clobbers user input every cycle — use `stm-prefill` for form fields. - **`stm-class.{name}`**: predicate per class. Truthy → `classList.add`, falsy → remove. - **`stm-attribute.{name}`**: boolean → set/remove (preferring the IDL property when one exists); any other defined value → `setAttribute`; `undefined` → remove. Note (known issue C2): for `value` this writes the ATTRIBUTE (the default), not the live property — server-wins semantics; live form values want `stm-prefill`. - **`stm-disabled`**: truthy → disable the element and descendant controls. - **`stm-action`**: binds the element's primary event (`click`, or `submit` for a `
`) to an action object resolved from state. The action object (shape `{ uri, method, private }`) is authored server-side and embedded in slot data. Companions: - **`stm-data-{key}`**: string data collected into the action's payload. - **`stm-confirm`**: gates the action behind the confirm handler (`statum.onConfirmHandler`; default `window.confirm`). - **`stm-busy-class`**: classes applied to the action element while busy. - **`stm-on-error`** / **`stm-on-success`**: class names applied on failure / success respectively (mirrored: each clears the other; a new attempt clears both). The element's `__stmAction` promise (and `__stmActionFailed` flag) are stashed per trigger for `stm-clear-on-success`. ### Section: statum-markdown.js The CommonMark 0.31.2 engine (escape-first: raw source HTML is always neutralised; `javascript:`/`data:`/`vbscript:` URLs are filtered) plus its bindings: - **`stm-markdown`**: renders the expression's markdown to safe HTML into the element (`innerHTML`). Runs after `stm-html`. - **`stm-h-demotion`**: demotes child header tags (`h1`→`h2` …) by N levels, capping at `h6`. Runs after `stm-markdown` so rendered headers exist. Both live in this module — a core+document page sees them do nothing. - Programmatic: **`statum.markdown.render(src)`** → HTML string; **`statum.markdown.renderTo(el, src)`** — same code path as the binding. ### Section: statum-document.js (form bindings) Form-state bindings (hosted in the document module since the former `statum-forms.js` was folded into it): - **`stm-prefill`**: writes an expression into form controls as PROPERTIES every cycle: text-like inputs + textarea → `.value` (null → `''`); checkbox → `.checked` (truthiness); radio → `.checked = (el.value === String(value))` — give every member of a radio group the same `stm-prefill` expression; select (single) → `.value`; select (multiple) → per-option `.selected` when the value is an array. `file` inputs are skipped (script assignment is forbidden by browsers). DIRTY TRACKING: capture-phase `input`/`change` mark the element dirty; a dirty element is NEVER overwritten by prefill; the form's `reset` event un-dirties its controls (also the server `form-reset` directive's path). Dirty state is per-element, so keyed-loop forms keep their own dirty state per instance. - **`stm-clear-on-success`** (on a `` with its own `stm-action`): when the form's action promise resolves successfully, reset exactly THAT form (values back to defaults, un-dirtied). On failure the fields stay. A form carrying it without `stm-action` warns once. - **`stm-clear-on-error`** (on a `` with its own `stm-action`): the mirror — when the form's own action FAILS (HTTP error or `error` directive), reset exactly that form. A DECLINED `stm-confirm` does not fire it: decline is cancellation, not error. A form carrying it without `stm-action` warns once. - **`stm-on-success`** (on an action element): classes applied when the action succeeds — the mirror of `stm-on-error`. The two are mirrored: a success clears the error classes, a failure clears the success classes, and a new attempt clears both at its start. - Programmatic: **`statum.document.reset(formOrEl)`**, **`statum.document.markDirty(el)`**, **`statum.document.isDirty(el)`**. ### Section: statum-binding.js Client-side variables usable in any other `stm-*` expression. All are LAZY-CACHED: a binding's expression evaluates at most once per render pass, only when something reads the variable; the cache resets each pass. Cycles (`a` reading `b` reading `a`) log `[statum] variable cycle: a → b → a` and the variable reads as `undefined` for the pass — never a page crash. - **`stm-global-{name}="expr"`**: a page-wide variable, visible in any expression anywhere — including elements that precede its declaring element in document order (the pass-start sweep exposes lazy getters). Duplicate declarations: last in document order wins, one warn per pass. - **`stm-global-{name}-from-{attribute}`**: page-wide variable whose value is the element's attribute; attribute changes trigger a redraw. - **`stm-local-{name}="expr"`**: subtree-scoped variable (element + descendants). Replaces the removed `stm-def-{name}-as`. - **`stm-local-{name}-from-{attribute}`**: subtree-scoped attribute-sourced variable; attribute changes trigger a redraw. - **`stm-declare-{name}`**: declares `{name}` for a subtree WITHOUT a value, so a descendant binds it (`stm-local-{name}=…` or `stm-local-{name}-from-…`) and SIBLINGS of that binder can read it: ```html

Username is too short

``` **Property form**: `from-value` and `from-checked` on input/select/textarea read the live PROPERTY, not the attribute — an input/change listener updates the variable and redraws immediately (per keystroke). Every other attribute name (and every non-form-control element) uses a MutationObserver. REDRAW GUARD: an observed attribute change only redraws when the variable's derived value actually changed — the walk itself writes attributes (`stm-attribute.*`, `hidden`, `data-stm-active`), so a naive observer would loop forever. Declared-but-unbound variables read `undefined` (an expression like `x.length` then degrades to `undefined` for that expression via the evaluator's error policy — the page stays alive). Load order: after core and document; order-independent of markdown, and need not co-exist with it. ## Part III — Quick reference: load order and cherry-picking Canonical: core → document → markdown → binding (markdown and binding are order-independent of each other and need not co-exist). Cherry-pick trailing modules only. Without markdown: no `stm-markdown`, no `stm-h-demotion`. Without binding: no `stm-global-*` / `stm-local-*` / `stm-declare-*` variables (and the removed `stm-def-*` is inert everywhere). Core alone is HEADLESS — it applies state and directives with no DOM at all (point `statum.config` at your endpoints before init), so a non-document templating framework can use it as its state layer. Without document there is no render walk, no `stm-*` bindings, no `statum.registerAttribute` (it is document API), no `statum.document`, no boot visibility, no guards. All modules assume core; loading any module first logs an error and does nothing.