ps-blitz-dom-api 0.4.13

Runtime-agnostic DOM operations over blitz-dom
Documentation

blitz-dom-api

Runtime-agnostic DOM operations over blitz-dom.

blitz-script is currently the only path from a language runtime to the DOM, and its operations are entangled with Boa: JsValue in, JsResult out, a Context threaded through, prototype objects holding the registration. A second runtime cannot reuse any of it. This crate is those operations with the runtime removed, so that a binding does argument coercion and result construction and nothing else.

Nothing here depends on Boa, and nothing here may: tests/no_boa.rs resolves the real dependency graph and fails if a boa_* crate appears in it.

blitz-script is untouched. Reparenting it onto this crate is a separate change.

Using it

use blitz_dom_api::{document, element, node, style};

let panel = document::create_element(&mut doc, "div")?;
element::class_list_add(&mut doc, panel, &["panel"])?;
style::set_property(&mut doc, panel, "width", "200px")?;
node::append_child(&mut doc, body, panel)?;

Document first, then the node the operation is on (what a binding calls this), then the arguments in the order the DOM method declares them. Readers take &BaseDocument, mutators &mut BaseDocument, everything returns Result<T, DomError>.

Two rules worth reading before you use it

No return value keeps a document borrow alive. Readers that would naturally hand back &str return owned String. That is a deliberate allocation: a binding calls out to guest code between operations and guest code calls back in, and a borrow still live across that boundary panics inside RefCell at a call site with no relationship to the code that took it.

A binding that does not want the String does not have to pay for it. The buffer-writing readers — element::get_attribute_into, node::text_content_into and node::text_content_len — write into a slice the caller owns and return the value's full byte length, so nothing is allocated and nothing is truncated. They sit beside the owning readers rather than replacing them. blitz-wasm measured what the difference is worth; see MAPPING.md, "Readers allocate a String".

Mutations do not mark layout dirty and do not request a redraw. blitz-script routes every mutation through DomCtx::mutate_doc, which does both. Both are properties of the embedding, so both stay with the binding. A binding that forgets the flag reads geometry from before its own mutations; one that forgets the redraw makes a change that never reaches the screen.

The same applies to geometry::bounding_client_rect, which reads layout without flushing it. The caller flushes, then reads. See MAPPING.md.

What is not here

Events. Event objects, addEventListener / removeEventListener / dispatchEvent, the propagation path, and the on<event> IDL properties.

Selection. document.getSelection and everything reachable from it.

Pointer capture. setPointerCapture, releasePointerCapture, hasPointerCapture.

Focus. element.focus, element.blur, document.activeElement.

These are not omissions of convenience. Each needs a dispatch model — a listener registry keyed by a guest callable, a propagation walk that invokes guest code and observes what it did, a notion of object identity so that removing a listener can find the one that was added. None of that can be expressed over a borrowed document without inventing the runtime's object model first, and inventing it here would mean the second runtime inherits the first one's shape. They belong with the binding until there are two bindings to generalise from.

Also absent, for the same reason: wrapper caching and node identity, argument coercion, and the custom-element upgrade that must follow an insertion. MAPPING.md lists these under "needs a runtime decision".

Reading the code

  • document — node creation and lookup
  • node — tree structure, tree mutation, text content
  • element — attributes, classList, innerHTML, selector matching
  • character_data — data
  • style — the inline style attribute
  • geometry — getBoundingClientRect, the one operation that reads layout
  • atom — AtomId and Interner, for a guest that cannot cheaply pass strings. Nothing requires it yet; see the module docs for why the operations still take &str and where the ownership boundary is.

MAPPING.md has a row per operation: upstream file:line, the facade function, and whether the semantics are identical or deliberately differ.

Tests

cargo test -p blitz-dom-api

A unit test per operation, asserting the copied edge cases rather than only the happy paths; tests/tree_and_layout.rs, which builds a document through the facade alone and then asserts exact laid-out geometry; and tests/no_boa.rs.

The one test that is not here is parity against blitz-script, because a dev-dependency on blitz-script would put Boa in this crate's graph and fail no_boa. MAPPING.md says where that test belongs instead.