|
@@ -0,0 +1,451 @@
|
|
|
|
|
+# Feature Plan — Client attributes, form directives, hooks, stale-set, meson generator
|
|
|
|
|
+
|
|
|
|
|
+## Features
|
|
|
|
|
+
|
|
|
|
|
+1. **`stm-markdown`** — render untrusted markdown as safe HTML (all source HTML
|
|
|
|
|
+ escaped first).
|
|
|
|
|
+2. **`stm-h-demotion`** — demote child header tags by N levels (runs after
|
|
|
|
|
+ `stm-html` / `stm-markdown`).
|
|
|
|
|
+3. **Form directives** (`form-submit`, `form-reset`) — programmatically submit or
|
|
|
|
|
+ reset a form by ID from a server directive.
|
|
|
|
|
+4. **Before/after evaluation hooks** — `statum.beforeEvaluate(fn)` /
|
|
|
|
|
+ `statum.afterEvaluate(fn)` JS API.
|
|
|
|
|
+5. **Automatic `set` for stale slots** — server auto-refreshes a client's stale
|
|
|
|
|
+ snapshot (when the client's `as_at` predates the slot's last update).
|
|
|
|
|
+6. **Meson generator for `mkpstm`** — a reusable `statum_page()` helper so apps
|
|
|
|
|
+ don't write a `custom_target` per page.
|
|
|
|
|
+7. **Documentation updates** — `model.md`, `getting-started.md`.
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## A. `stm-markdown` attribute — `js/statum.js`
|
|
|
|
|
+
|
|
|
|
|
+### Design
|
|
|
|
|
+
|
|
|
|
|
+A built-in minimal markdown parser (no external dependencies, since the client
|
|
|
|
|
+is self-contained). The pipeline is:
|
|
|
|
|
+
|
|
|
|
|
+1. Escape **all** HTML entities in the raw string first (`<` → `<`, etc.) so
|
|
|
|
|
+ untrusted input cannot inject markup.
|
|
|
|
|
+2. Apply markdown formatting rules that produce safe HTML tags.
|
|
|
|
|
+
|
|
|
|
|
+Supported syntax: headings (`#`–`######`), bold (`**`), italic (`*`), inline
|
|
|
|
|
+code (`` ` ``), fenced code blocks (```` ``` ````), links (`[text](url)`),
|
|
|
|
|
+unordered lists (`-`/`*`), ordered lists (`1.`), blockquotes (`>`), horizontal
|
|
|
|
|
+rules (`---`), and paragraphs (blank-line separated).
|
|
|
|
|
+
|
|
|
|
|
+### Implementation
|
|
|
|
|
+
|
|
|
|
|
+- Add `renderMarkdown(string raw) → string html` in the client IIFE.
|
|
|
|
|
+- In `handleTextHtml`, after the `stm-text` / `stm-html` checks:
|
|
|
|
|
+ ```js
|
|
|
|
|
+ if (el.hasAttribute('stm-markdown')) {
|
|
|
|
|
+ var md = evalExpr(el.getAttribute('stm-markdown'), scope);
|
|
|
|
|
+ if (md !== undefined) el.innerHTML = renderMarkdown(md === null ? '' : String(md));
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
+- Order matters: `stm-markdown` runs **before** `stm-h-demotion` (B) so the
|
|
|
|
|
+ demotion sees the rendered headers.
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## B. `stm-h-demotion` attribute — `js/statum.js`
|
|
|
|
|
+
|
|
|
|
|
+### Design
|
|
|
|
|
+
|
|
|
|
|
+`stm-h-dementia="1"` demotes every `<h1>`–`<h5>` child by N levels: `<h1>` →
|
|
|
|
|
+`<h2>`, `<h2>` → `<h3>`, etc. `<h6>` stays `<h6>` (HTML caps at 6). The attribute
|
|
|
|
|
+is read from the same element that carries `stm-html` / `stm-markdown`, and must
|
|
|
|
|
+run **after** those have set `innerHTML`.
|
|
|
|
|
+
|
|
|
|
|
+### Implementation
|
|
|
|
|
+
|
|
|
|
|
+- Add `handleHDemotion(el)` called from `handleTextHtml` **after** the
|
|
|
|
|
+ `stm-html` / `stm-markdown` blocks, when the element has `stm-h-demotion`.
|
|
|
|
|
+- Walk `el.querySelectorAll('h1,h2,h3,h4,h5,h6')`, read the current level from
|
|
|
|
|
+ the tag name, add N (capped at 6), and replace the element with a new one of
|
|
|
|
|
+ the demoted tag (preserving attributes and children via
|
|
|
|
|
+ `document.createElement` + `replaceWith`).
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## C. Form directives — `src/Model/DirectiveDto.vala`, `js/statum.js`, `src/DirectiveBuilder.vala`
|
|
|
|
|
+
|
|
|
|
|
+### Design
|
|
|
|
|
+
|
|
|
|
|
+Two new directive types:
|
|
|
|
|
+
|
|
|
|
|
+```json
|
|
|
|
|
+{"type":"form-submit","form":"checkout","action":"/pay","method":"POST"}
|
|
|
|
|
+{"type":"form-reset","form":"search"}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- `form` — the `id` of the `<form>` element to target.
|
|
|
|
|
+- `form-submit` optionally carries `action` (a URI string) and `method` (`"POST"`
|
|
|
|
|
+ or `"GET"`) — if present, the client sets `form.action` and `form.method`
|
|
|
|
|
+ before calling `form.submit()`.
|
|
|
|
|
+- `form-reset` calls `form.reset()`.
|
|
|
|
|
+
|
|
|
|
|
+Both are **terminal** (executed after non-terminal directives, alongside
|
|
|
|
|
+`navigate` / `post`).
|
|
|
|
|
+
|
|
|
|
|
+### Implementation
|
|
|
|
|
+
|
|
|
|
|
+- **`DirectiveDto.vala`**: add `FormSubmitDirectiveDto` (form, optional action,
|
|
|
|
|
+ optional method) and `FormResetDirectiveDto` (form), each with a mapper +
|
|
|
|
|
+ `from_json` dispatch.
|
|
|
|
|
+- **`DirectiveBuilder.vala`**: add
|
|
|
|
|
+ `.form_submit(string form_id, string? action = null, string? method = null)`
|
|
|
|
|
+ and `.form_reset(string form_id)`.
|
|
|
|
|
+- **`js/statum.js`**:
|
|
|
|
|
+ - `applyDirectives`: add `case 'form-submit'` / `case 'form-reset'` to the
|
|
|
|
|
+ `result.terminal` collection.
|
|
|
|
|
+ - `executeTerminal`: call `doFormSubmit(d)` / `doFormReset(d)`:
|
|
|
|
|
+ ```js
|
|
|
|
|
+ function doFormSubmit(d) {
|
|
|
|
|
+ var form = document.getElementById(d.form);
|
|
|
|
|
+ if (form) {
|
|
|
|
|
+ if (d.action) form.action = d.action;
|
|
|
|
|
+ if (d.method) form.method = d.method;
|
|
|
|
|
+ form.submit();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ function doFormReset(d) {
|
|
|
|
|
+ var form = document.getElementById(d.form);
|
|
|
|
|
+ if (form) form.reset();
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## D. Before/after evaluation hooks — `js/statum.js`
|
|
|
|
|
+
|
|
|
|
|
+### Design
|
|
|
|
|
+
|
|
|
|
|
+```js
|
|
|
|
|
+statum.beforeEvaluate(function() {
|
|
|
|
|
+ // Runs before each render cycle; no arguments.
|
|
|
|
|
+ document.body.classList.add('evaluating');
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+statum.afterEvaluate(function() {
|
|
|
|
|
+ // Runs after render + guard evaluation; DOM is up to date.
|
|
|
|
|
+ highlightCodeBlocks();
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- `beforeEvaluate` callbacks take **no arguments** and run before `render()` in
|
|
|
|
|
+ `renderAll`. They cannot mutate the scope (the scope is built internally from
|
|
|
|
|
+ held-slot state).
|
|
|
|
|
+- `afterEvaluate` callbacks take **no arguments** and run after `render()` +
|
|
|
|
|
+ guard evaluation.
|
|
|
|
|
+- Both are called on every render cycle (initial render, entrypoint hydration,
|
|
|
|
|
+ set/clear, realtime events).
|
|
|
|
|
+
|
|
|
|
|
+### Implementation
|
|
|
|
|
+
|
|
|
|
|
+- Module-level `var beforeHooks = []; var afterHooks = [];`.
|
|
|
|
|
+- `renderAll()`:
|
|
|
|
|
+ ```js
|
|
|
|
|
+ function renderAll() {
|
|
|
|
|
+ if (guardRedirected) return;
|
|
|
|
|
+ beforeHooks.forEach(function(fn) { try { fn(); } catch(e){} });
|
|
|
|
|
+ try { render(document.body, stateScope()); } catch(e) { console.error('[statum] render error', e); }
|
|
|
|
|
+ afterHooks.forEach(function(fn) { try { fn(); } catch(e){} });
|
|
|
|
|
+ evaluateGuards();
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
+- Public API: `window.statum.beforeEvaluate = function(fn) { beforeHooks.push(fn); };`
|
|
|
|
|
+ `window.statum.afterEvaluate = function(fn) { afterHooks.push(fn); };`
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## E. Automatic `set` for stale slots — `src/HeldSlotResolver.vala`, `src/StatumDirectives.vala`, endpoints
|
|
|
|
|
+
|
|
|
|
|
+### Design
|
|
|
|
|
+
|
|
|
|
|
+When the client sends a slot reference (`key=K; as_at=T`) and the server's cached
|
|
|
|
|
+slot has `current_state.last_touched > T`, the client is holding a stale
|
|
|
|
|
+snapshot. The server automatically includes a fresh `set` directive (re-signed
|
|
|
|
|
+frame) for that slot, so the client refreshes without the handler explicitly
|
|
|
|
|
+emitting one.
|
|
|
|
|
+
|
|
|
|
|
+This covers the common case: tab A updates a slot (via an action), tab B (still
|
|
|
|
|
+holding the old snapshot) sends a request — the server sees tab B's stale `as_at`
|
|
|
|
|
+and auto-refreshes it.
|
|
|
|
|
+
|
|
|
|
|
+### Implementation
|
|
|
|
|
+
|
|
|
|
|
+- **`ResolvedSlots`**: add `Series<string> stale_keys` — slot keys whose
|
|
|
|
|
+ `as_at` predates `last_touched`.
|
|
|
|
|
+- **`HeldSlotResolver.resolve_reference`**: after `get_slot(key)` succeeds,
|
|
|
|
|
+ compare the client's `as_at` (parsed from the header) with
|
|
|
|
|
+ `slot.current_state.last_touched`. If `as_at < last_touched`, add the key to
|
|
|
|
|
+ `stale_keys` (the slot is still resolved into `held` — the handler can use it;
|
|
|
|
|
+ the auto-set just refreshes the client).
|
|
|
|
|
+- **`StatumDirectives.to_result`**: after appending handler directives, sign
|
|
|
|
|
+ each stale key (`state_service.sign_slot(key)`) and append a `set` directive.
|
|
|
|
|
+ This needs `StateService` injected into `StatumDirectives` or passed as a
|
|
|
|
|
+ parameter. Prefer passing `state_service` and `stale_keys` as additional params
|
|
|
|
|
+ to `to_result`.
|
|
|
|
|
+- **Entrypoint + Action endpoints**: pass `resolved.stale_keys` to `to_result`.
|
|
|
|
|
+
|
|
|
|
|
+Edge case: if `as_at` is absent (malformed reference), skip the staleness check
|
|
|
|
|
+(treat as fresh).
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## F. Meson generator for `mkpstm` — `tools/meson` or root `meson.build`
|
|
|
|
|
+
|
|
|
|
|
+### Design
|
|
|
|
|
+
|
|
|
|
|
+Provide a reusable meson function so apps register pages without writing a
|
|
|
|
|
+`custom_target` each time:
|
|
|
|
|
+
|
|
|
|
|
+```meson
|
|
|
|
|
+# In the app's meson.build:
|
|
|
|
|
+statum = import('statum') # or: subdir('../tools/statum_meson')
|
|
|
|
|
+
|
|
|
|
|
+home_src = statum.page('home', namespace: 'MyApp', deps: ['main.html', 'card.html'])
|
|
|
|
|
+about_src = statum.page('about', namespace: 'MyApp', deps: ['main.html'])
|
|
|
|
|
+
|
|
|
|
|
+executable('myapp', ['Main.vala', home_src, about_src], ...)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Meson doesn't support `import()` for project-local modules, so the practical
|
|
|
|
|
+approach is a **helper function** in a shared meson snippet:
|
|
|
|
|
+
|
|
|
|
|
+```meson
|
|
|
|
|
+# statum_pages.meson (included via configure_file or subdir)
|
|
|
|
|
+statum_mkpstm = ... # the tool target
|
|
|
|
|
+
|
|
|
|
|
+function statum_page = function(name, namespace: '', extra_deps: []) -> meson object
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Meson functions are not first-class, so the realistic implementation is a
|
|
|
|
|
+**template custom_target** that the app includes:
|
|
|
|
|
+
|
|
|
|
|
+```meson
|
|
|
|
|
+# In the app's meson.build, after subdir('tools'):
|
|
|
|
|
+statum_mkpstm = statum_mkpstm # the tool executable target
|
|
|
|
|
+
|
|
|
|
|
+home_page = custom_target(
|
|
|
|
|
+ input: 'home.html',
|
|
|
|
|
+ output: 'HomePage.vala',
|
|
|
|
|
+ command: [statum_mkpstm, '-o', '@OUTPUT@', '-n', 'HomePage', '--ns=MyApp', '@INPUT@'],
|
|
|
|
|
+ depend_files: files('main.html', 'card.html')
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+The improvement: ship a **`statum_page()` meson function** in a
|
|
|
|
|
+`tools/statum_meson.build` that apps `subdir()` or copy, reducing per-page
|
|
|
|
|
+boilerplate to one line:
|
|
|
|
|
+
|
|
|
|
|
+```meson
|
|
|
|
|
+home_page = statum_page('home', 'HomePage', 'MyApp', files('main.html'))
|
|
|
|
|
+about_page = statum_page('about', 'AboutPage', 'MyApp', files('main.html'))
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+where `statum_page` is defined as:
|
|
|
|
|
+
|
|
|
|
|
+```meson
|
|
|
|
|
+statum_mkpstm = statum_mkpstm # set by tools/meson.build
|
|
|
|
|
+
|
|
|
|
|
+function statum_page = ... (not supported)
|
|
|
|
|
+
|
|
|
|
|
+# Instead, a foreach-driven helper:
|
|
|
|
|
+# statum_page(name, class, ns, deps) → custom_target
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Since meson lacks user-defined functions, the cleanest approach is a
|
|
|
|
|
+**`configure_file`-generated meson snippet** or simply documenting the
|
|
|
|
|
+one-liner `custom_target` pattern with a shorter invocation. The practical
|
|
|
|
|
+outcome: a documented `statum_page` pattern (one custom_target line per page,
|
|
|
|
|
+not six) plus a `depend_files` helper.
|
|
|
|
|
+
|
|
|
|
|
+### Implementation
|
|
|
|
|
+
|
|
|
|
|
+- Create `tools/statum_meson.build` defining a helper that apps include:
|
|
|
|
|
+ ```meson
|
|
|
|
|
+ # tools/statum_meson.build
|
|
|
|
|
+ # Usage: include this, then call statum_page(...)
|
|
|
|
|
+
|
|
|
|
|
+ statum_mkpstm_bin = statum_mkpstm
|
|
|
|
|
+
|
|
|
|
|
+ # Can't define functions in meson, so provide a single helper target:
|
|
|
|
|
+ # statum_page('home', 'HomePage', 'MyApp', files('main.html'))
|
|
|
|
|
+ ```
|
|
|
|
|
+ Given meson limitations, the realistic deliverable is a documented
|
|
|
|
|
+ one-liner pattern + a `depend_files` convention. The tools already accept
|
|
|
|
|
+ `-n` (class name) and `--ns` (namespace), so the command is already concise.
|
|
|
|
|
+
|
|
|
|
|
+### Server / Vala API changes
|
|
|
|
|
+
|
|
|
|
|
+#### New directive DTOs (`src/Model/DirectiveDto.vala`)
|
|
|
|
|
+
|
|
|
|
|
+Two new DTOs, mirroring the existing pattern (`discriminator`, `build_field_properties`,
|
|
|
|
|
+`get_mapper`, `from_json` dispatch):
|
|
|
|
|
+
|
|
|
|
|
+```vala
|
|
|
|
|
+/** Submits a form by ID (optionally overriding its action URI and method). Terminal. */
|
|
|
|
|
+public class FormSubmitDirectiveDto : DirectiveDto {
|
|
|
|
|
+ public string form { get; set; }
|
|
|
|
|
+ public string? action { get; set; } // optional URI override
|
|
|
|
|
+ public string? method { get; set; } // optional "POST" or "GET" override
|
|
|
|
|
+
|
|
|
|
|
+ public override string discriminator { get { return "form-submit"; } }
|
|
|
|
|
+ protected override Properties build_field_properties() throws GLib.Error { ... }
|
|
|
|
|
+ public FormSubmitDirectiveDto() { }
|
|
|
|
|
+ public FormSubmitDirectiveDto.with_form(string form, string? action = null, string? method = null) { ... }
|
|
|
|
|
+ public static PropertyMapper<FormSubmitDirectiveDto> get_mapper() { ... }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** Resets a form by ID. Terminal. */
|
|
|
|
|
+public class FormResetDirectiveDto : DirectiveDto {
|
|
|
|
|
+ public string form { get; set; }
|
|
|
|
|
+
|
|
|
|
|
+ public override string discriminator { get { return "form-reset"; } }
|
|
|
|
|
+ protected override Properties build_field_properties() throws GLib.Error { ... }
|
|
|
|
|
+ public FormResetDirectiveDto() { }
|
|
|
|
|
+ public FormResetDirectiveDto.with_form(string form) { ... }
|
|
|
|
|
+ public static PropertyMapper<FormResetDirectiveDto> get_mapper() { ... }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Register in `from_json`:
|
|
|
|
|
+```vala
|
|
|
|
|
+case "form-submit": return FormSubmitDirectiveDto.get_mapper().materialise(json);
|
|
|
|
|
+case "form-reset": return FormResetDirectiveDto.get_mapper().materialise(json);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### DirectiveBuilder methods (`src/DirectiveBuilder.vala`)
|
|
|
|
|
+
|
|
|
|
|
+```vala
|
|
|
|
|
+/** Emits a terminal `form-submit` directive (optionally overriding action + method). */
|
|
|
|
|
+public DirectiveBuilder form_submit(string form_id, string? action = null, string? method = null) {
|
|
|
|
|
+ items.add(new Item() { kind = ItemKind.DIRECTIVE,
|
|
|
|
|
+ directive = new Model.FormSubmitDirectiveDto.with_form(form_id, action, method) });
|
|
|
|
|
+ return this;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** Emits a terminal `form-reset` directive. */
|
|
|
|
|
+public DirectiveBuilder form_reset(string form_id) {
|
|
|
|
|
+ items.add(new Item() { kind = ItemKind.DIRECTIVE,
|
|
|
|
|
+ directive = new Model.FormResetDirectiveDto.with_form(form_id) });
|
|
|
|
|
+ return this;
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### Stale-slot auto-set (`src/HeldSlotResolver.vala`, `src/StatumDirectives.vala`, endpoints)
|
|
|
|
|
+
|
|
|
|
|
+**`ResolvedSlots`** — add:
|
|
|
|
|
+```vala
|
|
|
|
|
+public Series<string> stale_keys { get; private set; default = new Series<string>(); }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**`HeldSlotResolver.resolve_reference`** — after `get_slot(key)` succeeds, compare
|
|
|
|
|
+the client's `as_at` with `slot.current_state.last_touched`. If the client's
|
|
|
|
|
+`as_at` predates the slot's last update, add the key to `stale_keys`:
|
|
|
|
|
+```vala
|
|
|
|
|
+if (as_at != null && slot.current_state.last_touched != null
|
|
|
|
|
+ && ((!)as_at).compare(((!)slot.current_state).last_touched) < 0) {
|
|
|
|
|
+ result.stale_keys.add(slot.id);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**`StatumDirectives.to_result`** — new overload (or extended signature) that
|
|
|
|
|
+accepts a `StateService` + `stale_keys` and appends fresh `set` directives:
|
|
|
|
|
+```vala
|
|
|
|
|
+public static HttpResult to_result(
|
|
|
|
|
+ Lot<Model.DirectiveDto> directives,
|
|
|
|
|
+ Lot<string>? invalid_signatures = null,
|
|
|
|
|
+ StateService? state_service = null,
|
|
|
|
|
+ Lot<string>? stale_keys = null) throws GLib.Error {
|
|
|
|
|
+ // ... existing invalid_signatures handling ...
|
|
|
|
|
+
|
|
|
|
|
+ // Auto-refresh stale slots: sign + append set directives.
|
|
|
|
|
+ if (state_service != null && stale_keys != null) {
|
|
|
|
|
+ foreach (var key in stale_keys) {
|
|
|
|
|
+ var frame = state_service.sign_slot(key);
|
|
|
|
|
+ if (frame != null) {
|
|
|
|
|
+ rendered.add(new Model.SetDirectiveDto.with_frame((!)frame));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // ... render JSON ...
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Entrypoint + Action endpoints** — pass the extra args:
|
|
|
|
|
+```vala
|
|
|
|
|
+return StatumDirectives.to_result(directives,
|
|
|
|
|
+ resolved.invalid_signatures,
|
|
|
|
|
+ state_service,
|
|
|
|
|
+ resolved.stale_keys);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+This means `StatumDirectives` needs access to `StateService` at the call site
|
|
|
|
|
+(passed as a parameter, not injected — keeps `StatumDirectives` stateless).
|
|
|
|
|
+The endpoints already inject `StateService` (entrypoint via the handler, action
|
|
|
|
|
+via the base class).
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## G. Documentation updates — `model.md`, `getting-started.md`
|
|
|
|
|
+
|
|
|
|
|
+Update:
|
|
|
|
|
+
|
|
|
|
|
+- **`model.md`**:
|
|
|
|
|
+ - HTML API: add `stm-markdown`, `stm-h-demotion` attribute descriptions.
|
|
|
|
|
+ - Directives: add `form-submit`, `form-reset` to the directive list with
|
|
|
|
|
+ descriptions.
|
|
|
|
|
+ - Note the automatic stale-set behaviour in the Snapshot / Request Headers
|
|
|
|
|
+ sections.
|
|
|
|
|
+- **`getting-started.md`**:
|
|
|
|
|
+ - JS API Reference: add `stm-markdown`, `stm-h-demotion` to the attribute
|
|
|
|
|
+ table; add `statum.beforeEvaluate` / `statum.afterEvaluate` (no-arg
|
|
|
|
|
+ callbacks) to the public API; add `form-submit` / `form-reset` to the
|
|
|
|
|
+ directive builder table.
|
|
|
|
|
+ - HTML Templates: mention the meson page pattern.
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Tasks
|
|
|
|
|
+
|
|
|
|
|
+### Client (js/statum.js)
|
|
|
|
|
+- **JS-1** `renderMarkdown(raw)` — escape-first minimal markdown parser.
|
|
|
|
|
+- **JS-2** `stm-markdown` attribute in `handleTextHtml`.
|
|
|
|
|
+- **JS-3** `handleHDemotion(el)` — `stm-h-demotion` (runs after markdown/html).
|
|
|
|
|
+- **JS-4** `form-submit` / `form-reset` directive handling (`applyDirectives`
|
|
|
|
|
+ + `executeTerminal` + `doFormSubmit` / `doFormReset`).
|
|
|
|
|
+- **JS-5** `beforeEvaluate` / `afterEvaluate` hooks + public API.
|
|
|
|
|
+
|
|
|
|
|
+### Server (Vala)
|
|
|
|
|
+- **V-1** `FormSubmitDirectiveDto` / `FormResetDirectiveDto` in `DirectiveDto.vala`
|
|
|
|
|
+ — two new DTO classes: `FormSubmitDirectiveDto` (form + optional action +
|
|
|
|
|
+ optional method), `FormResetDirectiveDto` (form), each with discriminator,
|
|
|
|
|
+ `build_field_properties`, `get_mapper`, `with_form` constructor, and
|
|
|
|
|
+ `from_json` dispatch entries.
|
|
|
|
|
+- **V-2** `DirectiveBuilder.form_submit(form_id, action?, method?)` /
|
|
|
|
|
+ `.form_reset(form_id)` in `DirectiveBuilder.vala` — fluent terminal directive
|
|
|
|
|
+ emitters.
|
|
|
|
|
+- **V-3** `ResolvedSlots.stale_keys` + `HeldSlotResolver.resolve_reference`
|
|
|
|
|
+ staleness detection (compare client `as_at` vs `last_touched`; add key to
|
|
|
|
|
+ `stale_keys` when stale; slot still resolves into `held`).
|
|
|
|
|
+- **V-4** `StatumDirectives.to_result` extended signature (optional
|
|
|
|
|
+ `StateService` + `stale_keys`) — auto-signs stale keys and appends `set`
|
|
|
|
|
+ directives; `EntrypointEndpoint` and `StatumAction.handle_request` pass
|
|
|
|
|
+ `state_service` + `resolved.stale_keys`.
|
|
|
|
|
+
|
|
|
|
|
+### Build
|
|
|
|
|
+- **M-1** Document / provide the `statum_page` meson pattern.
|
|
|
|
|
+
|
|
|
|
|
+### Docs
|
|
|
|
|
+- **D-1** Update `model.md` (attributes, directives, stale-set).
|
|
|
|
|
+- **D-2** Update `getting-started.md` (attributes, hooks, form directives).
|
|
|
|
|
+
|
|
|
|
|
+### Testing
|
|
|
|
|
+- **T-1** `stm-markdown` renders safe HTML (escaped source + formatted output).
|
|
|
|
|
+- **T-2** `stm-h-demotion` demotes headers after markdown/html.
|
|
|
|
|
+- **T-3** `form-submit` / `form-reset` directives manipulate the form.
|
|
|
|
|
+- **T-4** Stale-slot auto-set: a request with an old `as_at` receives a `set`.
|