# pathland-render-html (Rust) — implementation status
**Last updated:** October 2, 2026
The **server-side / remote-projection HTML renderer**: a **stateless, streaming**
pure function of the opcode stream producing declarative HTML. Each render call
decodes a self-contained snapshot batch into a **transient** map, walks it once,
and emits HTML text out — no retained tree, zero cross-call state (Renderer
Statelessness). Protocol contract: `spec/`.
## Implemented
- **Stateless streaming API**: `HtmlRenderer::render_document(opcodes, strings, root)`
/ `render_fragment(...)` build a transient decode map per call and stream HTML;
the retained `apply`/`apply_frame`/`render` model is removed.
- **All spec components render**: `TEXT`, `IMAGE`, `COLOR` (layout-greedy:
`flex:1;align-self:stretch`), `SHAPE` (CSS/SVG by `SHAPE_KIND`), `DIVIDER`, `SPACER` (inline
`flex:1`), `PROGRESS_VIEW`/`GAUGE` (`data-min`/`data-max` ride on the gauge so the DOM
client can recompute its percentage), `VSTACK`/
`HSTACK`/`LAZY_VSTACK`/`LAZY_HSTACK` (flex), `ZSTACK` (overlay), `GRID`/
`LAZY_VGRID`/`LAZY_HGRID` (CSS grid), `SCROLLVIEW`, `BUTTON`, `TEXT_FIELD`
(incl. `IS_SECURE` → `type="password"`), `TEXT_EDITOR`, `TOGGLE`
(`TOGGLE_STYLE`), `SLIDER`, `STEPPER` (the `.pathland-stepper` composite with
`data-min`/`data-max`/`data-step` on its hidden range span), `DATE_PICKER` (`SET_DATE` →
`days_to_date`), `PICKER` (option children rendered as `<option data-pathland-id="{child}"
value="{index}">` + `SELECTION`), `MENU` (the `.pathland-menu` composite:
`.pathland-menu-trigger` label + `.pathland-menu-items` children),
`COLOR_PICKER`, `COMMENT`.
- **Native elements** (the specs carry no renderer mappings — this is the
concrete HTML map the removed spec hints lived in): `TEXT` → `<span>` (or
`<p>`/`<h1>`–`<h5>` by `ROLE`/heading `TEXT_STYLE`), `IMAGE` → `<img>`,
`ICON` → inline **Remix (filled)** `<svg class="pathland-icon">` (the canonical
`ICON_NAME` resolves to its filled SVG markup via this renderer's own
`icons` module (`crate::icons`, the web owner of the mapping); the svg carries
`data-pathland-icon="<name>"` — the DOM client's lazy resolver reads it as an
embedded glyph for instant on-page swaps; `LABEL` →
`role="img"` + `aria-label`, none → `aria-hidden`; sized `1em` so `FONT_SIZE`
scales it, tinted via `fill="currentColor"`),
`SIZE_THAT_FITS` → `<div class="pathland-ftf">` (the fit slot: fill-sized with
`data-pathland-fit` mirroring the `FIT_QUERY` LIST, decoded as `[count][f32×count]`
at decode time — the DOM client derives the fit locally and reports
`FIT_CHANGED` on transitions; the slot is layout-transparent, forwarding the
parent flex container's `flex-direction`/`align-items`/`justify-content`
(`inherit`) into its own box so a wrapper never pins a centered candidate to
the start — `VStack(CENTER)` > fit slot > `HSTACK` keeps centering; stays
size-taking via `flex:1 1 auto;align-self:stretch;min-width/height:0`),
and the **web-SSR snapshot form** (issue #141: the slot carries EVERY candidate,
in `FIT_QUERY` order — the protocol's candidate-identity contract) renders each
branch in a `data-pathland-fit-cand="i" data-pathland-fit-cand-id="<child>"`
gate wrapper whose inline `display` is the pre-CSS default (candidate 0
`contents`, every other candidate `none`), marks the slot as the query context
(`container-type:inline-size; container-name:pf-<id>`), and emits the
`@container` **band rules** into a `<style data-pathland-fit>` head element: for
each distinct threshold `T_i` (`i ≥ 1`, ascending) a self-contained
`@container pf-<id> (min-width: T_i)` band that **hides every candidate `j ≠ i`
and shows candidate `i`**, so the highest matching band wins and **exactly one**
candidate is visible at any width (no band for `T_0`; below the first band the
inline default shows candidate 0). A no-JS first paint therefore already shows
the candidate matching the slot's width; the single-child live form is
unchanged. The bands are **SSR-only** — the first `FIT_CHANGED` collapses the
slot to the live single-child form and the DOM client removes the gating
(`collapseFitSlotFor` unwraps the gate wrappers, strips the container/candidate
attrs, and `dropFitBandRules` deletes the slot's rules)),
`COLOR` → `<div>`, `SHAPE` → CSS shapes / inline SVG, `DIVIDER` → `<hr>`,
`SPACER` → inline flex filler, `PROGRESS_VIEW` → `<progress>` /
`.pathland-spinner`, `GAUGE` → `.pathland-gauge`, stacks/lazy stacks → flex
`div`, `ZSTACK` → grid overlay `div`, grids → CSS-grid `div`, `SCROLLVIEW` →
overflow `div`, `BUTTON` → `<button>`, `TEXT_FIELD` → `<input>` (secure →
`type="password"`), `TEXT_EDITOR` → `<textarea>`, `TOGGLE` → checkbox/switch
input, `SLIDER` → `<input type="range">`, `STEPPER` → `.pathland-stepper`,
`DATE_PICKER` → `<input type="date">`/`<input type="time">`, `PICKER` →
`<select>` + `<option>`, `MENU` → `.pathland-menu` composite, `COLOR_PICKER`
→ `<input type="color">`, `AUDIO`/`VIDEO` → `<audio controls>` /
`<video controls>` (hidden media element + custom body when app-driven),
`COMMENT` → none.
- **Media**: `IMAGE` renders `<img src alt>` (the `LABEL` accessibility text is
the `alt`; empty = decorative) and honors `CONTENT_MODE`/`ASPECT_RATIO`
inline (`Fit`→`object-fit:contain`, `Fill`→`object-fit:cover`); `VIDEO` →
`<video src controls>` and `AUDIO` → `<audio src controls>` (playback
interaction is renderer-native by default). **App-driven media**: an
`AUDIO`/`VIDEO` node whose custom `AudioStyle`/`VideoStyle` body is its child
(spec DSL.md §5.7) — or any node carrying `AUDIO_SOURCE`/`VIDEO_SOURCE` with
children — renders a `.pathland-media` wrapper holding a hidden control-less
media element as its first child (the DOM client wires playback events,
spec/EVENTS.md Media). Asset refs are absolute (`/_pathland/assets/…`);
bytes never ride the opcode stream.
- **Composite override mode**: `BUTTON`/`TOGGLE`/`SLIDER` with children render
the custom body wrapped in the native element.
- **Properties**: `ALIGNMENT` (cross-axis **position** — the default is hug
(`flex-start`), never CSS `align-items:stretch`, per `spec/LAYOUT.md`; only
`FILL`-sized children stretch via their `100%` size), `CONTENT_MARGINS`,
`WIDTH`/`HEIGHT` (`FILL` → `width:100%`/`height:100%` expansion, `HUG` →
intrinsic), `PADDING` + per-edge, `COLOR`,
`BACKGROUND_COLOR`, `FONT_SIZE`/`WEIGHT`/`FAMILY`, `TEXT_STYLE` (the
renderer-owned default size/weight; explicit `FONT_SIZE`/`WEIGHT` override),
`OPACITY`, `VISIBLE`,
`Z_INDEX`, `LINE_LIMIT` (positive → `-webkit-box`/`-webkit-line-clamp` **with
`overflow:hidden`** — the clamp draws the ellipsis but does not clip on its own,
so Safari would otherwise paint the overflowed text below the box, matching
the DOM client), `TEXT_ALIGNMENT`, `TRUNCATION_MODE`, `BORDER_*`,
`SELECTED`, `TOGGLE_STYLE`, `ENABLED`, `ROLE`/`STATE` (ARIA), plus
`TEXT_CASE`, `FONT_STYLE`, `FONT_DESIGN`, `UNDERLINE`/`STRIKETHROUGH`,
`CLIPS_TO_BOUNDS`, `ALLOWS_HIT_TESTING`, `COLOR_INVERT`. `WIDTH`/`HEIGHT`
`HUG_CONTENT` (-2) is **omitted** (intrinsic size); `FILL` (-1) → `100%`.
- **`Z_INDEX` (draw-order override)**: a `ZSTACK` child's `Z_INDEX` is emitted on
its **shell** (the grid item that participates in stacking — the child's own
`z-index` is inert inside the shell), so a higher value draws above lower
siblings regardless of child index (spec/PRIMITIVES.md §ZStack). **Every shell
carries an explicit `z-index`** (the child's `Z_INDEX`, else `0`) and the ZStack
container sets `isolation:isolate`, so overlapping grid items paint
deterministically in every browser (WebKit paints a non-fill grid item below a
full-area sibling when both are `z-index:auto`). Mirrored by the DOM client's
`applyLayout`.
- **Layout contract (spec/LAYOUT.md)**: cross-axis default is **hug**
(`flex-start`), not CSS stretch; `ALIGNMENT` positions only (`Fill`=3 → hug
default). `DIVIDER` is greedy on the cross axis (`width:100%`); `SCROLLVIEW`
is greedy on both axes (`flex:1 1 auto;align-self:stretch`); fixed frames emit
exact px boxes. Conformance cases C1–C6 asserted in `lib.rs` tests.
- **Fill propagation (spec/PRIMITIVES.md §stack layout model)**: implemented —
`fills_axis` resolves a Hug stack/ZStack containing a `FILL` child / greedy
primitive on an axis as `FILL`, emitting `width/height:100%` on that axis, and
cross-axis `FILL` children stretch via `align-self:stretch` (SSR + TS DOM
client, kept canonical via the layout pass in `applyLayout`).
- **ZStack (spec/PRIMITIVES.md)**: implemented — an overlapping grid
(`grid-area:1/1` cells); the container hugs to its largest child (`max-content`)
unless Fixed/FILL (or fill propagation → `100%`); each child keeps its own
size and is positioned by `ALIGNMENT` on both axes (SSR + TS DOM client).
**Grid tracks are `minmax(0,1fr)`** (ZStack + count-generated grid tracks,
`repeat(n,minmax(0,1fr))`, and lazy-hgrid auto columns), so a FILL ZStack/grid
can shrink below its content min-height on a short viewport — an `end`-aligned
child (e.g. a player bar) stays pinned instead of being pushed out of view and
the inner `ScrollView` scrolls. A `GRID_TRACKS` per-track spec is emitted
verbatim (app-authored). **Child shells carry no explicit
`width/height`** — `justify-self/align-self:stretch` only applies when a grid
item's size is `auto`, so pinning `max-content` silently degraded `stretch` to
`start` and a FILL child's `100%` resolved circularly (content height),
overflowing the viewport. With `auto` sizes a FILL child genuinely stretches
to its (shrinkable) track and a Hug child keeps content size + alignment.
- **ARIA ROLE/STATE maps match the DOM client**: the full `ROLE` set (button…
menu, incl. `text`/`img`/`radio`/`spinbutton`/`tablist`/`list`/`grid`/
`region`/`menu`) and the `STATE` semantics (one true `aria-*` per state) are
canonical with `lib/typescript/src/classes.ts`.
- **Semantic HTML elements from `ROLE` + typography** (spec/OPCODE.md §semantic
properties): a semantic role retags a **generic `div`/`span` shell** to its
native element — `HEADER`→`<h2>`, `PARAGRAPH`→`<p>`, `LIST`→`<ul>`,
`LIST_ITEM`→`<li>`, `SUMMARY`→`<section>`, and the landmarks `BANNER`→
`<header>`, `NAVIGATION`→`<nav>`, `MAIN`→`<main>`, `CONTENT_INFO`→`<footer>`,
`COMPLEMENTARY`→`<aside>`, `ARTICLE`→`<article>`, `SECTION`→`<section>`,
`SEARCH`→`<search>`. **Control components keep their native element** and
emit no ARIA role from `ROLE` (`BUTTON` stays a `<button>`, `MENU` renders
its intrinsic `role="menu"`). **Headings** come from a heading `TEXT_STYLE`
typography (LargeTitle…Headline → `<h1>`–`<h5>`, always a heading) or from
`ROLE_HEADER` (default `<h2>`); non-heading text defaults to `<span>`.
Interactive/control roles are NOT in the catalog (intrinsic to the
components); a custom button uses `Button`+`ButtonStyle`. The mapping lives
in **`src/role_spec.rs`** (canonical with the DOM client —
`pathland-ts-codegen` emits `generated/role-spec.ts`), with the role codes
centralized in `pathland_core::constants::role`.
- **`TREE::INSERT_CHILD` honors its `C` index** (`u32::MAX` = append), matching
the protocol and the DOM client's `insertAt`.
- **Event surfacing**: `data-event-listeners` / `data-action-id` /
`data-binding-id` attributes.
- **Navigation slot attrs** (spec DSL.md §4.5 / MODIFIERS.md): a slot node's
`ROUTE` (STRING) renders as `data-pathland-route="<path>"`, its
`TRANSITION` hint renders as `data-pathland-transition="<platform|fade|slide|scale>"`,
its `NAV_CHROME` (F32 enum) renders `data-pathland-nav-chrome="custom"`
when `Custom` (the renderer adds no default chrome), and `NAV_DEPTH` (U32)
renders `data-pathland-depth="N"` when deeper than its root so the DOM client
can hydrate its default back button from the SSR HTML — the DOM renderer
mirrors the route into the URL, may animate a swap, and draws the renderer's
own back button (the web has no native navigation container).
- `PARAMETER::SET_DATE` handled (days + millis-of-day → date/time).
- **Design tokens (spec/TOKENS.md)**:
- The naming/length conventions live in **`src/token_spec.rs`** — the single
source of truth consumed by the renderer AND by `pathland-ts-codegen`, which
emits the DOM client's `lib/typescript/src/generated/tokens-core.ts`
(`tokenToCssVar`, `isDarkToken`, `isLengthToken`, `resolveTokenCssRef`).
- `PARAMETER::SET_DESIGN_TOKEN` overrides are collected per snapshot batch and
emitted into the document head as CSS: base (light) tokens as `:root`
rules, `dark.*` overrides inside `@media (prefers-color-scheme: dark)` —
the browser resolves the scheme natively, so SSR needs no client scheme.
- `DESIGN_TOKEN`-typed `SET_PROPERTY` values resolve at render time to
`var(--pl-…)`, with the generative `space.<N>` family resolving to
`calc(var(--pl-space-base) * N)` (`Node::token_ref`).
- **`STRING`-valued token overrides** (e.g. `font.body.family`): a
`SET_DESIGN_TOKEN` with `valueType = STRING` resolves the value string from
the batch's string section and emits it single-quoted (escaped) as a
`:root` rule (or inside the dark media query).
- The override rules render inside their own **`<style data-pathland-tokens>`**
element in the document head, **after** the built-in block (so their
`:root` variables win the cascade and the browser applies them) — matching
the JS DOM client's `style[data-pathland-tokens]` element. No overrides →
no extra element.
- The built-in `:root` tokens use the **canonical spec paths** (`--pl-color-primary`,
`--pl-color-background`, `--pl-color-text-primary`, `--pl-color-text-secondary`,
`--pl-color-surface`, `--pl-color-border`) so app overrides retheme the
renderer.
- **Full Tier-1 default coverage**: `--pl-space-base` (light + dark, so the
generative `space.<N>` family always resolves), canonical typography
(`--pl-font-body-size/weight/family`), the radius scale + `--pl-border-width-thin`,
and the **shared control core mapped onto the canonical catalog** —
`--pl-control-*`, `--pl-button-*`, `--pl-input-*` (light + dark), with the
focus ring mapped to `control.accent` and shadows mapped onto the composite
`elevation.low.*` / `elevation.high.*` tokens. `.pathland-*` rules reference
the canonical variables, so `SET_DESIGN_TOKEN` overrides retheme components.
## Design decision (decision A, September 2026): inline-all + built-in design system
Tailwind is **removed**. The renderer emits **everything inline** as CSS in a
single `style` attribute — no external compiler, no class system, no safelist:
- **All opcode properties render inline** (`style_css()` + `border_style()`),
including the previously class-based enum surface (alignment, text alignment,
text case, visible, font style/design, underline/strikethrough, clips-to-bounds,
hit-testing, invert, truncation). **1 dp = 1 CSS px**.
- **`css::STYLE`** is a built-in `<style>` block injected by `render_document`,
containing:
- a **preflight reset** (vendored from Tailwind CSS, MIT — see
`THIRD_PARTY_NOTICES`),
- `:root` **design tokens** as CSS custom properties (`--pl-color-primary`,
`--pl-color-background` (document background), `--pl-color-surface`,
`--pl-color-text-primary`/`--pl-color-text-secondary`, `--pl-color-border`,
`--pl-radius-*`, `--pl-font-sans`, `--pl-input-*` (field bg/text/outline/
placeholder/focus), button tokens), named to match the canonical protocol
design-token paths so `SET_DESIGN_TOKEN` can retheme the renderer,
- `.pathland-button` (the prettified-button POC, with `prefers-color-scheme`
dark mode) and `.pathland-*` component defaults (toggle switch, slider, text
field/editor, stepper, spinner, gauge, menu) — the interactive/hover/focus/
disabled states that can't be expressed inline. Text fields use a
Tailwind-style **inset outline** (`outline: 1px solid`, `outline-offset: -1px`,
focus → `2px`/`-2px` + indigo) instead of a border, with dark-mode tokens;
`html` sets `color-scheme: light dark` + the `--pl-color-bg` background so
native form controls and the page follow the theme.
- **`pathland_html_*` cdylib** (`pathland_html_render` / `_render_fragment` /
`_free`, plus `pathland_html_icon_svg`) for cross-language hosts (Java JNA
shim). `pathland_html_icon_svg(name)` returns the icon glyph's full `<svg>`
(canonical name → glyph, unknown/extension → the renderer's fallback glyph) —
how any host serves `/_pathland/icons/<name>.svg` from ONE Rust source (the
DOM client's lazy icon fetch) instead of per-platform static files. The
`_tailwind_compile`
entry point, `tw.rs`, `tailwind.rs`, `build.rs`, `scripts/fetch-tailwind.mjs`,
`vendor/`, and the `tailwind-embed` feature are **deleted**.
**The cdylib must be refreshed with `cargo build -p pathland-render-html`
(NOT just `cargo test` — tests rebuild the rlib, not the `.dylib`) before
`mvn install`**: the jar-embed step only fails on a *missing* dylib, a
*stale* one embeds silently and hosts SSR the old component surface (e.g. a
blank page for a newly-added component like `SIZE_THAT_FITS`).
- **Debug comments** (`HtmlRenderer::with_debug_comments(true)`; C ABI
`pathland_html_render_debug` / `_fragment_debug`): every rendered node is
prefixed with an HTML comment naming its component type and the modifiers
applied (`<!-- #1 VStack: spacing=4, alignment=Fill, width=FILL -->`) — values
decoded by protocol type (FILL/HUG sentinels, well-known enums, colors,
strings, bitmasks); opt-in so default output is unchanged, and comments stay
valid HTML (content scrubbed of `--`).
## Cross-renderer conformance (no drift)
The **`pathland-html-golden`** crate renders a battery of scenarios to committed
fixtures under `lib/typescript/test/fixtures/ssr/` (`{name}.plpl` batch bytes +
`{name}.html` canonical `render_fragment`). Its `tests/guard.rs` fails if the
SSR output changes without the fixtures being regenerated
(`cargo run -p pathland-html-golden -- --emit`), and the TypeScript
`test/ssr-conformance.test.ts` asserts the DOM client reproduces the same DOM
from the same batches (fresh-DOM + hydrate-then-delta). The SSR output is the
contract both renderers must satisfy.
## Not implemented / gaps
- **`TRUNCATION_MODE`** (spec/LAYOUT.md §content fitting): aligned — it **alone**
has no observable effect (the old `nowrap`+`text-overflow:ellipsis` emission is
gone, SSR + TS DOM client); under a `LINE_LIMIT` clamp the renderer tail-
ellipsizes via `line-clamp`. Renderer-owned fidelity: CSS cannot place a
head/middle ellipsis, so non-Tail modes fall back to the tail ellipsis.
- **`DESIGN_TOKEN` property references** cover the directly-mappable subset
(colors, font size/weight, spacing/padding, corner radius, opacity,
width/height, border width/color, shadow color). Compound accumulators that
need concrete numbers (shadow radius/x/y) remain literal-only.
- **Grid/ScrollView/Lazy layout (spec/PRIMITIVES.md §Grid / §ScrollView)**:
grids emit `gap` from `SPACING`, equal-`1fr` track templates from the
`GRID_COLUMNS`/`GRID_ROWS` constructor properties (columns for GRID/
LAZY_VGRID, rows for LAZY_HGRID + `grid-auto-flow:column`), **`GRID_TRACKS`**
per-track specs (`flex`/`fixed:<pts>`/`adaptive:<pts>` → `1fr`/`<pts>px`/
`minmax(<pts>px,1fr)`, taking precedence over the count), and **per-cell
alignment** — each cell is wrapped in an auto-placed shell whose
`justify-self`/`align-self` is `stretch` for a `FILL`/greedy cell, else the
grid `ALIGNMENT` position (default start) — with `justify-items`/
`align-items` on the container. **`GRID_ROW`** (0x1D) rows are flattened with
explicit `grid-row`/`grid-column` placement; each row renders as a transparent
`<div style="display:contents">` (its id hydrates the DOM client), and a
short row leaves trailing columns empty. `WIDTH`/`HEIGHT` on a grid are the
universal pixel box, never a count. `SCROLLVIEW` is greedy
(`flex:1 1 auto;align-self:stretch;overflow:auto`) and renders **only its
first child** (the spec-pinned single content). Conformance cases C11–C14
covered by the SSR tests + golden fixtures (incl. the `gridrow` scenario).
- `SHAPE` `Path` renders as an SVG placeholder (no path data wire property).
- GAUGE/SHAPE visuals are CSS approximations, not pixel-exact.
- **Typography uses the platform system font stack** (`system-ui,
-apple-system, 'Segoe UI', Roboto, …`) — no external/webfont loading, so the
rendered document makes no network requests for fonts and works offline.
An app can still pick its own typeface via a `font.body.family`
`SET_DESIGN_TOKEN` override (any STRING value, e.g. `"Inter"` if the app
hosts it).
## Verified by
`cargo test -p pathland-render-html` — 43 headless render tests (components,
properties, composite, event attrs, navigation slot route/transition attrs,
`days_to_date`, network-decoded frames, inline-all styling, built-in CSS block
contents, Inter CDN head links, design tokens: base/dark override CSS, `px`
lengths, `DESIGN_TOKEN` refs, generative `space.N`). `cargo test -p
pathland-html-golden` — the SSR golden-fixture guard.