# 01 — `.htmlx` format spec **Context sources:** [`./00-overview.md`](./00-overview.md) §§ "`.htmlx` with live subscriptions — target format" (L127–166), "Design decisions" (L28–37), [`.dev/reference/crates/wo-htmlx/`](../../../.dev/reference/crates/wo-htmlx/) (the v1 template engine that 90% of this phase ports), [`templates/article.htmlx`](../../../templates/article.htmlx) and [`templates/home.htmlx`](../../../templates/home.htmlx) (v1 concrete usage), [`docs/examples/ecommerce/shared/components/order-row.htmlx`](../../examples/ecommerce/shared/components/order-row.htmlx) (the live-binding workload this format must serve). ## Goal Lock the exact `.htmlx` grammar — every v1 Mustache construct unchanged plus two new constructs: a `…` subscription subtree and a `wo:bind="field"` field-level live attribute — and define the JSON manifest emitted in a `` and consumed only by phase 03's client runtime. Schema below; `version: 1` is a constant for this phase. ## Scope ### New files inside `crates/ui/src/htmlx/` | File | Responsibility | Port source | | --- | --- | --- | | `mod.rs` | Re-exports `Template`, `Manifest`, `LiveSubscription`, `BindSite`, `ParseError`, `RenderError` | [`.dev/reference/crates/wo-htmlx/src/lib.rs`](../../../.dev/reference/crates/wo-htmlx/src/lib.rs) (11 LOC) | | `ast.rs` | Adds `Node::Live { attrs, body }` and `wo_bind: Option` on element nodes | [`.dev/reference/crates/wo-htmlx/src/ast.rs`](../../../.dev/reference/crates/wo-htmlx/src/ast.rs) (18 LOC) — extend by ~50 LOC | | `parser.rs` | Adds `` body in `
` for the runtime | [`.dev/reference/crates/wo-htmlx/src/render.rs`](../../../.dev/reference/crates/wo-htmlx/src/render.rs) (140 LOC) — extend by ~70 LOC | | `manifest.rs` | Walks the AST, collects subscriptions + bind sites, serialises JSON | new (~150 LOC) | Total: ~835 LOC (585 ported + ~250 new). ### `Cargo.toml` change ```toml [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" ``` (Both already in `crates/rt`; `crates/ui` adopts them rather than hand-rolling JSON for the manifest at this phase.) ### Manifest schema ```json { "version": 1, "subscriptions": [ { "id": "orders-live-0", "source": "Order{ status != Cancelled }", "key": "id", "sort": "placed_at desc", "filter": null, "root_selector": "[data-wo-subscription=\"orders-live-0\"]" } ], "bind_sites": [ { "subscription_id": "orders-live-0", "key": "id", "field": "status" }, { "subscription_id": "orders-live-0", "key": "id", "field": "total" } ] } ``` ## API shape (target) ```rust use ui::htmlx::{Template, Manifest, HelperRegistry}; let tmpl = Template::parse(src)?; // Result let html = tmpl.render(&ctx, ®istry)?; // Result let mani = tmpl.manifest(); // Manifest // SSR pattern: page = head + html + "" ``` ## Exit criteria 1. `cargo build -p ui` green; no new top-level dependencies beyond `serde`/`serde_json`. 2. **Golden parse + render** for every `.htmlx` file under [`docs/examples/blog/ui/components/`](../../examples/blog/ui/components/) and [`docs/examples/ecommerce/shared/components/`](../../examples/ecommerce/shared/components/) — output is HTML and parses back into an isomorphic AST. 3. **Manifest emission** for `…` produces a `LiveSubscription` with the source string preserved verbatim and the body wrapped under `data-wo-subscription="orders-live-0"`. 4. **Bind-site collection** for `{{status}}` inside `` records `(subscription_id, key="id", field="status")` once and only once. 5. **v1 regression**: `cargo run --bin wo -- run docs/examples/blog` continues to start without parser errors. The blog sample has no `` or `wo:bind` today; nothing should regress. 6. All 14 existing `crates/rt` tests pass. ## Non-scope - **No SSR.** Phase 02 emits templates; phase 03 ships the runtime; serving them is a downstream concern that the per-app binary in phase 05 wires together. - **No nested ``.** Parse error in this phase. Author can compose live subtrees by partial inclusion (`{{> child}}`). - **No author-extensible helpers.** The closed enum is the contract for this phase. - **No streaming render.** Templates are rendered to a single `String`. - **No manifest version negotiation.** Wire format is `version: 1` always. ## Verification ```bash cargo build -p ui cargo test -p ui --test golden_v1 # blog/ecommerce templates byte-identical cargo test -p ui --test golden_extensions # + wo:bind cases cargo test -p ui --test manifest # manifest emission # v1 regression cargo run --bin wo -- run docs/examples/blog & PID=$!; sleep 1; curl -fsS http://127.0.0.1:8080/ >/dev/null; kill $PID cd .dev/reference/crates && cargo build && cargo test ``` ## After this phase Phase 02 (`02-ui-compiler.md`) consumes the `Template` + `Manifest` types defined here as its emission target — every `##ui` block compiles down to an `.htmlx` file that parses cleanly under this phase's parser. Phase 03 (`03-client-runtime.md`) consumes the manifest JSON schema as its wire input.