You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Complete the arena-allocated DOM tree with node creation, tree mutation, traversal iterators, and query methods. This is a leaf dependency with zero external crate deps beyond std — everything else builds on it.
Current State
crates/ie-dom/src/lib.rs has basic type definitions:
NodeId = usize
Document { nodes: Vec<Node>, root: NodeId } with new() and Default
Find first node where attributes.get("id") == Some(id)
Return Some(node_id) or None
Known limitation: arena memory
remove_child detaches nodes from the tree but does not free their arena slots. NodeIds are indices into Vec<Node>, and removed nodes remain allocated in the vector. Their parent is set to None and they are removed from their parent's children vec, but the Node struct itself stays in self.nodes.
This is acceptable for Phase 1. Document this as a known limitation in code comments.
Future optimization: Use slotmap or a generational arena with a free list for slot reuse. This would allow remove_child to actually reclaim memory and prevent NodeId reuse bugs (generational indices detect use-after-free).
Document::live_node_count(&self) -> usize — counts nodes that have at least one reference: nodes that have a parent OR are the root node. This gives the count of nodes actually in the tree, excluding orphaned/detached nodes.
Tests
All in crates/ie-dom/src/ as #[cfg(test)] mod tests blocks in the relevant modules.
Document & creation tests
new() creates doc with root node of kind Document
create_element("div") → node exists, kind is Element("div")
create_text("hello") → kind is Text("hello")
create_comment("note") → kind is Comment("note")
Created nodes have no parent and no children
Tree mutation tests
append_child: parent's children contains child, child's parent is parent
append_child twice to same parent: both are children, in order
append_child reparenting: child with existing parent moves — removed from old parent's children, old parent has fewer children
remove_child: child removed from parent's children, child's parent is None
remove_child with wrong parent → NotAChild error
remove_child with non-existent ids → NodeNotFound
insert_before: new_child appears before reference in parent's children
insert_before with reference not a child of parent → NotAChild
insert_before reparenting: detaches from old parent first
Parent: #2
Goal
Complete the arena-allocated DOM tree with node creation, tree mutation, traversal iterators, and query methods. This is a leaf dependency with zero external crate deps beyond std — everything else builds on it.
Current State
crates/ie-dom/src/lib.rshas basic type definitions:NodeId = usizeDocument { nodes: Vec<Node>, root: NodeId }withnew()andDefaultNode { kind, parent, children, attributes }NodeKind { Document, Element(String), Text(String), Comment(String) }No manipulation methods, no traversal, no queries, no error handling.
File Changes
Split
src/lib.rsinto modules:src/lib.rs— re-exports onlysrc/node.rs—Node,NodeKind,NodeIdtypes with serde derivessrc/document.rs—Documentstruct and all impl methodssrc/traversal.rs—DescendantsIter,AncestorsItersrc/error.rs—DomErrorenumModify
crates/ie-dom/Cargo.toml— addserdeandserde_jsondependencies.Implementation
Error type (
error.rs)DomError::NodeNotFound(NodeId)— operation references a node that doesn't exist in the arenaDomError::CycleDetected—append_child/insert_beforewould make a node its own ancestorDomError::NotAChild—remove_childcalled with nodes that aren't in a parent-child relationshipthiserror::ErrorforDisplay/ErrorimplsNode types (
node.rs)NodeId,Node,NodeKindhere#[derive(Serialize, Deserialize)]onNodeKind#[derive(Serialize, Deserialize)]onNodeNode::is_element(&self) -> boolNode::is_text(&self) -> boolNode::element_name(&self) -> Option<&str>— returns tag name if ElementNode::text_content(&self) -> Option<&str>— returns text if Text or CommentNode creation methods on
Document(document.rs)create_element(tag: &str) -> NodeId:Node { kind: Element(tag.to_string()), parent: None, children: vec![], attributes: HashMap::new() }toself.nodescreate_text(text: &str) -> NodeId— same pattern withTextkindcreate_comment(text: &str) -> NodeId— same pattern withCommentkindTree mutation methods on
Document(document.rs)append_child(parent: NodeId, child: NodeId) -> Result<(), DomError>:parentandchildexist →NodeNotFoundif notchild != parent→CycleDetectedif self-loopparent— ifchildis found among them →CycleDetectedchildalready has a parent, remove it from old parent's children vecchild.parent = Some(parent)childtoparent.childrenremove_child(parent: NodeId, child: NodeId) -> Result<(), DomError>:NodeNotFoundchild.parent == Some(parent)→NotAChildif notchildfromparent.childrenvecchild.parent = Noneinsert_before(parent: NodeId, new_child: NodeId, reference: NodeId) -> Result<(), DomError>:NodeNotFoundreference.parent == Some(parent)→NotAChildif reference is not a child of parentappend_childnew_childhas existing parent, detachreferenceinparent.children, insertnew_childat that indexnew_child.parent = Some(parent)Accessor methods on
Document(document.rs)node(&self, id: NodeId) -> Option<&Node>—self.nodes.get(id)node_mut(&mut self, id: NodeId) -> Option<&mut Node>—self.nodes.get_mut(id)parent(&self, id: NodeId) -> Option<NodeId>—self.node(id)?.parentchildren(&self, id: NodeId) -> &[NodeId]— return&node.childrenor&[]if not foundget_attribute(&self, id: NodeId, name: &str) -> Option<&str>— look up in node'sattributesHashMapset_attribute(&mut self, id: NodeId, name: &str, value: &str)— insert intoattributesHashMap (no-op if node not found)Traversal iterators (
traversal.rs)DescendantsIter<'a>:&'a Documentand aVec<NodeId>stack (explicit, no recursion)next(): pop from stack, push current node's children in reverse order, return currentAncestorsIter<'a>:&'a Documentandcurrent: Option<NodeId>node.parentof the starting nodenext(): return current, advance to current's parentDocument::descendants(&self, id: NodeId) -> DescendantsIterDocument::ancestors(&self, id: NodeId) -> AncestorsIterQuery methods on
Document(document.rs)get_elements_by_tag_name(&self, root: NodeId, tag: &str) -> Vec<NodeId>:descendants(root)iteratornode.kindisElement(name)wherename == tagget_element_by_id(&self, root: NodeId, id: &str) -> Option<NodeId>:descendants(root)iteratorattributes.get("id") == Some(id)Some(node_id)orNoneKnown limitation: arena memory
remove_childdetaches nodes from the tree but does not free their arena slots.NodeIds are indices intoVec<Node>, and removed nodes remain allocated in the vector. Theirparentis set toNoneand they are removed from their parent'schildrenvec, but theNodestruct itself stays inself.nodes.This is acceptable for Phase 1. Document this as a known limitation in code comments.
Future optimization: Use
slotmapor a generational arena with a free list for slot reuse. This would allowremove_childto actually reclaim memory and preventNodeIdreuse bugs (generational indices detect use-after-free).Document::node_count(&self) -> usize— returnsself.nodes.len()(total allocated slots, including detached nodes)Document::live_node_count(&self) -> usize— counts nodes that have at least one reference: nodes that have a parent OR are the root node. This gives the count of nodes actually in the tree, excluding orphaned/detached nodes.Tests
All in
crates/ie-dom/src/as#[cfg(test)] mod testsblocks in the relevant modules.Document & creation tests
new()creates doc with root node of kindDocumentcreate_element("div")→ node exists, kind isElement("div")create_text("hello")→ kind isText("hello")create_comment("note")→ kind isComment("note")Tree mutation tests
append_child: parent's children contains child, child's parent is parentappend_childtwice to same parent: both are children, in orderappend_childreparenting: child with existing parent moves — removed from old parent's children, old parent has fewer childrenremove_child: child removed from parent's children, child's parent is Noneremove_childwith wrong parent →NotAChilderrorremove_childwith non-existent ids →NodeNotFoundinsert_before: new_child appears before reference in parent's childreninsert_beforewith reference not a child of parent →NotAChildinsert_beforereparenting: detaches from old parent firstCycle detection tests
append_child(node, node)(self-loop) →CycleDetectedappend_child(C, A)→CycleDetectedappend_child(root, root)→CycleDetectedCycleDetectedAccessor tests
node()returnsSomefor valid id,Nonefor out-of-boundsset_attributethenget_attributeround-tripsget_attributeon non-existent attribute →Nonechildren()returns correct slicechildren()on non-existent node → empty sliceTraversal tests
descendants(root)yields A, C, D, B, E (depth-first pre-order)descendantson leaf node → empty iteratordescendantson node with one child → yields just that child (and its subtree)ancestors(E)yields B, rootancestors(root)→ empty iteratorQuery tests
get_elements_by_tag_name(root, "div")finds all divs, ignores spansget_elements_by_tag_namewith no matches → empty vecget_element_by_id(root, "main")finds node withid="main"attributeget_element_by_idwith no match → Noneget_element_by_idwith multiple matches → returns first in tree orderArena memory tests
node_count()after creating 5 nodes equals 6 (root + 5)live_node_count()after attaching 3 of 5 nodes to root equals 4 (root + 3)remove_child:node_count()unchanged,live_node_count()decrementedSerialization tests
Documentto JSON via serde_json, deserialize back, verify tree structure matchesAcceptance Criteria
cargo test -p ie-dom— all tests passcargo clippy -p ie-dom -- -D warnings— no warningscargo fmt -p ie-dom --check— formatted