瀏覽代碼

Document the module split: WHATS_NEW.md, delivery story, mkpstm expansion, S-4 status

WHATS_NEW.md is the short, example-driven summary for developers: the four module tags and their load contract, stm-prefill and its dirty semantics across the control matrix, stm-clear-on-success and statum.forms, statum.markdown.render/renderTo, statum.registerAttribute for module authors, and the reenter directive.

getting-started.md now shows the four per-module includes and explains cherry-picking and the build-time expansion of legacy statum.js references (also in the pstm-res table), and points at tests/markdown, tests/split and tests/dom. model.md gains a Delivery and module structure section (module list, load order, registerAttribute semantics). statum-mkpstm's header documents the expansion, warning and dedupe rules. S-4 in statum-issues.md is marked addressed by stm-prefill, superseding the set-once proposal; stm-attribute.value semantics unchanged.
saddle-ClankerSaddle 3 周之前
父節點
當前提交
9d36e77c03
共有 5 個文件被更改,包括 250 次插入 和 12 次删除
  1. 143 0
      WHATS_NEW.md
  2. 42 9
      getting-started.md
  3. 47 0
      model.md
  4. 7 1
      statum-issues.md
  5. 11 2
      tools/statum-mkpstm/statum-mkpstm.vala

+ 143 - 0
WHATS_NEW.md

@@ -0,0 +1,143 @@
+# What's new — the Statum client is now four modules
+
+The single-file client (`js/statum.js`, served as
+`/_statum/resource/statum.js`) is gone. The browser client now ships as four
+ES5, dependency-free scripts with a strict 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>
+```
+
+**Load order is a contract.** `statum-core.js` must be first: it owns the slot
+protocol and installs the seams (`statum.registerAttribute`, the render
+driver, the internal bridge) that every later module registers into. The
+canonical order is core → document → markdown → forms.
+
+You may cherry-pick trailing modules (core first, always): core + document
+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.
+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
+load the markdown module.
+
+Legacy pages that still say `<script pstm-res-src="statum.js"></script>`
+keep working: `statum-mkpstm` expands that reference at build time into the
+four tags above (one build warning per page names the expansion). 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.
+The single-file artifact itself no longer exists; in new pages, just write
+the four tags.
+
+Depth on all of this: `model.md` (wire protocol, directives, every
+attribute) and `getting-started.md` (tutorial + full JS API).
+
+## stm-prefill — state into controls, without clobbering edits
+
+`stm-prefill` writes a state expression into form controls as **properties**,
+not attributes, every render cycle:
+
+```html
+<input type="text" name="name" stm-prefill="profile.name">
+<select name="theme" stm-prefill="profile.theme">…</select>
+<input type="checkbox" name="news" stm-prefill="profile.news">
+```
+
+The control matrix:
+
+- **Text-like inputs and `textarea`** — writes `.value` (`null`/`undefined`
+  become `''`; `undefined` leaves the element untouched).
+- **Select** — single: writes `.value`; `multiple`: sets each option's
+  `.selected` from an array of string values.
+- **Checkbox** — writes `.checked`, coerced truthy/falsy.
+- **Radio** — per-element: give every member of the group the same
+  expression and the one whose `value` matches is checked.
+
+  ```html
+  <input type="radio" name="cadence" value="daily" stm-prefill="profile.cadence">
+  <input type="radio" name="cadence" value="weekly" stm-prefill="profile.cadence">
+  ```
+
+- **File inputs are skipped**: assigning `.value` from a script is forbidden
+  by the platform, so there is nothing sensible to write.
+
+**Dirty tracking is the point.** The moment a user types in or changes a
+control (capture-phase `input`/`change` listeners), it is *dirty* and
+stm-prefill never overwrites it again — the edit outranks the state
+snapshot. A form `reset` (a reset button, `statum.forms.reset`, or the
+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
+per-element (a `WeakSet`), a form inside a keyed `stm-loop` instance keeps
+its dirty state across re-renders: the instance owns its nodes.
+
+## stm-clear-on-success and statum.forms
+
+A form whose own `stm-action` succeeds can clear itself:
+
+```html
+<form stm-action="profile.saveAction" stm-clear-on-success>…</form>
+```
+
+The clear waits for the action's promise and resets the form only when it
+resolved (a failed action leaves the fields as they are). The reset also
+un-dirties the controls, so the next render cycle re-applies `stm-prefill`
+against the fresh state — and `stm-clear-on-success` is scoped to exactly
+this form; no other form on the page is touched.
+
+The `statum.forms` API:
+
+```js
+statum.forms.reset(formOrElement); // reset the form owning the element
+statum.forms.markDirty(el);        // force an element dirty (prefill skips it)
+statum.forms.isDirty(el);          // query dirtiness
+```
+
+## statum.markdown — the renderer as an API
+
+The CommonMark engine (unchanged, just moved) is now callable directly, on
+the same code path the `stm-markdown` binding uses:
+
+```js
+statum.markdown.render(src);   // → safe HTML string
+statum.markdown.renderTo(el, src); // replaces el's content
+```
+
+Escape-first posture unchanged: raw source HTML is neutralised and
+`javascript:`/`data:` URLs never survive as hrefs.
+
+## statum.registerAttribute — write your own bindings
+
+Module authors extend the render walk without touching core:
+
+```js
+statum.registerAttribute('stm-foo', function (el, scope) {
+  // called once per element per render cycle when el carries stm-foo
+}, { after: 'stm-html' }); // or before: 'stm-class.'
+```
+
+A trailing dot makes the name a **prefix matcher** (`'stm-class.'` matches
+`stm-class.hidden`); the handler then iterates the element's attributes
+itself. `after`/`before` name another registered attribute and insert
+relative to it — order matters (`stm-prefill` runs before `stm-class.*`;
+markdown renders before `stm-h-demotion` demotes). A constraint naming an
+unregistered attribute warns and keeps registration order; handlers get no
+per-handler try/catch — a throwing handler aborts that render cycle, exactly
+like the built-ins.
+
+## The reenter directive
+
+A response can ask the client to re-fetch the page's entrypoint for the
+current URL and apply its directives in place — no page reload:
+
+```json
+{ "type": "reenter" }
+```
+
+Use it after an action that invalidates page-level state. `reenter` runs
+after `form-reset`/`form-submit` and before `navigate`/`post` (navigation
+still wins if both appear), and the refresh is awaited — the originating
+action's promise (and so `stm-clear-on-success` and `statum.act`) resolves
+only after the re-entered state has been applied. A failed refresh is logged
+and swallowed.

+ 42 - 9
getting-started.md

@@ -29,8 +29,7 @@ It provides:
  Browser                          Server
  ───────                          ──────
  GET / ──────────────────────►   StatumPage (precompressed HTML)
- page loads, statum.js boots
- GET /_statum/entrypoint ────►   StatumEntrypoint → directives (set/clear/...)
+ page loads, the statum client modules boot GET /_statum/entrypoint ────►   StatumEntrypoint → directives (set/clear/...)
  client applies directives
  client binds stm-* attributes
 
@@ -68,7 +67,10 @@ It provides:
 <head>
     <pstm-name>main</pstm-name>
     <pstm-title>My App</pstm-title>
-    <script pstm-res-src="statum.js"></script>
+    <script pstm-res-src="statum-core.js"></script>
+    <script pstm-res-src="statum-document.js"></script>
+    <script pstm-res-src="statum-markdown.js"></script>
+    <script pstm-res-src="statum-forms.js"></script>
 </head>
 <body>
     <header><h1>My App</h1></header>
@@ -142,6 +144,7 @@ All `pstm-*` elements **must appear inside `<head>`** (except `pstm-content` and
 | `<pstm-fragment name="card" />` | body | Inlines a fragment (component) by name. |
 | `pstm-materialise-as="div"` | `<body>` attr | When this body is composited into a template or fragment, it is wrapped in the named element. |
 | `pstm-res-src="logo.png"` | any attr | Rewrites to `src="/_statum/resource/logo.png"`. Also `pstm-res-href`, etc. |
+| `pstm-res-src="statum.js"` | `<script>` | **Legacy.** Expands at build time into the four client module tags in load order (one warning per page; explicit module tags are deduped). Prefer the four explicit tags. |
 
 ### Head merging
 
@@ -364,14 +367,30 @@ No route is specified — a GUID is auto-assigned at registration.
 
 ## JS API Reference
 
-The Statum client (`statum.js`) is embedded in the library and served from
-`/_statum/resource/statum.js`. Include it in your template:
+The Statum client is embedded in the library and served as four modules from
+`/_statum/resource/statum-core.js`, `statum-document.js`,
+`statum-markdown.js` and `statum-forms.js`. Include them in your template, in
+this order (core installs the seams the later modules register into):
 
 ```html
-<script pstm-res-src="statum.js"></script>
+<script pstm-res-src="statum-core.js"></script>
+<script pstm-res-src="statum-document.js"></script>
+<script pstm-res-src="statum-markdown.js"></script>
+<script pstm-res-src="statum-forms.js"></script>
 ```
 
-It auto-initialises on DOMContentLoaded.
+You may cherry-pick trailing modules (core first, always): core + document
+gives the full state-binding walk without the markdown engine or the form
+bindings — but note `stm-markdown` and `stm-h-demotion` live in the markdown
+module and do nothing without it.
+
+A legacy `<script pstm-res-src="statum.js"></script>` still works:
+`statum-mkpstm` expands it at build time into the four tags above (one build
+warning per page names the expansion; a page mixing the legacy reference with
+explicit module tags is deduplicated). Prefer writing the four tags
+explicitly in new pages.
+
+The client auto-initialises on DOMContentLoaded.
 
 ### Configuration overrides (body attributes)
 
@@ -465,8 +484,8 @@ statum.afterEvaluate(function() {
 
 ### Running the markdown tests
 
-The `stm-markdown` renderer (`js/statum.js`) has a JavaScript test suite that
-does not require Node.js — it runs under
+The `stm-markdown` renderer (`js/statum-markdown.js`) has a JavaScript test
+suite that does not require Node.js — it runs under
 [QuickJS](https://bellard.org/quickjs/), whose sources are vendored in
 `tools/qjs/` and compiled on demand by `gcc`:
 
@@ -480,6 +499,20 @@ example suite (excluding the raw-HTML sections, which Statum escapes by
 design) and fails when it drops below the 95% target. See
 `tests/markdown/README.md` for details.
 
+### Running the module-split and DOM-shim tests
+
+Two more QuickJS suites cover the split client itself:
+
+```sh
+tests/split/run.sh    # modules load in order and register into the seams
+tests/dom/run.sh      # the document walk + stm-prefill dirty/skip/reset
+```
+
+`tests/split` proves each module boots, registers its attributes through the
+core seam in the canonical order, and that cherry-picked subsets work.
+`tests/dom` runs the render walk and the form bindings against a minimal DOM
+shim (value/checked writes, radio groups, dirty skipping, clear-on-success).
+
 ---
 
 ## Vala API Reference

+ 47 - 0
model.md

@@ -199,6 +199,53 @@ The Javascript API provides access to the state and actions via the global `stat
 
 State is exposed to the application keyed by **slot type**. Statum assumes at most one slot per type, so each type name maps to a single current snapshot; the slot's `key` is used only on the wire (in headers and directives).
 
+## Delivery and module structure
+
+The client ships as four ES5, dependency-free scripts (each a guarded IIFE
+adding to the same `window.statum` global), served as default resources:
+
+- `/_statum/resource/statum-core.js` — the slot protocol: storage and
+  persistence, `X-Statum-Slot` assembly, entrypoint/action fetch, directive
+  dispatch, SSE/SharedWorker delivery, the alert/confirm/notify override
+  mechanism, and the seams modules register into (`statum.registerAttribute`,
+  the render driver, the internal bridge).
+- `/_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`.
+- `/_statum/resource/statum-markdown.js` — the CommonMark engine plus the
+  `stm-markdown` binding, `stm-h-demotion`, and the programmatic
+  `statum.markdown` API. Zero DOM dependencies in the engine itself.
+- `/_statum/resource/statum-forms.js` — the form bindings `stm-prefill` and
+  `stm-clear-on-success`, the `statum.forms` API, and the shared dirty set.
+
+**Load order is a contract**: core first, then document, markdown, forms.
+Core installs the seams; every later module registers its attribute handlers
+and render pieces into them. Trailing modules may be omitted (a core +
+document page has the full state-binding walk minus markdown and the form
+extras — note `stm-markdown` and `stm-h-demotion` live in the markdown
+module and are inert without it).
+
+`statum-mkpstm` expands a legacy `<script pstm-res-src="statum.js">` at
+build time into the four tags in canonical order, once per module even when
+the page mixes the legacy reference with explicit module tags, and prints
+one build warning per expanded page naming the expansion. New pages should
+reference the four modules explicitly.
+
+### Extending the client: statum.registerAttribute
+
+`statum.registerAttribute(name, fn, opts)` adds a binding to the render walk
+without touching the modules. `name` is an exact attribute name, or a prefix
+matcher when it ends with a dot (`'stm-class.'` matches `stm-class.hidden`;
+the handler then iterates the element's attributes itself, since several may
+match). `fn(el, scope)` runs once per element per render cycle.
+`opts.after` / `opts.before` name another registered attribute and insert
+relative to it — order is semantic (prefill writes properties before
+`stm-class.*` predicates evaluate; markdown renders before header
+demotion). A constraint naming an unregistered attribute warns and keeps
+registration order; duplicate registrations warn and are ignored; handlers
+receive no per-handler try/catch — a throwing handler aborts that render
+cycle, exactly as the built-in handlers do.
+
 ## Initialization
 
 On startup, before fetching the entrypoint or evaluating any bindings, the library checks for a `_statum_sdc` session cookie:

+ 7 - 1
statum-issues.md

@@ -28,7 +28,7 @@ Percipio friction report.
 
 ---
 
-## Client / JS side (`js/statum.js`)
+## Client / JS side (formerly `js/statum.js`, now the four modules in `js/`)
 
 ### S-1. Slot names that shadow JS globals silently break `stm-if` → blank page — SEVERE (orig #1)
 - **What:** naming a slot `eval` made `stm-if="… eval == null …"` evaluate
@@ -67,6 +67,12 @@ Percipio friction report.
   `!important`. At minimum, document this prominently.
 
 ### S-4. `stm-text` on an editable `<input>`/`<textarea>` resets the user's typing (orig #4)
+- **Status: addressed by `stm-prefill`** (statum-forms.js). The shipped
+  mitigation is the `stm-prefill` binding: property writes with per-element
+  dirty tracking, so an edit is never overwritten until the form resets. This
+  supersedes the "set-once / `stm-value`" proposal below —
+  `stm-attribute.value`'s server-wins semantics are unchanged. See
+  `model.md` (Form prefill) and `WHATS_NEW.md`.
 - **What:** binding a form field's value with `stm-text` is one-way state→DOM;
   on any re-evaluation (e.g. after a co-equal action's `notify`) it wipes what
   the user just typed, resetting it to the stale state value.

+ 11 - 2
tools/statum-mkpstm/statum-mkpstm.vala

@@ -12,8 +12,17 @@ namespace Statum.Tools {
      * Turns an app HTML page (plus its templates/fragments) into a Vala
      * {@link Statum.StatumPage} subclass: a static, precompressed document whose
      * route comes from its `<pstm-uri>`. Composition (templates, fragments,
-     * `pstm-res` rewriting) is applied, then the result is precompressed with
-     * identity/gzip/zstd/br.
+     * `pstm-res` rewriting, legacy statum.js expansion) is applied, then the
+     * result is precompressed with identity/gzip/zstd/br.
+     *
+     * Legacy client references: a `<script pstm-res-src="statum.js">` (the
+     * pre-split single-file client) expands into the four module tags in
+     * canonical load order, with plain final `src` attributes. Explicit
+     * module references the expansion covers are subsumed and later
+     * duplicates dropped, so each module appears exactly once; one build
+     * warning per page names the page, the expansion and any dedupe. Pages
+     * referencing only module files (or plain `src` scripts) are untouched
+     * and unwarned.
      */
     public class Mkpstm : Object {