web-api-cat 0.7.1

Bindings between boa-cat (JS engine) and the DOM (html-cat tree) plus fetch (net-cat). v0.7.1 upgrades `fetch(url)` to return a `Promise<Response>` matching the DOM Fetch API shape: success becomes a resolved Promise carrying the response Object; bad URL / argument errors become a rejected Promise with a `TypeError:`-prefixed string reason; `response.text()` returns a resolved `Promise<string>` of the body; `response.json()` is a rejected-stub deferring to `JSON.parse(await r.text())` (ecma-runtime-cat already exposes JSON.parse). Scripts now use `await fetch(url)` and `fetch(url).then(r => r.text())` end-to-end through the engine's microtask driver. Seventh sub-crate of a Servo-replacement webview runtime targeting Tauri.
# 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)

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