Skip to content

Instantly share code, notes, and snippets.

@bcardarella
Created August 28, 2025 21:20
Show Gist options
  • Save bcardarella/bde9407f8fd8b7d6b2a8b0e746071bdd to your computer and use it in GitHub Desktop.
Save bcardarella/bde9407f8fd8b7d6b2a8b0e746071bdd to your computer and use it in GitHub Desktop.

What changes inside Lightpanda (revised scope)

0) Repository layout & boundaries

  • 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).


1) Content negotiation & parser selection

  • When Lightpanda fetches a document, route to VML iff the response header is Content-Type: application/swiftui+vml (accepting optional parameters like target=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)

2) VML parser (strict, XML-like) — exact rules from your spec

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-level id 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> via template="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 using template="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)

3) VML DOM class family (parallel to HTML)

  • Base: Node / Element (reuse Lightpanda’s neutral core if it exists).
  • New branch: VMLElement + leaf classes like VMLTextElement, 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.

4) Renderer element registration (tag → class key)

  • Provide a startup API for the client to register mappings:

    • Example: "Button" → "VMLButtonElement".
  • 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.

5) Node identity & lookup (fast path; no DOM scans)

  • 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’s id attribute (which can change).

Deliverables

  • ID allocator (monotonic, wrap-safe).
  • Hashmap with slab-allocated entries; clear on navigation.

6) Mutation streaming (Lightpanda → renderer)

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.

7) Event ingress (renderer → Lightpanda)

  • 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, generic Event), 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).

8) iOS/Swift interop (C ABI only; no JIT; Lightpanda keeps networking)

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.)


9) Performance & safety notes

  • 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)

Test plan (essentials)

  1. 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)
  2. Validation suite (§8)

    • Each rule has positive/negative fixtures (e.g., duplicate template names among siblings fail; templates not direct children fail; attr(:foo) missing foo fails). (Gist)
  3. Mutation round-trip

    • Load a sample VML doc; assert the external consumer can reconstruct an isomorphic tree purely from deltas.
  4. Event loopback

    • Fire tap/change from the host into node_id → handler runs → DOM mutation observed in next batch.
  5. Perf & stability

    • Stress with 50–100k nodes; measure allocations, batch sizes, and event throughput.

Open points I still need from you (short & surgical)

  1. 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?

  2. 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?

  3. 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?

  4. Registry lifetimes: Should tag→class registration be immutable post-document-load, or do you need hot-reload for dev?

  5. 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/*.

Appendix: spec excerpts I implemented against

  • Headers & targets; doctype; envelope; head/body requirements; lifecycle templates; client-side registry; attribute-to-initializer mapping; explicit booleans; encoding; id vs style; 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment