rdom-core
The pure DOM — arena-backed nodes, CSS selectors, events,
MutationObserver. Zero rendering dependencies, zero
presentation concerns.
Use this crate directly for:
- Building + querying an in-memory HTML-ish tree.
- Dispatching events with full capture → target → bubble
semantics (stop-propagation, prevent-default,
AbortSignal). - Observing tree mutations for reactive bindings, a11y mirrors, devtools, or cascade invalidation.
- Serializing a subtree back to markup (
outer_markup,inner_markup).
If you want to paint a tree to a terminal, pair it with
rdom-tui. If you want to parse HTML-ish
templates into a tree, pair it with
rdom-parser.
Quick start
use Dom;
let mut dom: Dom = new;
let root = dom.root;
let app = dom.create_element;
dom.set_attribute.unwrap;
let h1 = dom.create_element;
let t = dom.create_text_node;
dom.append_child.unwrap;
dom.append_child.unwrap;
dom.append_child.unwrap;
// CSS queries.
assert_eq!;
// Walk the tree with the accessor API.
assert_eq!;
assert_eq!;
// Serialize.
println!;
// <div id="app"><h1>Hello, rdom!</h1></div>
See examples/tree_builder.rs for a
longer tour.
Generic over the "presentation ext"
Dom<Ext> is generic over a per-element extension struct. Pure
rdom-core uses Ext = () (the Dom alias). Presentation layers
parameterize with their own struct to hang layout / style /
rendering state off each element without touching core code.
// In rdom-tui:
pub type TuiDom = ;
Core operates entirely through generic NodeId + attributes +
classes + children. It never reads or mutates the Ext data.
Selectors
Full spec-subset matching via query_selector, query_selector_all,
matches, closest:
| Syntax | |
|---|---|
| type | div, h1 |
| universal | * |
| id | #app |
| class | .hero |
| attribute | [lang], [lang="en"], [class~="a"], `[class |
| compound | a.active[href="#"] |
| descendant | ul li |
| child | ul > li |
| next-sibling | h1 + p |
| subsequent | h1 ~ p |
| pseudo-classes | :not(...), :first-child, :last-child, :only-child, :empty, :root, :hover, :active, :focus, :focus-within |
Pseudo-elements (::before, ::after) are recognized as selector
suffixes; they're extracted before parsing and delivered to
rdom-tui's cascade separately. Core doesn't know what a
pseudo-element means, only that the suffix exists.
Events
Capture → target → bubble dispatch with spec semantics:
use ;
let mut dom: Dom = new;
let root = dom.root;
let btn = dom.create_element;
dom.append_child.unwrap;
dom.add_event_listener
.unwrap;
let mut ev = new;
dom.dispatch_event.unwrap;
Options include capture, once, and AbortSignal for scope-bound
cleanup (signal.abort() removes every listener that shares it).
Mutations + observers
Every mutation entry point (set_attribute, add_class,
append_child, set_node_value, set_hovered, set_focused,
set_selection, ...) fires a Mutation record to every
registered observer:
;
let mut dom: Dom = new;
dom.add_mutation_observer;
let d = dom.create_element;
dom.append_child.unwrap;
dom.add_class.unwrap;
// Logger sees: ChildListChanged, ClassChanged.
Observers are the single invalidation mechanism — rdom-tui's
DirtyTracker is one registered observer. A reactive framework or
devtools mirror would register its own without touching core.
MutationObserver::observe panics if it attempts to mutate the
Dom — enforced by an is_observing re-entrancy guard.
Mutation record types
AttributeChanged { id, name, old, new }ClassChanged { id, added, removed }ChildListChanged { parent, added, removed }CharacterDataChanged { id, old, new }— text / comment node dataInteractionChanged { prev, next, kind }— hover / focusSelectionChanged { prev, next }— document selection
See examples/mutation_observer.rs
for a logger that prints every record type.
AbortSignal
Modern-HTML cancellation pattern. Create a controller, share its signal across listener registrations, abort once to remove them all:
use AbortController;
let controller = new;
let opts = default.signal;
dom.add_event_listener?;
dom.add_event_listener?;
// ... some time later ...
controller.abort; // removes both listeners
Supersedes the older remove_event_listener workflow for
scope-bound cleanup (widgets unmounting, components tearing down).
Serialization
outer_markup(id) emits a subtree as HTML-ish text:
let out = dom.outer_markup;
// <div id="app"><h1>Hello</h1></div>
inner_markup(id) omits the element's own tag, emitting only its
children. For most inputs in the defined subset, round-tripping
via rdom-parser yields the original source —
see rdom-parser's examples/round_trip.rs.
Benchmarks
cargo bench -p rdom-core --bench arena
Currently covers arena-construction and tree-walk micros. Full
dispatch + cascade benches live in
rdom-tui.
Further reading
DESIGN.md— architectural overview: crate map, non-negotiable invariants, roadmap.DIVERGENCES.md— every deliberate departure from the web platform.- Module docs under
src/— each file has a top-level doc comment covering its specific role.