The core data structure of BrowserOxide. Every other crate either produces, queries, or mutates this tree.
Implements the subset of DOM Living Standard, HTML Living Standard, and Shadow DOM needed for SOTA 2026 web scraping.
pub enum NodeData {
Document {
url: Url,
title: String,
mode: DocumentMode, // quirks, limited-quirks, no-quirks
},
DocumentType {
name: String,
public_id: String,
system_id: String,
},
Element {
name: QualName,
attrs: Vec<Attribute>,
template_contents: Option<NodeId>,
shadow_root: Option<NodeId>, // Shadow DOM
custom_element_state: CustomElementState,
},
Text(String),
Comment(String),
ProcessingInstruction { target: String, data: String },
DocumentFragment,
ShadowRoot {
mode: ShadowRootMode, // Open or Closed
host: NodeId, // Element this shadow is attached to
delegates_focus: bool,
},
}
pub enum ShadowRootMode { Open, Closed }
pub enum CustomElementState { Undefined, Failed, Uncustomized, Precustomized, Custom }pub struct Dom {
nodes: Vec<Node>,
free_list: Vec<NodeId>,
}
pub struct Node {
pub data: NodeData,
pub parent: Option<NodeId>,
pub first_child: Option<NodeId>,
pub last_child: Option<NodeId>,
pub prev_sibling: Option<NodeId>,
pub next_sibling: Option<NodeId>,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct NodeId(usize);Arena gives us: O(1) node access, cache-friendly traversal, no Rc<RefCell<>> overhead, easy JS engine integration (NodeId is a lightweight handle).
~20% of page loads use Custom Elements (2026). Anti-bot widgets (Turnstile, hCaptcha) may use Shadow DOM for isolation. Frameworks like Lit, Ionic, and Angular use it heavily.
Element.attachShadow({mode})— Creates a ShadowRoot node attached to the element- ShadowRoot — DocumentFragment-like node, root of shadow tree
<slot>content distribution — Light DOM children distributed into shadow tree slots- Default slot (unnamed)
- Named slots (
<slot name="header">) HTMLSlotElement.assignedNodes({flatten: true})slotchangeevent
- CSS scoping — Styles in shadow tree don't leak out; external styles don't penetrate in
:host,:host(),::slotted(),::part()- CSS custom properties DO cross shadow boundaries
- Event retargeting — Events from shadow tree retarget to host element at shadow boundary
event.composedPath()reveals full path- Only
composed: trueevents cross boundaries (click, focus, input do; custom events don't by default)
element.shadowRoot— Returns ShadowRoot formode: 'open', null formode: 'closed'
For layout and selector matching, we need the flat tree — the composed view that combines light DOM and shadow DOM through slots:
Light DOM: Shadow DOM: Flat Tree:
<my-card> #shadow-root <my-card>
<h2>Title</h2> <div class="wrap"> #shadow-root
<p>Body</p> <slot></slot> <div class="wrap">
</my-card> </div> <h2>Title</h2> (slotted)
<p>Body</p> (slotted)
</div>
iframes are critical for anti-bot — Cloudflare Turnstile, reCAPTCHA, and hCaptcha all run inside cross-origin iframes.
- Separate DOM + JS context per iframe — Each iframe gets its own Document, Window, and V8 Context
- Same-origin access — Parent can access
iframe.contentWindowandiframe.contentDocument - Cross-origin isolation —
contentDocumentreturns null. OnlypostMessage()communication window.postMessage()— MessageEvent with structured clone data, origin checkingsrcdocattribute — Inline HTML content (about:srcdocorigin)sandboxattribute —allow-scripts,allow-same-origin,allow-forms, etc.contentWindow/contentDocument— Primary JS access APIs- Origin checking — Same-Origin Policy: protocol + host + port must match
- Lazy loading — Defer offscreen iframes; eagerly load in-viewport and JS-accessed iframes
Page loads Turnstile widget
→ Creates cross-origin iframe (challenges.cloudflare.com)
→ iframe runs WASM proof-of-work + canvas fingerprint + env checks
→ iframe sends token back to parent via postMessage
→ Parent includes token in form submission
Without working iframes + postMessage, Turnstile/reCAPTCHA/hCaptcha are completely broken.
| Interface | Key Methods/Properties |
|---|---|
document.hasFocus() |
Must return true (anti-bot checks this) |
EventTarget |
addEventListener(), removeEventListener(), dispatchEvent() |
Event |
type, target, composedPath(), preventDefault(), stopPropagation() |
MessageEvent |
For postMessage/iframe communication |
Window.postMessage() |
Cross-origin iframe communication |
HTMLIFrameElement |
contentWindow, contentDocument, src, srcdoc, sandbox |
| Interface | Key Methods/Properties |
|---|---|
Node |
nodeType, parentNode, childNodes, firstChild, lastChild, textContent, appendChild(), removeChild(), insertBefore(), cloneNode(), contains() |
Element |
tagName, id, className, classList, getAttribute(), setAttribute(), innerHTML, outerHTML, children, querySelector(), querySelectorAll(), matches(), closest(), getBoundingClientRect(), attachShadow(), shadowRoot |
Document |
documentElement, head, body, getElementById(), getElementsByClassName(), getElementsByTagName(), querySelector(), querySelectorAll(), createElement(), createTextNode(), createDocumentFragment(), title, URL, cookie, readyState, fonts |
HTMLElement |
style, dataset, offsetWidth, offsetHeight, offsetTop, offsetLeft, scrollTop, scrollLeft, click(), focus(), blur(), checkVisibility() |
| Interface | Key Methods/Properties |
|---|---|
MutationObserver |
observe(), disconnect(), takeRecords() |
IntersectionObserver |
observe(), unobserve(), disconnect() |
ResizeObserver |
observe(), unobserve(), disconnect() |
DOMTokenList |
add(), remove(), toggle(), contains(), replace() |
NodeList / HTMLCollection |
item(), length, forEach() |
DOMParser |
parseFromString() |
XMLSerializer |
serializeToString() |
HTMLSlotElement |
assignedNodes(), assignedElements(), assign() |
ShadowRoot |
mode, host, innerHTML, querySelector() |
HTMLTemplateElement |
content (DocumentFragment) |
| Interface | Key Methods/Properties |
|---|---|
TreeWalker |
Full traversal API |
Range |
Selection and manipulation |
FormData |
Form serialization |
CustomElementRegistry |
define(), get(), whenDefined() (for Web Components) |
Critical for SPAs — frameworks build UI by assigning HTML strings:
- Parse HTML fragment with html5ever (fragment parsing algorithm)
- Remove existing children
- Append parsed nodes
- Trigger MutationObserver callbacks
Full DOM Events with three phases:
- Capture — root → target
- Target — at the target
- Bubble — target → root
- Composed — events cross shadow boundaries when
composed: true
struct EventListener {
event_type: String,
callback: JsFunction, // V8 function handle
capture: bool,
once: bool,
passive: bool,
}- Arena allocation — NodeId is Copy, no lifetime issues, integrates with V8 GC via weak handles
- Lazy HTMLCollection / NodeList — Re-traverse on access, not pre-computed
- Flat tree caching — Shadow DOM flat tree computed lazily, invalidated on slot/mutation
- iframe isolation — Each iframe's DOM is a separate
Dominstance with its own arena