-
New VML subsystem parallel to HTML (no inheritance from HTML classes):
-
src/browser/vml/
parser/
(tokenizer + treebuilder)dom/
(VMLElement, VML*Element subclasses)bridge/
(mutation emitter + event ingress)registry/
(tag → class key)
-
-
Keep the existing DOM event system; we simply initiate dispatch when events arrive from the renderer (no re-implementation).
- When Lightpanda fetches a document, route to VML iff the response header is
Content-Type: application/swiftui+vml
(accepting optional parameters liketarget=visionos
). The request Lightpanda sends should include the Accept header provided by the client at instance creation (e.g.,Accept: application/swiftui+vml; target=visionos
). The doctype is annotation only; do not use it to switch parsers. (Gist)
Deliverables
- Parser selector wired to
Content-Type
. - Instance configuration to set the Accept header before network starts (since Lightpanda owns networking). (Gist)
Key differences vs. HTML
- Case-sensitive tag names and case-sensitive attribute names (no lowercasing).
- Explicit booleans only; no implicit boolean attributes. (Gist)
Structure
- Required envelope:
<vml><head>…</head><body>…</body></vml>
;<body>
contains the primary app view hierarchy. Lifecycle templates live in<head>
. The doctype (<!doctype swiftui>
) is present but ignored by parsers. (Gist)
View registry assumption
- Tag names map 1:1 to registered native views; exact case preserved. Parser must not normalize or guess. (Gist)
Attributes & deserialization
- All attribute values are strings on the wire. Type-aware deserialization happens in the client, not in Lightpanda. Keep strings as-is. (Gist)
Encoding
- Use standard HTML character entity encoding rules for special characters. Only arrays/lists may be JSON-encoded inside attribute values; otherwise treat as plain strings. (Gist)
id
vs style
- Only SwiftUI’s
.id()
becomes a root-levelid
attribute. - All other modifiers collapse into a single semicolon-delimited, order-preserving
style
attribute (e.g.,style="padding(); foregroundStyle(.green)"
). Preserve leading dots in member expressions (e.g.,.green
). Parser must split on top-level semicolons only (parentheses may nest). (Gist)
Templates
- Lifecycle templates: declared in
<head>
viatemplate="disconnected" | "reconnecting" | "error"
. Triggering is client-side; the parser only records them. (Gist) - View-builder closures inside modifiers become named templates referenced from
style
(e.g.,content: :starOverlay
) with the actual subtree usingtemplate="starOverlay"
. Template names are unique only among siblings, and templates must be direct children of their referencing element. Parser records these relationships. (Gist)
Validation
- Proper nesting & closing; preserve whitespace in text nodes; accept self-closing tags. Enforce the validation list in §8 (e.g.,
attr(:name)
must refer to an existing attribute on the same element). (Gist)
Deliverables
- New VML tokenizer (no HTML insertion modes).
- New VML tree builder (strict nesting).
style
splitter with a small state machine (top-level;
only).- Errors with line/column and a short code (e.g.,
VML_E_BAD_TEMPLATE_SCOPE
). - Conformance tests that mirror §2–§9 examples verbatim. (Gist)
- Base:
Node
/Element
(reuse Lightpanda’s neutral core if it exists). - New branch:
VMLElement
+ leaf classes likeVMLTextElement
,VMLButtonElement
,VMLToggleElement
,VMLFormElement
, etc. - No inheritance from HTML element classes, but expose the same generic DOM surface (node traversal, attributes, events) so the rest of Lightpanda utilities keep working.
- Attribute reflection: generic
getAttribute/setAttribute
updates the element state; no HTML boolean reflection logic.
Deliverables
- Types, constructors, attribute handling, unit tests.
-
Provide a startup API for the client to register mappings:
- Example:
"Button" → "VMLButtonElement"
.
- Example:
-
Parser uses this registry to choose the right subclass at element creation time; unknown tags fall back to
VMLElement
(still functional).
Deliverables
- Registry data structure (immutable after document start).
- Fast lookup by case-sensitive tag string.
- Error reporting for duplicate registrations.
- Assign each node an internal opaque
node_id
(64-bit). Never reuse during the life of a document. - Maintain a hashmap:
node_id → Node*
for O(1) lookup (meets your “don’t query the DOM” requirement). - Keep
node_id
distinct from the author’sid
attribute (which can change).
Deliverables
- ID allocator (monotonic, wrap-safe).
- Hashmap with slab-allocated entries; clear on navigation.
You already send DOM to a built-in renderer; here we add a bridge to also stream mutations to an external renderer.
-
Hook Lightpanda’s existing DOM mutation points (create/insert/remove/move/setAttr/setText) and enqueue delta records:
CreateNode {node_id, tag, attrs, parent_id, index}
SetAttr/RemoveAttr
SetText
MoveNode
RemoveNode
DefineTemplate {owner_id, name, root_id}
-
Batch & flush (coalesce) per “tick” to reduce chatter.
-
The payload format can be NDJSON for v1 (debug-friendly); we can swap to MessagePack later without changing the logical schema.
Deliverables
- Mutation queue + back-pressure (bounded size with drop policy or blocking).
- Subscription API so the host can register a callback to receive batches.
- Sample “mirror tree” consumer used by tests.
-
Add a single API to initiate dispatch using the existing DOM event system:
dispatch_event(node_id, event_type, payload_json_or_null)
-
We’ll synthesize the correct Event object (e.g.,
InputEvent
,MouseEvent
, genericEvent
), attach payload, and run your existing capture → target → bubble pipeline. (You stated we use Lightpanda’s system and just initiate from the rendering client—perfect.) -
DOM mutations produced by handlers are naturally picked up by the mutation bridge and streamed back.
Deliverables
- Event adapter layer (node lookup + event construction).
- Minimal standard events (
tap
,input/change
,submit
,focus/blur
), plus custom events (string-named).
Because Zig compiles with LLVM, we’ll expose a C ABI and package as a static library (per-arch) and optionally combine into an XCFramework for host apps. Swift calls plain C functions; there’s no Swift-specific logic in Lightpanda. (No JIT anywhere; stays App-Store-safe.)
What we expose (minimal):
- instance lifecycle (
lp_new
,lp_free
) - networking kickoff with Accept header prepared by the host (Lightpanda does all fetches)
- element registration (tag → class)
- subscribe to mutation batches (host provides callback)
- dispatch event to
node_id
- memory helpers for any buffers we allocate/return
(You don’t need me to wire a SwiftUI app; but the ABI must be small, stable, and thread-safe. The host can be anything.)
- Arena per document + string interning for tag/attribute names.
- Stable ordering: keep
style
tokens in author order (significant per spec). (Gist) - Whitespace preservation in text nodes (do not HTML-collapse). (Gist)
- Validation errors include line/column and the exact violated rule (§8). (Gist)
-
Parser conformance
- All examples in §§2–7 parse to the expected tree;
style
splitter respects nesting; explicit booleans; JSON in attributes only for arrays/lists; leading.
preserved. (Gist)
- All examples in §§2–7 parse to the expected tree;
-
Validation suite (§8)
- Each rule has positive/negative fixtures (e.g., duplicate template names among siblings fail; templates not direct children fail;
attr(:foo)
missingfoo
fails). (Gist)
- Each rule has positive/negative fixtures (e.g., duplicate template names among siblings fail; templates not direct children fail;
-
Mutation round-trip
- Load a sample VML doc; assert the external consumer can reconstruct an isomorphic tree purely from deltas.
-
Event loopback
- Fire
tap
/change
from the host intonode_id
→ handler runs → DOM mutation observed in next batch.
- Fire
-
Perf & stability
- Stress with 50–100k nodes; measure allocations, batch sizes, and event throughput.
-
Neutral DOM core: Does Lightpanda already have a neutral
Node/Element
layer that HTML builds on? If not, are you okay with a short refactor to introduce it so VML doesn’t duplicate common DOM bits? -
Where to hang the bridge hooks: Do you prefer we instrument mutations at the DOM layer (Node/Element ops) or at the document/renderer boundary?
-
Event payloads: Any existing Lightpanda event payload schemas we should mirror (e.g., pointer positions, key codes), or should we accept opaque JSON and let handlers parse as needed?
-
Registry lifetimes: Should tag→class registration be immutable post-document-load, or do you need hot-reload for dev?
-
Error handling policy: For unknown tags or bad templates, do we fail the parse or insert a placeholder
VMLElement
and keep going (logging an error)?
Give me answers to those, and I’ll produce:
- the exact C header (types, error codes),
- the mutation/event schema (v1 NDJSON),
- and a concrete implementation checklist PR plan broken into tasks that map to files in
src/browser/vml/*
.
- Headers & targets; doctype; envelope; head/body requirements; lifecycle templates; client-side registry; attribute-to-initializer mapping; explicit booleans; encoding;
id
vsstyle
; modifier order significance; view-builder closures → templates;attr
helper; stylesheet references and client-defined VSS; validation rules;.vml
+ UTF-8 + whitespace preserved. (Gist)
If anything above doesn’t match your intent, point at the exact section of the gist and I’ll adjust the parser rules accordingly.