Просмотр исходного кода

Add TEMPLATES.md: pstm + stm-* markup reference

Extensive developer reference for the build-time pstm preprocessor (tags, attributes, composition, legacy statum.js expansion) and one section per runtime module (attributes, semantics, load contract, registerAttribute). Also corrects two stale attribute names in WHATS_NEW.md and model.md: the loop binding is stm-for-{name}-in (not stm-loop) and the attribute binding family is stm-attribute.* (not stm-attr-*).
saddle-ClankerSaddle 3 недель назад
Родитель
Сommit
af439775d8
3 измененных файлов с 312 добавлено и 4 удалено
  1. 308 0
      TEMPLATES.md
  2. 2 2
      WHATS_NEW.md
  3. 2 2
      model.md

+ 308 - 0
TEMPLATES.md

@@ -0,0 +1,308 @@
+# 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
+four browser modules (`statum-core.js`, `statum-document.js`,
+`statum-markdown.js`, `statum-forms.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 `<pstm-uri>` unless
+`-r` overrides it.
+
+### `<pstm-uri>` — the page route (element, page head)
+
+Child element of the page's `<head>` whose text is the route this page is
+served at (e.g. `<pstm-uri>/dashboard</pstm-uri>`). 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.
+
+### `<pstm-name>` — the page name (element, page head)
+
+Names the page for build diagnostics. Structural: consumed during
+composition, never delivered.
+
+### `<pstm-title>` — title contribution (element, page head)
+
+The page's title fragment. Titles CONCATENATE page-first: a page's
+`<pstm-title>` text is prepended to its template's title. Merged like all
+head content, then consumed.
+
+### `<pstm-template>` — 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 `<body>` contains a
+`<pstm-content>` outlet. Chains (a template naming a `<pstm-template>` of its
+own) compose recursively. An unknown template name is a build ERROR; a cyclic
+chain is not diagnosed — keep chains acyclic.
+
+### `<pstm-content>` — content outlet (element, template body)
+
+The element inside a template's `<body>` that the child's `<body>` content is
+spliced into, replacing the outlet. A template without an outlet is a build
+error (there is nowhere to splice).
+
+### `<pstm-fragment name="…">` — fragment inclusion (element, any body)
+
+Includes a named fragment. Fragments live in their own files and are declared
+inside `<pstm-fragment name="…"> … </pstm-fragment>` 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 `<head>` content is merged into the document head like a
+template's. A fragment `<body>` may carry metadata elements such as
+`<pstm-input>` (consumed, not delivered).
+
+### `pstm-materialise-as` — wrapper element (attribute)
+
+On a page's `<body>` or a fragment's `<body>` or a `<pstm-fragment>` 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}"`. `<script
+pstm-res-src="statum-core.js">` becomes `<script
+src="/_statum/resource/statum-core.js">`. Works for any attribute name
+(`src`, `href`, ...). The named resource must be registered server-side
+(`StatumConfigurator.add_resource` or the four default client modules).
+
+### The legacy `statum.js` expansion (script tags)
+
+A `<script pstm-res-src="statum.js">` — the pre-split single-file client —
+expands at build time into the four module tags in canonical load order:
+
+```html
+<script src="/_statum/resource/statum-core.js"></script>
+<script src="/_statum/resource/statum-document.js"></script>
+<script src="/_statum/resource/statum-markdown.js"></script>
+<script src="/_statum/resource/statum-forms.js"></script>
+```
+
+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 II 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
+(`statum.registerAttribute`, the render driver, the internal
+`statum._bridge`) that every later module registers into. The canonical
+order is core → document → markdown → forms. Trailing modules may be
+cherry-picked after core (e.g. core + document for pages without markdown or
+form extras — note `stm-markdown` AND `stm-h-demotion` live in the markdown
+module). 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-def-*` 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
+
+Slot protocol, HTTP, directives, realtime, boot, public JS API. Attributes it
+owns:
+
+- **`stm-entrypoint`**, **`stm-slots`**, **`stm-channel`**, **`stm-worker`**
+  (on `<body>`): override the four endpoint URLs (defaults
+  `/_statum/entrypoint`, `/_statum/slots`, `/_statum/channel`,
+  `/_statum/resource/statum-worker.js`). Read once at init.
+- **`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 `<link rel="stm-guard" require="…" href="…">`, 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.
+- **`stm-on-error`** (on action elements): space-separated class names
+  applied while the action's request is failing / after an `error` directive.
+- **`stm-class.{name}`**: class-list predicates shared by core's busy-state
+  logic and the document walk (see below for semantics).
+- **`stm-disabled`**: predicate disabling the element and its descendant form
+  controls.
+
+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`, the `onConfirmHandler` / `onNotifyHandler` /
+`onErrorHandler` getter/setter pairs (the override mechanism for
+`stm-confirm` / `notify` / failures — frontend-demo installs toast handlers
+through it), and `registerAttribute` (below).
+
+**`statum.registerAttribute(name, fn, {after, before})`** — the extension
+seam. `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.
+
+
+### Section: statum-document.js
+
+The render walk and the structural/value bindings. Installs the render
+driver. Attributes:
+
+- **`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-def-{name}-as`**: local definition. The expression is evaluated
+  against the incoming scope and the result is visible to this element and
+  its descendants (a child scope). Multiple defs per element; defs cannot
+  reference each other within the same element. Used heavily by generated
+  fragment markup.
+- **`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 `<form>`) 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`**: see core section.
+  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-forms.js
+
+Form-state bindings:
+
+- **`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 `<form>` 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.
+- Programmatic: **`statum.forms.reset(formOrEl)`**, **`statum.forms.markDirty(el)`**,
+  **`statum.forms.isDirty(el)`**.
+
+
+## Part III — Quick reference: load order and cherry-picking
+
+Canonical: core → document → markdown → forms. Cherry-pick trailing modules
+only. Without markdown: no `stm-markdown`, no `stm-h-demotion`. Without
+forms: no `stm-prefill`, no `stm-clear-on-success`, no `statum.forms`. All
+modules assume core; loading any module first logs an error and does nothing.

+ 2 - 2
WHATS_NEW.md

@@ -18,7 +18,7 @@ canonical order is core → document → markdown → forms.
 
 
 You may cherry-pick trailing modules (core first, always): core + document
 You may cherry-pick trailing modules (core first, always): core + document
 gives you the full state-binding walk (`stm-text`, `stm-html`, `stm-if`,
 gives you the full state-binding walk (`stm-text`, `stm-html`, `stm-if`,
-`stm-loop`, `stm-action`, …) with no markdown engine and no form extras.
+`stm-for-…-in`, `stm-action`, …) with no markdown engine and no form extras.
 Note that `stm-markdown` *and* `stm-h-demotion` live in the markdown module,
 Note that `stm-markdown` *and* `stm-h-demotion` live in the markdown module,
 so a core+document page sees those attributes do nothing — remove them, or
 so a core+document page sees those attributes do nothing — remove them, or
 load the markdown module.
 load the markdown module.
@@ -69,7 +69,7 @@ stm-prefill never overwrites it again — the edit outranks the state
 snapshot. A form `reset` (a reset button, `statum.forms.reset`, or the
 snapshot. A form `reset` (a reset button, `statum.forms.reset`, or the
 server's `form-reset` directive — all funnel through the same `reset` event)
 server's `form-reset` directive — all funnel through the same `reset` event)
 un-dirties the form's controls so state can re-apply. Because dirtiness is
 un-dirties the form's controls so state can re-apply. Because dirtiness is
-per-element (a `WeakSet`), a form inside a keyed `stm-loop` instance keeps
+per-element (a `WeakSet`), a form inside a keyed loop instance keeps
 its dirty state across re-renders: the instance owns its nodes.
 its dirty state across re-renders: the instance owns its nodes.
 
 
 ## stm-clear-on-success and statum.forms
 ## stm-clear-on-success and statum.forms

+ 2 - 2
model.md

@@ -210,8 +210,8 @@ adding to the same `window.statum` global), served as default resources:
   mechanism, and the seams modules register into (`statum.registerAttribute`,
   mechanism, and the seams modules register into (`statum.registerAttribute`,
   the render driver, the internal bridge).
   the render driver, the internal bridge).
 - `/_statum/resource/statum-document.js` — the render walk and the structural
 - `/_statum/resource/statum-document.js` — the render walk and the structural
-  bindings: `stm-text`, `stm-html`, `stm-attr-*`, `stm-class`, `stm-if/else`,
-  `stm-disabled`, `stm-loop`, `stm-def-*`, `stm-action`.
+  bindings: `stm-text`, `stm-html`, `stm-attribute.*`, `stm-class.*`,
+  `stm-if/else`, `stm-for-…-in`, `stm-disabled`, `stm-def-*`, `stm-action`.
 - `/_statum/resource/statum-markdown.js` — the CommonMark engine plus the
 - `/_statum/resource/statum-markdown.js` — the CommonMark engine plus the
   `stm-markdown` binding, `stm-h-demotion`, and the programmatic
   `stm-markdown` binding, `stm-h-demotion`, and the programmatic
   `statum.markdown` API. Zero DOM dependencies in the engine itself.
   `statum.markdown` API. Zero DOM dependencies in the engine itself.