# CLAUDE.md -- Rust Project Conventions
## Philosophy
Functional, type-driven, domain-driven.
## Architecture
- Modules by domain context.
- One module per concern (error, document, element, fetch, install, cookie).
- Thin lib.rs that exposes `install(env, heap, html_doc)`.
## Types
- Sum types for error variants.
- No public struct fields.
- `#[must_use]` on getters and constructors.
## Error Handling
- Single project-wide `Error` enum.
- Display + std::error::Error impls by hand; no thiserror, no anyhow.
- Never panic.
## Style
- Prefer match over if/else, except on bool.
- No `return` keyword.
- No `mut`.
- Combinators over loops.
- Never match on Option<_>; use combinators.
- No unwrap()/expect() anywhere.
- No loop or for.
- No scan.
- No Rc/Arc.
- No naked `as` casts.
- Exhaustive matches; no `_` wildcard arm on enums.
## Traits
- No dyn Trait.
- Implement standard traits over ad-hoc methods.
## Linting
```toml
[lints.clippy]
all = { level = "deny", priority = -1 }
pedantic = { level = "warn", priority = -1 }
needless_pass_by_value = "warn"
manual_map = "warn"
```
## Verification
- Always run `RUSTFLAGS="-D warnings" cargo clippy --all-targets`.
- Always run `cargo fmt`.
## Testing
- Tests return `Result<(), Error>` and propagate failures with `?`.
- No assert!, assert_eq!, panic!, unreachable!, unwrap, expect.
## Dependencies
- `boa-cat = "0.7"` -- JS engine (accessor-property support since 0.3; Promise type + microtask driver + await dispatch since 0.4-0.6; ecma-parse-cat 0.3 parser bump in 0.7 unblocks `async () => ...` arrow syntax + `obj.catch(cb)` member access; this crate uses the accessor API for `document.cookie` and adds `Value::Promise(_) => None` arms in DOM extraction so engine-allocated promises pass through cleanly).
- `html-cat = "0.1"` -- source DOM tree.
- `net-cat = "0.1"` -- HTTP transport for `fetch`.
- `proptest`, `ecma-lex-cat`, `ecma-parse-cat` dev-deps.
- No path dependencies.
## Documentation
- `///` doc comments on every public item.
- `# Examples` with runnable code blocks.
## Layer context
This crate is the **JS ↔ DOM/fetch bridge**:
1. `boa-cat` -- JS engine.
2. `html-cat`, `css-cat`, `dom-cat`, `layout-cat`, `paint-cat`, `net-cat` -- rendering + network.
3. **`web-api-cat`** -- exposes the DOM and fetch to scripts.
4. `tauri-runtime-servocat` -- meta-crate.
## Strategy
DOM data lives entirely in the boa-cat heap as a tree of Objects. Native callables look up element data via the heap-handle passed as `this`, then return a new wrapping value. Mutations create new heap objects (e.g. `setAttribute` returns the new element-object). Scripts hold the same handles before and after; the underlying boa-cat heap threads the new state through `evaluate_program_with`.
## v0.7 scope (current)
- Image element accessor pack (v0.7.20) opens the `<img>` story for the Tauri pitch. New module `src/image.rs` installs seven properties via a single `install_image_accessors` call wired into all three element build sites in `src/document.rs` (alongside the v0.7.18 input pack). Four attribute-reflecting accessor pairs: `src` (string), `alt` (string), `width` (number, parsed from attribute via `text.trim().parse::<f64>().ok().unwrap_or(0.0)`), `height` (number, same). Three hidden-slot read-only accessors: `naturalWidth` reads the `__natural_width` slot, `naturalHeight` reads `__natural_height`, `complete` reads `__complete`. All three install a no-op setter so `el.naturalWidth = 0` does not throw (matching the v0.6.7 outerHTML setter pattern). Downstream consumers populate the slots through two public helpers: `set_natural_size(&element_value, w, h, heap)` writes the two natural-size slots; `set_complete(&element_value, bool, heap)` flips the load flag. Both return the new heap (functional pattern, no mutation). Non-img elements see harmless defaults (empty strings, 0, false) since the pack installs on every element same as the v0.7.18 form-control accessors -- simpler than tag-gating at install time. 15 new integration tests in `tests/image.rs` cover attribute reflection getters / setters round-tripping through `getAttribute`, slot defaults before download, no-op setter on read-only properties, `createElement('img')` install-site coverage, non-img element defaults, attribute-selector queryability (`img[src="/b.png"]`), innerHTML round-trip preserving attributes, and an end-to-end `set_natural_size` + `set_complete` test that drives the heap through both helpers and then reads back via JS string concat (avoiding `String(v)` since boa-cat does not yet expose a `String` constructor).
- `:disabled` and `:enabled` pseudo-classes (v0.7.19) complete the form-state pseudo trio started in v0.7.18. `PseudoClass` gains two new bare variants `Disabled` and `Enabled`; parser's bare-keyword arm recognises both names. Matcher uses a new `is_form_control(node_id, heap)` helper that walks the element's `tagName`, lowercases it, and `matches!` against the fixed HTML form-control set (`input | button | select | textarea | option | optgroup | fieldset`). Tag gate matters: `<div disabled>` matches NEITHER `:disabled` nor `:enabled` per spec. After the tag gate, `:disabled` reuses the v0.7.18 `has_attribute_set(node_id, "disabled", heap)` check; `:enabled` is the inverse `!has_attribute_set(...)`. Combines with attribute selectors (`input[type=checkbox]:disabled`), `:not()`, and the existing `:checked` pseudo. Updates through `setAttribute('disabled', '')` flip the matcher state live so `matches(':disabled')` reflects post-write state. 7 new integration tests in `tests/input.rs` cover the disabled-pick, tag gate (skip `<div disabled>`), enabled/disabled partition (3/3 split across 6 controls), full form-control coverage (7 tags), setAttribute-driven state flip, and `input[type=checkbox]:disabled` combination.
- Form-control accessors + `:checked` pseudo-class (v0.7.18) opens the form-input story for the Tauri pitch. New module `src/input.rs` installs two accessor pairs (`AccessorPair::new(getter, setter)`) on every element via three new install sites in `src/document.rs` (alongside the existing inner/outer HTML accessors): `install_value_accessor` and `install_checked_accessor`. The `value` accessor reads / writes `__attributes.value` via `pub(crate)` thin wrappers `element::read_attribute` / `write_attribute` (existing private helpers, bumped to `pub(crate)` so the input module can call them). The `checked` accessor reflects attribute presence: the getter returns `Value::Boolean(read_attribute(...).is_some())`; the setter routes true through `write_attribute("checked", "")` and false through a new `element::remove_attribute` helper (which rebuilds `__attributes` via `Object::from_properties(properties().filter(...))` since boa-cat's `Object` has no `.without` API). Selector machinery gains `PseudoClass::Checked` (bare variant) parsed in the bare-keyword arm, with the matcher arm calling a new `has_attribute_set(node_id, name, heap)` helper that walks `attributes_id_of` and checks `attrs.get(name).is_some()`. Truthiness conversion in the setter follows JS rules (Boolean direct, Number nonzero+non-NaN, String non-empty, Object/Function/Native/Promise always true, Null/Undefined/None false) -- exhaustive enum match per the working-rule no-`_` arms. 12 new integration tests in `tests/input.rs` cover the value getter/setter round-trip, checked getter/setter, `:checked` via querySelector / querySelectorAll / matches, combination with `[type=radio]` attribute selector + `:not(:checked)` negation, and post-setter selector queries seeing the new state.
- `:empty` and `:root` structural pseudo-classes (v0.7.17) join the v0.7.13-16 pseudo-class roster. `PseudoClass` gains `Empty` and `Root` bare variants and the parser's bare-keyword arm recognises both names. `:empty` is backed by a new `is_empty_element` helper that combines `element_children(node_id, heap).is_empty()` (no element children) with `string_property(object, "textContent").is_empty()` (no text content -- whitespace included per CSS3 spec). `:root` is a one-line `!has_parent(node_id, heap)` check. v0.7.17 also fixes a pre-existing semantic gap: `document.querySelector` / `querySelectorAll` / `getElementsByTagName` / `getElementsByClassName` previously walked descendants of the document root only, so `document.querySelector('html')` and `document.querySelector(':root')` returned `null`. Two new public helpers in `src/element.rs` -- `find_first_in_subtree` and `find_all_in_subtree` -- include the root in the candidate set before recursing into descendants. The four document call sites in `src/document.rs` switched to these helpers. Element APIs (`Element.querySelector` and friends) still use the descendant-only helpers since the spec excludes self for those. Pre-existing tests `universal_selector_matches_first_descendant` (now expects html, was body) and `not_pseudo_class_accepts_selector_list` (now accepts html | body | div, was body | div) widened to the corrected semantics.
- `:first-of-type` / `:last-of-type` / `:only-of-type` type-aware structural pseudo-classes (v0.7.16) join the v0.7.13 child-position trio and the v0.7.15 `:not()` negation. `PseudoClass` gains three new variants (`FirstOfType`, `LastOfType`, `OnlyOfType`) and the bare-keyword arm of `parse_pseudo_class` is extended to recognise the new names. Matcher uses two new helpers: `tag_name_of(node_id, heap) -> Option<String>` (lowercased `tagName` lookup), then `has_preceding_sibling_of_same_type` and `has_following_sibling_of_same_type` which walk siblings via `std::iter::successors(previous_sibling_id, ...)` / `successors(next_sibling_id, ...)` and call `.any(|id| tag_name_of(id).is_some_and(|t| t.eq_ignore_ascii_case(&want)))`. Semantics: `h2:first-of-type` picks the first h2 sibling even when an earlier sibling has a different tag (different from `:first-child` which would fail). The parent-required guard still applies (detached / root elements match nothing), so the matcher arms `&&` `has_parent` with the same-type check. Reaches scripts through `querySelector` / `querySelectorAll` / `matches` / `closest` and combines freely with `:not()`, attribute selectors, descendant / child / sibling combinators.
- `:not(selector)` negation pseudo-class (v0.7.15) joins the v0.7.13 structural pseudo-classes. `PseudoClass` enum gains a `Not(Box<SelectorList>)` variant — the argument is a full `SelectorList`, so `:not(.foo, .bar)` (Selectors Level 3 list arg) and `:not(:not(.foo))` (Selectors Level 4 nested negation) both work. Parser: a new `parse_pseudo_class(bytes, start) -> (Option<PseudoClass>, usize)` helper handles both the bare `:first-child` form and the paren form `:not(arg)`; for the paren form, `find_balanced_close_paren(bytes, arg_start, 1)` walks bytes tracking nesting depth so nested parens parse correctly. The inner argument text is then handed back to `parse_selector_list` for a recursive parse. Matcher: `matches_pseudo_class` takes `&PseudoClass` (Copy dropped from the enum since `Not` wraps an owned `Box<SelectorList>`); `PseudoClass::Not(list) => !matches_selector_list(node_id, list, heap)` — straightforward inversion. The parent-required guard from v0.7.13 applies only to the three structural variants (extracted to a new `has_parent(node_id, heap)` helper); negation doesn't need it. Reaches scripts through `querySelector` / `querySelectorAll` / `matches` / `closest` — all four entry points consume the same selector machinery.
- `getElementsByTagName(name)` / `getElementsByClassName(classNames)` (v0.7.14) on both `Element` and `document`. Both return a NodeList-shaped Object (same shape as `querySelectorAll`'s output, see v0.7.11): numeric-key entries + `length`.
- **`getElementsByTagName(name)`**: `name === '*'` matches every descendant; case-insensitive tag match (since the selector parser lowercases tag names already); empty `name` returns an empty NodeList per spec. Implementation: just calls `find_all_descendants(root, name, heap)` -- a tag name is a valid one-compound selector.
- **`getElementsByClassName(classNames)`**: splits on ASCII whitespace; requires ALL classes to be present on each match. Empty / whitespace-only input returns an empty NodeList per spec (NOT match-everything, even though the underlying matcher would treat an empty classes Vec as "no constraints" = match-all). Implementation: rewrites the parsed class list to a concatenated `.foo.bar` selector string via the new `class_names_to_selector_string` helper, then calls `find_all_descendants`. Helper uses `fold(String::new(), |acc, c| format!("{acc}.{c}"))` to avoid the `format_collect` clippy lint that fires on `.map(format!).collect()`.
- Element-side variants scope to the subtree rooted at `this`; document-side variants walk from `documentElement` via the existing `document_root_id` helper. Both built atop the existing pub `find_all_descendants` and `build_node_list` from v0.7.11.
- `Element.matches(selector)` / `Element.closest(selector)` + `:first-child` / `:last-child` / `:only-child` pseudo-classes (v0.7.13).
- **`matches`**: pub `matches_impl` NativeFn; resolves `this`'s `ObjectId`, runs `matches_selector_list` (existing helper, unchanged), returns `Value::Boolean`. Bad inputs (non-Object `this`) return `false`.
- **`closest`**: pub `closest_impl` NativeFn; uses `std::iter::successors(Some(this_id), |id| parent_element_id(*id, heap))` to walk the chain inclusive of self, finds the first via `.find(|id| matches_selector_list(*id, list, heap))`. Per spec, `closest` includes `this` in the search, so a script calling `el.closest('.thing')` when `el` itself matches gets back `el`.
- **Pseudo-classes** (`:first-child`, `:last-child`, `:only-child`): the `CompoundSelector` gains a `pseudo_classes: Vec<PseudoClass>` field; `parse_compound_recursive` now matches a `b':'` byte and scans the following identifier (lowercased) against three known names -- unknown pseudo-classes are silently skipped (forward-compat for `:nth-child(n)` etc.). Matcher: `matches_compound` checks `pseudos_ok` first (early-return on fail) via `matches_pseudo_class`, which guards on `parent_element_id` (detached / root elements match no pseudo-class) then dispatches: `FirstChild` via `previous_sibling_id(node, heap).is_none()`, `LastChild` via new `next_sibling_id` helper, `OnlyChild` via both. `next_sibling_id` mirrors `previous_sibling_id` (resolves parent's children-array Object, finds `node_id`'s position, returns `position + 1` entry).
- Sibling combinators in `querySelector` / `querySelectorAll` (v0.7.12). Adjacent sibling `a + b` and general sibling `a ~ b` extend the v0.7.8 selector grammar. Parser: `parse_complex_tail` now branches on `b'>' | b'+' | b'~'` via a single `matches!` arm that emits the matching `Combinator` variant; the compound terminator list gains `b'+'` and `b'~'` so `tag.class+span` parses as `tag.class` followed by `+` followed by `span`. Matcher: two new variants on `Combinator` (`AdjacentSibling`, `GeneralSibling`) drop into the existing reverse-walk in `matches_complex_selector`. Adjacent uses a new `previous_sibling_id(node_id, heap)` helper that resolves the parent via the v0.6.8 `__parent__` slot, walks the parent's children-array Object to find `node_id`'s position, and returns the entry at `position - 1` (or `None` for the first child or detached node). General sibling uses `walk_previous_siblings_find_match` -- `std::iter::successors(previous_sibling_id, ...)` finds the closest preceding sibling matching the target compound. Sibling combinators do NOT cross parent boundaries (a `~` candidate's match must share a parent with the candidate), matching spec. Combines with all existing combinators (`section h2 + p` reaches the descendant + adjacent-sibling pattern). Available through both `querySelector` (first match) and `querySelectorAll` (all matches in document order).
- `querySelectorAll(selector)` (v0.7.11). Companion to v0.7.8's `querySelector`, returning ALL descendant matches in depth-first pre-order rather than stopping at the first. Available on both `Element` and `document`. Result is a `NodeList`-shaped Object: numeric-key entries plus `length`, same shape `Element.children` uses, so script-side `for (let i = 0; i < list.length; i++) list[i]` iteration patterns work uniformly. Same selector grammar as `querySelector` (no source duplication; both share `parse_selector_list` + `matches_selector_list`). New helpers in `src/element.rs`: `query_selector_all_impl(args, this, heap, fuel)` (the NativeFn registered on every element); `find_all_descendants(root, source, heap) -> Vec<ObjectId>` pub helper used by `document.querySelectorAll` via `document_query_selector_all_impl`; private `find_all_descendants_matching(root, &list, heap)` does the recursive DFS via `flat_map`; pub `build_node_list(matches, heap) -> (Value, Heap)` wraps a `Vec<ObjectId>` as the NodeList-shaped Object. No-match case returns an empty NodeList (`length === 0`), not `null`, so `.length` is always a Number. Deferred: full live-NodeList semantics (real browsers update the list as DOM mutates; ours is a snapshot at call time), `forEach` / iterator-protocol bindings on the NodeList Object.
- `cancelable` honouring on `preventDefault()` (v0.7.10). `prevent_default_impl` now reads `this.cancelable` and only flips `defaultPrevented` when it's `Value::Boolean(true)`. Per DOM spec, this means:
- `new Event(type)` (default cancelable=false): `preventDefault` is a no-op. `dispatchEvent` returns `true` even if listeners called `preventDefault`.
- `new Event(type, { cancelable: true })`: `preventDefault` works.
- `{ type: 'foo' }` plain object (no cancelable key): `preventDefault` no-ops (`read_bool_flag` returns false for missing).
- `{ type: 'foo', cancelable: true }` plain object: `preventDefault` works.
Pre-dispatch `e.preventDefault()` also honours the gate -- on a non-cancelable event the flag stays false. Backward compat note: the v0.7.5 / v0.7.9 tests that dispatched plain `{type: 'click'}` objects expecting preventDefault to work had to add `cancelable: true` to the literal; documented in the test comments.
- `new Event(type, options)` + `new CustomEvent(type, options)` (v0.7.9). Two new pub `NativeFn`s in `src/event.rs` -- `event_constructor_impl` and `custom_event_constructor_impl` -- bound as the `Event` and `CustomEvent` globals from `install.rs`. Both work as constructors (`new Event(...)`) and as bare calls (`Event(...)`) since boa-cat 0.7.2's `expression::construct` discards the engine-allocated `this` when the NativeFn returns an Object. Constructor reads `args[0]` as `type` and `args[1]` (if present and Object-shaped) for `bubbles` / `cancelable` / `composed` boolean options, plus `detail` for CustomEvent. Each option defaults to false / null. Resulting Object carries the full event surface: `type` / `bubbles` / `cancelable` / `composed` / `defaultPrevented` (false) / `target` (null) / `currentTarget` (null) / `detail` plus the three method bindings (`preventDefault`, `stopPropagation`, `stopImmediatePropagation`) and the two internal stop-flag keys (`__propagation_stopped__`, `__immediate_propagation_stopped__`). Dispatch refactor (also v0.7.9): `dispatch_event_impl` now calls `decorate_event_in_place(event, target_id, heap)` which `store_object`s the user's event Object with all spec slots set/reset, rather than the v0.7.5 `decorate_event` which built a fresh decorated copy. Net effect: a user holding `const e = new Event('click')` sees `e.defaultPrevented` reflect what listeners did during `el.dispatchEvent(e)`. Plain `{type: 'foo'}` literals still work -- they get the slots added in place, which is a side effect scripts won't notice unless they introspect the literal's keys. Limitation: no `cancelable` honoring yet (`preventDefault` always flips the flag regardless of `event.cancelable`); no `eventPhase` slot; no `timeStamp` / `isTrusted`.
- `querySelector` extended grammar (v0.7.8). The v0.6.x five-pattern subset (`tag` / `.class` / `#id` / `tag.class` / `tag#id`) is replaced with a real CSS selector parser + matcher. Surface: comma-separated selector lists; per-compound `*` universal, tag, `#id`, multiple `.class`, multiple `[attr]` / `[attr=val]` / `[attr^=val]` / `[attr$=val]` / `[attr*=val]`; whitespace (descendant) and `>` (child) combinators between compounds. Data model in `src/element.rs`: `SelectorList { selectors: Vec<ComplexSelector> }`, `ComplexSelector { compounds: Vec<CompoundSelector>, combinators: Vec<Combinator> }` (invariant: `combinators.len() == compounds.len() - 1`), `CompoundSelector { universal, tag, id, classes, attributes }`, `AttributeMatch { name, operator, value }` where `AttrOperator` is `Exists / Equals / Contains / StartsWith / EndsWith`. Matching walks the candidate's ancestor chain in reverse via `parent_element_id` (reading the v0.6.8 `__parent__` slot): the last compound must match the candidate; for each preceding combinator+compound pair, `Combinator::Child` requires the immediate parent to match, `Combinator::Descendant` finds any matching ancestor via `std::iter::successors(__parent__, ...).find(matches_compound)`. Helpers `find_first_descendant` and `find_by_id` rebuilt around the new model. Deferred: sibling combinators (`+`, `~`), pseudo-classes (`:first-child` / `:nth-child(n)` / `:not(...)`), pseudo-elements, namespaces.
- `Element.dataset` (v0.7.7). Each parsed element gets a `dataset` Object snapshotting every `data-*` attribute under its camelCase suffix. `<div data-user-name='alice'>` -> `el.dataset.userName === 'alice'`; `<div data-user-profile-id='42'>` -> `el.dataset.userProfileId === '42'`. Multi-segment kebab → camelCase via `crate::inline_style::kebab_to_camel` (promoted to `pub` in this release) applied to the suffix after stripping `data-`. Bare `data` (no suffix) is excluded; non-`data-*` attributes are excluded. `createElement` output gets an empty dataset Object. Per-element (not shared). Values are always strings (per spec). Built via `build_dataset_object(attribute_pairs, heap)` in `document.rs`; wired into both `build_element` (parsed-HTML, real data) and `build_blank_element` (createElement, empty). Limitation: snapshot semantics -- runtime `setAttribute('data-foo', 'x')` does NOT update dataset, and `el.dataset.foo = 'x'` does NOT call setAttribute. Real browsers expose dataset as a live DOMStringMap proxying through to the attribute store; we don't have Proxy support so the snapshot is the v0 compromise. For round-tripping after writes, scripts should use `setAttribute('data-foo', value)` + `getAttribute('data-foo')` directly.
- Capture-phase listeners (v0.7.6). `addEventListener(type, callback, useCapture)` accepts a third arg parsed by `parse_capture_arg`: `Value::Boolean(true)` registers as capture; `Value::Object({capture: true})` does the same via the options-object shape; anything else (including `Value::Undefined`) defaults to bubble. Storage shape changed in this release: each per-type slot under `__listeners__` is now an array of `{callback, capture}` Object entries (allocated via `make_listener_entry`) rather than bare callable Values. `removeEventListener` parses the same third arg and matches on the `(callback, capture)` pair per spec -- a `(cb, true)` registration is NOT dropped by a `(cb)` removal call and vice versa. `dispatchEvent` now walks three phases through a shared `walk_chain(chain, event, type, filter, heap, fuel)` helper parameterised by a `ListenerFilter` enum: CAPTURE walks the ancestor chain in reverse (root -> target's parent) with `ListenerFilter::Capture`; AT_TARGET fires all of target's listeners regardless of capture flag (`ListenerFilter::Any`) in registration order; BUBBLE walks ancestors forward (target's parent -> root) with `ListenerFilter::Bubble`. `stopPropagation` / `stopImmediatePropagation` checks honour the same flags through all three phases. Limitations: no `once` / `passive` / `signal` options yet; `useCapture` still ignored when passed to `removeEventListener` as `Value::Undefined`.
- Event flow methods (v0.7.5). `dispatchEvent` now decorates the supplied event Object with `target` / `currentTarget` / `defaultPrevented` (initially `false`) plus three method bindings: `preventDefault`, `stopPropagation`, `stopImmediatePropagation`. Decoration runs once at dispatch entry, before bubble walk; the original event Object is left untouched (decoration produces a fresh Object via `Object::from_properties` chaining the user props with the spec slots). `currentTarget` updates per bubble level; `target` stays as the original dispatch target throughout. `preventDefault()` flips `defaultPrevented` and makes the function return `Value::Boolean(false)`; `stopPropagation()` halts the bubble after the current level (`__propagation_stopped__` checked at the top of each level's listener walk); `stopImmediatePropagation()` halts BOTH remaining listeners at the current level AND the bubble (`__immediate_propagation_stopped__` checked at the top of each listener invocation in addition to the level-entry check). All three methods are idempotent. The flags are exposed under `pub const DEFAULT_PREVENTED_KEY` (the others stay crate-private since their shape may evolve). Limitations: no capture phase, no `cancelable` honoring (every event behaves as if cancelable=true), no `Event` constructor (plain `{type: 'foo'}` objects work because decoration adds whatever spec slots are missing), no `composedPath`.
- `EventTarget` mixin (v0.7.4): every element gets `addEventListener(type, callback)`, `removeEventListener(type, callback)`, and `dispatchEvent(event)`. State lives under a lazy hidden `__listeners__` Object shaped as `{ <type>: { 0: cb, 1: cb, ..., length: n } }` -- created on first `addEventListener` so listener-free elements carry no extra heap weight. Identity for removal is `Value::PartialEq` (function pointer for `Native`, `FunctionId` for `Function`). `dispatchEvent` walks the bubble chain via the v0.6.8 `__parent__` backref (no separate event-flow infrastructure needed); target fires first, then each ancestor up to the first null-parent. Each handler invocation goes through `boa_cat::expression::call_function` (made `pub` in boa-cat 0.7.1) with `this = level`, `args = [event]`. Listener throws are swallowed at the dispatch boundary per DOM spec (report-and-continue). Limitations: no capture phase (bubble only), no `Event` constructor (use `{ type: 'foo' }` plain objects), no `preventDefault` / `stopPropagation`, no `once` / `passive` / `signal` options.
- HTTPS in `fetch` (v0.7.3). Bumped `net-cat` dep from `0.1` to `0.3` with the `tls` feature enabled. rustls + webpki-roots + ring as the pure-Rust crypto provider (no C toolchain, no openssl). `fetch('https://...')` now works through the same `perform_fetch` path as `http://`. Side benefits from net-cat 0.3: chunked-transfer decoding (`Transfer-Encoding: chunked` bodies are now correctly decoded), RFC 7231 §6.4 redirect handling (301/302/303 downgrade non-GET/HEAD to GET and drop body; 307/308 preserve method+body; cross-origin hops strip `Cookie` + `Authorization`; capped at 10 hops). No web-api-cat surface change. Live HTTPS testing intentionally out of scope to keep the test suite offline-deterministic.
- Inline `style="..."` (v0.7.2) parsed at build time + serialised on extract. `crate::inline_style::parse_inline_style(text)` splits on `;`, then on the first `:`, trims, and kebab-to-camelCases each property name so `<div style="font-size: 14px; color: red">` populates `el.style.fontSize === '14px'` and `el.style.color === 'red'`. `build_element` runs this once at construction time and pre-populates the style Object's properties (instead of starting empty as in v0.6.4). Symmetrically, `extract.rs`'s `collect_attributes` calls `synthesise_style_attribute(object, heap)` which walks the post-eval style Object's string properties, hands them to `inline_style::serialize_inline_style(...)`, and (a) strips any old `style` entry from `__attributes` and (b) appends the freshly-serialised one so it lands in the dom-cat element's attribute list. Empty style Object => no `style` attribute at all. Multi-segment kebab support (`border-bottom-color` => `borderBottomColor`). Vendor prefixes left lowercase-only -- `WebkitTransform` would round-trip as `webkittransform`, not `-webkit-transform`; a future chunk can special-case leading capitals.
- `fetch(url)` (v0.7.1) returns a `Promise<Response>`. The Promise wrapping is purely a boundary shape change so the engine's existing await / .then / .catch dispatch fires correctly; the underlying transport remains synchronous (net-cat is sync). Success: `PromiseState::Resolved(response_obj)`. Failure (bad URL, network error, no args, non-string arg): `PromiseState::Rejected(Value::String("TypeError: ..."))`. The Response Object now exposes `text()` (returns `Promise<string>` of the body, decoded UTF-8 at fetch time and stashed under a hidden `__body__` slot) and `json()` (currently a rejected-stub returning `"TypeError: response.json() not yet implemented; use JSON.parse(await r.text()) instead"` — a Rust-side JSON parser is out of scope; ecma-runtime-cat's `JSON.parse` makes the user path work via `JSON.parse(await r.text())`). Direct properties `ok`, `status`, `statusText`, `headers` are unchanged from v0.6.x.
- `localStorage` / `sessionStorage` (v0.7.0): both ship as Storage-shaped Objects with `getItem(key)`, `setItem(key, value)`, `removeItem(key)`, `clear()`, `key(index)`, and a read-only `length` accessor. Each owns a hidden `__items__` Object holding the actual key->value pairs (always coerced to strings per spec). Hosts wire the JS-visible projection like the v0.4 cookie pattern: pre-eval `seed_storage(storage_value, heap, entries)` overwrites `__items__`; post-eval `read_storage_items(storage_value, heap)` returns the final pairs. `lookup_local_storage(env, heap)` / `lookup_session_storage(env, heap)` pull the Storage Value back out of the env so hosts don't have to thread it manually. `localStorage` and `sessionStorage` are bound as top-level globals AND as properties on the new `window` Object; iteration order is `BTreeMap`-sorted-by-key (spec leaves `key(i)` order implementation-defined, so deterministic sort is conformant).
- `window` global (v0.7.0) is now a proper Object instead of an alias for `document`. Its `document` / `localStorage` / `sessionStorage` properties point at the same Values bound at the top level (`document === window.document`, etc.).
## v0.6 scope
- `document.getElementById(id)` -- walks the document tree, returns the matching element-object or `null`.
- `document.querySelector(selector)` -- limited selector subset (`tag`, `.class`, `#id`, `tag.class`, `tag#id`).
- Element properties: `tagName`, `id`, `className`, `textContent`, `children` array.
- Element methods: `getAttribute(name)`, `setAttribute(name, value)`, `hasAttribute(name)`, `appendChild(child)` (v0.6.1), `removeChild(child)` (v0.6.2), `insertBefore(newNode, referenceNode)` (v0.6.2), `cloneNode(deep)` (v0.6.6), `replaceChild(newChild, oldChild)` (v0.6.7), `remove()` (v0.6.8).
- Element properties: `parentElement` / `parentNode` accessor-getters (v0.6.8) that read the hidden `__parent__` slot. `Value::Object(parent_id)` when attached, `Value::Null` when detached or at the document root. Both accessors share the same `element::parent_getter_impl` `NativeFn` -- this crate doesn't yet distinguish elements from other node types, so `parentElement` and `parentNode` produce identical results.
- Navigation accessors (v0.6.9): `firstElementChild` / `lastElementChild` walk `this.children` directly; `previousElementSibling` / `nextElementSibling` walk `this.__parent__.children` after locating `this` by `ObjectId` equality. All four are getter-only accessor pairs installed via `install_sibling_accessors(element_id, heap)` after the existing parent accessors. Each returns the matching child element Value or `Value::Null` (out-of-bounds, no children, no parent). Helpers `own_children_view` and `parent_children_view` consolidate the lookup with `?` so the actual getter NativeFns stay one-liners.
- Element properties (extended): `classList` (v0.6.3), a `DOMTokenList`-shaped Object exposing `add(token)`, `remove(token)`, `contains(token)`, `toggle(token)`; `style` (v0.6.4), a fresh empty Object that scripts read and write through the engine's normal property storage (`el.style.color = 'red'; el.style.color === 'red'`). Inline `style="..."` attribute parsing is NOT performed (stays readable via `getAttribute('style')` only); back-prop from `el.style` into layout-cat is also deferred -- this v0 chunk targets script-compatibility, not visual styling.
- `Element.cloneNode(deep)` (v0.6.6) returns an independent copy of `this`. Shallow (no arg or anything other than literal `true`) clones only the element with an empty children array; deep (`true` argument) recursively clones every descendant. Implementation: read the original's data properties, allocate fresh copies of the `__attributes` and `style` Objects via `clone_object_option(id, heap)`, recursively clone children when deep via `clone_children_deep`, build the children-array Object via `build_array_object`, then reconstruct the element with `Object::from_properties` -- filtering out the per-instance keys (`__attributes`, `style`, `children`, `classList`) before re-attaching the cloned counterparts. Post-alloc: `install_class_list(new_id, heap)` builds a fresh `classList` with `__element__` retargeted to the new element; `install_inner_html_accessor` adds the `innerHTML` accessor pair. Native-method bindings (function pointers) are copied verbatim. Result: writing `clone.setAttribute`, `clone.classList.add`, `clone.style.x = ...`, or `clone.appendChild` never touches the original, and the same holds recursively for deep clones' descendants.
- Parent-backref infrastructure (v0.6.8): every element carries a hidden `__parent__` slot. Build-time: `build_element` writes `Value::Null` into the slot, then post-alloc folds over the just-built children setting each one's `__parent__` to the new parent's `ObjectId` (`element::set_parent_backref(child_value, parent_id, heap)`). Recursive nature of `build_element` means grandchildren get the right backref because each recursive call sets ITS own direct children before returning. `build_blank_element` (createElement output) defaults to `Null` since the new node is detached. Every structural mutator routes through the same helpers: `appendChild` / `insertBefore` set the new child's `__parent__` to the parent; `removeChild` clears it; `replaceChild` sets the new child's and clears the old child's; `innerHTML` setter first clears every old child's `__parent__` then sets each new child's to the host; `cloneNode(deep)` filters `__parent__` out of the `properties()` copy (defaults clone to `Null`) and folds backrefs over the deep-cloned children using the new element id. `Element.remove()` reads `this.__parent__`; if it's an Object, calls the same `remove_child_from_parent` helper that backs `removeChild` (which both excises `this` from the parent's children-array Object AND clears `this.__parent__`). Limitation: moving an already-attached child to a new parent via `appendChild` / `insertBefore` does NOT auto-detach from the old parent (the old parent's children array still lists it). This is a known v0.6.8 trade-off; full move-semantics is a future chunk.
- `Element.outerHTML` accessor (v0.6.7 getter, v0.6.9 setter). Getter returns `<tag attrs>inner</tag>` for the element itself by calling the same `serialize_element` helper that powers `innerHTML` (now `pub` so the outerHTML installer can reuse it). Setter (v0.6.9, on top of the v0.6.8 parent-backref infrastructure): reads `this.__parent__`; if null/non-Object the setter no-ops (matches the no-op-on-bad-input convention). Otherwise locates `this` by `ObjectId` equality in the parent's children-array Object, clears `this.__parent__`, parses the assigned string into a children Vec via `document::parse_fragment_children`, and rebuilds the parent's children-array Object with a 3-region splice (everything before `this_index` from old, the parsed children in order, everything after `this_index + 1` from old shifted by `inserted_count - 1`). Length adjusts by `inserted_count - 1`. `set_parent_backref` folds over the spliced-in roots so each new top-level element points at the parent. Detaches `this` (its own children stay intact, just detached as a subtree). Multi-root fragments work.
- `Element.replaceChild(newChild, oldChild)` (v0.6.7). Finds `oldChild` by `ObjectId` equality in `this.children`, writes `newChild` at that index via `children_obj.with(format!("{i}"), new_child.clone())`, `store_object`s the updated array (length preserved; the parent's `children` slot already points at the same `ObjectId` so no parent update needed). Returns `oldChild` per spec. No-ops when `oldChild` isn't actually a child (DOM spec would throw `NotFoundError`). Same in-place mutation pattern as `appendChild` / `removeChild` / `insertBefore`.
- `Element.innerHTML` accessor pair (v0.6.5). Getter walks `this.children` and serialises each as `<tag attrs>inner</tag>` recursively; if the element has no children, the (HTML-escaped) flattened `textContent` is emitted instead. Attributes are read from the hidden `__attributes` object in `BTreeMap` order so the output is deterministic. Minimal HTML-escaping per the spec subset: `<`, `>`, `&` (text), plus `"` (attributes). Setter calls `document::parse_fragment_children(input, heap)` which wraps the input in `<html><body>...</body></html>`, runs html-cat's full-document parser, locates the body element, runs `build_children` over its child nodes, then rebuilds the element's children-array Object in place via `heap.store_object`. On parse failure the setter no-ops, leaving the existing children untouched. Both halves are installed via `inner_html::install_inner_html_accessor` after `alloc_object` in both build paths (parsed-HTML and `createElement`). Limitation: serialisation flattens mixed text-and-element content (we don't track text-node positions); the leaf textContent fallback only kicks in when `children` is empty. The classList Object carries a hidden `__element__` backref to its parent element's `ObjectId`; methods resolve the current element via that backref each call so the latest `className` is always read. Writebacks delegate to `write_attribute(element, "class", new_class, heap)` so `className`, `getAttribute('class')`, and `__attributes.class` all stay in sync. `toggle` returns the new presence-state (`true` after add, `false` after remove) per MDN. classList is installed by `install_class_list(element_id, heap)` (a post-alloc step in both `build_element` and `build_blank_element`) since the backref needs the element's `ObjectId` and that only exists once `alloc_object` has run.
- DOM mutation (v0.6.1): `document.createElement(tag)` allocates a fresh element-shaped Object on the heap with the requested `tagName`, empty `id` / `className` / `textContent`, empty children array, empty `__attributes` object, and every standard method (`getAttribute`, `setAttribute`, `hasAttribute`, `querySelector`, `appendChild`, `removeChild`, `insertBefore`). A shared `document::build_blank_element(tag, heap)` helper carries the property template. `Element.appendChild(child)` clones the parent's children-array Object, writes the next numeric-key slot, increments `length`, and `store_object`s the updated array; the parent's `children` slot already points at the same `ObjectId` so the parent update is implicit. Returns the appended child per spec. `extract_document` walks `children` numerically, so newly-appended nodes reach layout / paint on the next eval pass.
- Structural mutation extras (v0.6.2): `Element.removeChild(child)` finds `child` by `ObjectId` equality in `this.children`, rebuilds the array Object with the slot dropped and subsequent indexes shifted down by one, decrements `length`, and `store_object`s the result. No-ops (and still returns the requested `child`) if `child` isn't actually a child of `this` (DOM spec says `NotFoundError`; this scoped impl stays inside the always-`Ok` `NativeFn` contract). `Element.insertBefore(newNode, referenceNode)` finds `referenceNode` by `ObjectId` equality, rebuilds the array Object with `newNode` slotted at that index and subsequent children shifted up by one, increments `length`. When `referenceNode` is `null` / `undefined` / any non-Object value the call delegates to `append_child_to_parent` (matching the DOM spec's null-ref case). A shared `children_object_of(this, heap)` helper resolves `(children_id, children_obj)` for all three mutation methods. Both new methods reach layout / paint via the same `extract_document` back-prop path as `appendChild`.
- `fetch(url)` -- synchronous net-cat call returning `{ ok, status, statusText, text, headers }` object.
- `extract_document(document_value, heap)` -- walks the post-script JS-side DOM tree and reconstructs a `dom_cat::Document`, so callers (e.g. `tauri-runtime-servocat`) can back-propagate scripted mutations into layout-cat / paint-cat. Text content is synthesized as a single Text child of each leaf element; comments and original text-node positions are not preserved.
- `document.cookie` -- installed as a `boa_cat::AccessorPair` (v0.4, requires boa-cat 0.3). Reads dispatch through a native getter that returns the current host-supplied projection of the jar; writes dispatch through a native setter that (a) appends the raw RHS string to a hidden `__cookie_writes__` log (preserving any `Max-Age` / `Path` / `Domain` / `Secure` / `HttpOnly` attributes) and (b) updates the JS-readable `__cookies_visible__` projection by extracting just the leading `name=value` and merging into the existing string by name (replace-by-name semantics matching real browsers). Hosts call `set_document_cookie(document, heap, &str)` once per evaluation to seed the projection and clear the write log; `read_cookie_writes(document, &heap)` returns the post-eval per-write entries in order, ready for `cookie::Cookie::parse` on the host side. `get_document_cookie` still returns the running JS-visible string for hosts that don't need attribute-aware merging. Visibility filtering (`HttpOnly`) remains the host's responsibility because this crate has no notion of cookie attributes; it only shuttles strings.
## Deferred to v0.5+
- Full CSS selector syntax in `querySelector` (combinators, attribute selectors, pseudo-classes).
- Async `fetch` (Promise-based; needs comp-cat-rs scheduler integration).
- Inline `style="..."` attribute parsing into `el.style`, computed styles, and back-prop from `el.style` into layout-cat (v0.6.4 ships only the empty-Object script-compat layer).
- Event API.
- `XMLHttpRequest`, `WebSocket`.
- HTTPS support (waits on net-cat's TLS feature).
- Faithful round-trip of text-node positions and comments through `extract_document`.
- Object-literal `{ get cookie() {} }` / `{ set cookie(v) {} }` syntax in user-provided scripts -- waits on ecma-parse-cat emitting `ObjectPropertyKind::Get/Set` (the engine in boa-cat 0.3 already handles those AST variants; the v0.4 cookie accessor is installed from Rust via `Object::with_accessor`).