Repository: niklak/dom_query
Pull request #98: Feature/validation update
Author: login: niklak
Labels: name: enhancement
description: New feature or request
Created: 2025-04-25T12:11:51Z | Merged: 2025-04-25T17:16:51Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added comprehensive tree validation to ensure structural integrity, including checks for root uniqueness, valid node references, and cycle detection.
  - Introduced a new method to verify the validity of node IDs before performing tree manipulations.

- **Bug Fixes**
  - Improved safety of HTML and node operations by preventing actions on invalid node IDs.
  - Ensured child nodes are properly detached from previous parents before reattachment to avoid lingering references.

- **Tests**
  - Added a new test for setting HTML content to empty and validating tree structure.
  - Enhanced test readability with clearer formatting.
  - Removed a deprecated test related to DOM rendering bugs.

- **Style**
  - Improved code and test formatting for better readability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index e36c0e1..f85685c 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -18,7 +18,7 @@ jobs:
       - name: Install cargo-llvm-cov
         uses: taiki-e/install-action@cargo-llvm-cov
       - name: Generate code coverage
-        run: cargo llvm-cov --ignore-filename-regex css.rs --all-features --workspace --lcov --output-path lcov.info
+        run: cargo llvm-cov --ignore-filename-regex "css.rs|validation.rs" --all-features --workspace --lcov --output-path lcov.info
       - name: Upload coverage to Codecov
         uses: codecov/codecov-action@v3
         with:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 639b31a..c2e0cae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,8 @@ All notable changes to the `dom_query` crate will be documented in this file.
 ## [Unreleased]
 
 ## Added
-- Introduced `NodeRef::wrap_node`,`NodeRef::wrap_html`, and `NodeRef::unwrap_node` methods, allowing a node to be wrapped with another node or HTML fragment, and unwrapped. (by @phayes).
+- Introduced `NodeRef::wrap_node`, `NodeRef::wrap_html`, and `NodeRef::unwrap_node` methods, allowing a node to be wrapped with another node or HTML fragment, and unwrapped (by @phayes).
+- Introduced `Tree::validate`, a method for performing comprehensive integrity checks on node relationships, links, and cycles within the DOM tree (by @phayes).
 
 ### Changed
 - Updated `selectors` dependency to version 0.27.0.
@@ -13,6 +14,7 @@ All notable changes to the `dom_query` crate will be documented in this file.
 - Updated `html5ever` dependency to version 0.31.0.
 - Improved `mini_selector::Attribute`: attribute values can now be enclosed in either double or single quotes, or left unquoted.
 - Changed `entities::Attr` visibility to `pub`.
+- `TreeNodeOps::append_child_of` and `TreeNodeOps::prepend_child_of` now internally call `TreeNodeOps::remove_from_parent` on the new child to ensure it is safely detached from the tree before being reattached. This guarantees that the node has no lingering references (parent or siblings).
 
 ### Fixed
 - Fixed `template` serialization in `NodeRef::html` and `NodeRef::inner_html` methods.
diff --git a/src/dom_tree.rs b/src/dom_tree.rs
index c8063ec..aaa3025 100644
--- a/src/dom_tree.rs
+++ b/src/dom_tree.rs
@@ -2,6 +2,7 @@ mod helpers;
 mod ops;
 mod traversal;
 mod tree;
+mod validation;
 
 pub use ops::TreeNodeOps;
 pub use traversal::Traversal;
diff --git a/src/dom_tree/ops.rs b/src/dom_tree/ops.rs
index d4d039c..fc50214 100644
--- a/src/dom_tree/ops.rs
+++ b/src/dom_tree/ops.rs
@@ -158,6 +158,10 @@ impl TreeNodeOps {
         }
         None
     }
+    /// Checks if the given node id is valid in the tree.
+    pub fn is_valid_node_id(nodes: &[TreeNode], id: &NodeId) -> bool {
+        nodes.get(id.value).map_or(false, |node| node.id == *id)
+    }
 }
 
 // manipulation
@@ -196,6 +200,7 @@ impl TreeNodeOps {
 
     /// Appends a child node by `new_child_id` to a node by `id`. `new_child_id` must exist in the tree.
     pub fn append_child_of(nodes: &mut [TreeNode], id: &NodeId, new_child_id: &NodeId) {
+        Self::remove_from_parent(nodes, new_child_id);
         let Some(parent) = nodes.get_mut(id.value) else {
             // TODO: panic or not?
             return;
@@ -225,6 +230,7 @@ impl TreeNodeOps {
 
     /// Prepend a child node by `new_child_id` to a node by `id`. `new_child_id` must exist in the tree.
     pub fn prepend_child_of(nodes: &mut [TreeNode], id: &NodeId, new_child_id: &NodeId) {
+        Self::remove_from_parent(nodes, new_child_id);
         let Some(parent) = nodes.get_mut(id.value) else {
             // TODO: panic or not?
             return;
@@ -335,12 +341,11 @@ impl TreeNodeOps {
     }
 
     /// Appends another node and it's siblings to the selected node.
-    pub fn append_children_of(nodes: &mut [TreeNode], id: &NodeId, new_node_id: &NodeId) {
-        let mut next_node_id = Some(new_node_id).copied();
+    pub fn append_children_of(nodes: &mut [TreeNode], id: &NodeId, new_child_id: &NodeId) {
+        let mut next_node_id = Some(new_child_id).copied();
 
         while let Some(node_id) = next_node_id {
             next_node_id = nodes.get(node_id.value).and_then(|n| n.next_sibling);
-            Self::remove_from_parent(nodes, &node_id);
             Self::append_child_of(nodes, id, &node_id);
         }
     }
diff --git a/src/dom_tree/tree.rs b/src/dom_tree/tree.rs
index e4b4e39..0d013fc 100644
--- a/src/dom_tree/tree.rs
+++ b/src/dom_tree/tree.rs
@@ -456,190 +456,7 @@ impl Tree {
     }
 }
 
-
-// Validation methods
-impl Tree {
-    /// Validates the structural integrity of the tree.
-    ///
-    /// This function checks for:
-    /// 
-    /// 1. **Root Uniqueness**:
-    ///    - Ensures exactly one root node exists (`parent == None`) and it is `NodeId(0)`.
-    ///
-    /// 2. **Parent-Child Link Consistency**:
-    ///    - `first_child.parent == Self`
-    ///    - `first_child.prev_sibling == None`
-    ///    - `last_child.parent == Self`
-    ///    - `last_child.next_sibling == None`
-    ///    - All children in the chain from `first_child` to `last_child` have correct `parent`, `prev_sibling`, and `next_sibling` references.
-    ///
-    /// 3. **Sibling Link Consistency**:
-    ///    - `prev_sibling.next_sibling == Self`
-    ///    - `next_sibling.prev_sibling == Self`
-    ///
-    /// 4. **Valid Node References**:
-    ///    - All `NodeId` references must point to valid nodes within the tree.
-    /// 
-    /// 5. **Orphaned Nodes** (when `check_orphans` is `true`):
-    ///   - Nodes with `parent == None` that are not the root node are considered orphaned.
-    /// 
-    /// 6. **Cycle Detection**:
-    ///    - No cycles in parent chains (traverse `parent` links).
-    ///    - No cycles in sibling chains (traverse `next_sibling` links).
-    /// 
-    /// Parameters:
-    /// - `check_orphans`: If `true`, check for orphaned nodes (nodes with `parent == None` that are not the root).
-    /// 
-    /// Returns:
-    /// - `Ok(())` if the tree structure is valid.
-    /// - `Err(String)` with a descriptive message if any inconsistency or cycle is detected.
-    ///
-    /// Orphaned nodes (non-root nodes with `parent == None`) are allowed and not considered invalid.
-    pub fn validate(&self) -> Result<(), String> {
-        let nodes = self.nodes.borrow();
-
-        // Root uniqueness check
-        let root_node = nodes.first().ok_or_else(|| "Root node (NodeId(0)) missing".to_string())?;
-        if root_node.parent.is_some() {
-            return Err("Root node (NodeId(0)) must have no parent".to_string());
-        }
-
-        // Node parent-child / sibling link consistency checks
-        for node in nodes.iter() {
-            let id = node.id;
-
-            // Validate self-cycles
-            if node.parent == Some(id) {
-                return Err(format!("Node {:?} references itself as parent", id));
-            }
-            if node.prev_sibling == Some(id) {
-                return Err(format!("Node {:?} references itself as prev_sibling", id));
-            }
-            if node.next_sibling == Some(id) {
-                return Err(format!("Node {:?} references itself as next_sibling", id));
-            }
-            if node.first_child == Some(id) {
-                return Err(format!("Node {:?} references itself as first_child", id));
-            }
-            if node.last_child == Some(id) {
-                return Err(format!("Node {:?} references itself as last_child", id));
-            }
-
-            // Validate first_child linkage
-            if let Some(first_child_id) = node.first_child {
-                let first_child = nodes.get(first_child_id.value)
-                    .ok_or_else(|| format!("Invalid first_child reference at node {:?}", id))?;
-
-                if first_child.parent != Some(id) {
-                    return Err(format!("first_child's parent does not match node {:?}", id));
-                }
-
-                if first_child.prev_sibling.is_some() {
-                    return Err(format!("first_child {:?} of node {:?} has non-None prev_sibling", first_child_id, id));
-                }
-
-                // Validate child chain
-                let mut current_id = Some(first_child_id);
-                let mut last_seen = None;
-                while let Some(cid) = current_id {
-                    let current = nodes.get(cid.value)
-                        .ok_or_else(|| format!("Invalid child reference at node {:?}", cid))?;
-
-                    if current.parent != Some(id) {
-                        return Err(format!("Child {:?} has incorrect parent, expected {:?}", cid, id));
-                    }
-
-                    if let Some(prev) = current.prev_sibling {
-                        let prev_node = nodes.get(prev.value)
-                            .ok_or_else(|| format!("Invalid prev_sibling reference at node {:?}", prev))?;
-                        if prev_node.next_sibling != Some(cid) {
-                            return Err(format!("prev_sibling {:?} next_sibling mismatch with {:?}", prev, cid));
-                        }
-                    }
-
-                    if let Some(next) = current.next_sibling {
-                        let next_node = nodes.get(next.value)
-                            .ok_or_else(|| format!("Invalid next_sibling reference at node {:?}", next))?;
-                        if next_node.prev_sibling != Some(cid) {
-                            return Err(format!("next_sibling {:?} prev_sibling mismatch with {:?}", next, cid));
-                        }
-                        current_id = Some(next);
-                    } else {
-                        last_seen = Some(cid);
-                        current_id = None;
-                    }
-                }
-
-                if node.last_child != last_seen {
-                    return Err(format!("last_child mismatch at node {:?}, expected {:?}, found {:?}", id, node.last_child, last_seen));
-                }
-            }
-
-            // Validate last_child linkage
-            if let Some(last_child_id) = node.last_child {
-                let last_child = nodes.get(last_child_id.value)
-                    .ok_or_else(|| format!("Invalid last_child reference at node {:?}", id))?;
-
-                if last_child.parent != Some(id) {
-                    return Err(format!("last_child's parent does not match node {:?}", id));
-                }
-
-                if last_child.next_sibling.is_some() {
-                    return Err(format!("last_child {:?} of node {:?} has non-None next_sibling", last_child_id, id));
-                }
-            }
-
-            // Validate sibling links
-            if let Some(prev_sibling_id) = node.prev_sibling {
-                let prev_sibling = nodes.get(prev_sibling_id.value)
-                    .ok_or_else(|| format!("Invalid prev_sibling reference at node {:?}", id))?;
-
-                if prev_sibling.next_sibling != Some(id) {
-                    return Err(format!("prev_sibling {:?} does not link back to {:?}", prev_sibling_id, id));
-                }
-            }
-
-            if let Some(next_sibling_id) = node.next_sibling {
-                let next_sibling = nodes.get(next_sibling_id.value)
-                    .ok_or_else(|| format!("Invalid next_sibling reference at node {:?}", id))?;
-
-                if next_sibling.prev_sibling != Some(id) {
-                    return Err(format!("next_sibling {:?} does not link back to {:?}", next_sibling_id, id));
-                }
-            }
-        }
-
-        // Cycle detection: Parent chains
-        for node in nodes.iter() {
-            let mut visited = std::collections::HashSet::new();
-            let mut current = Some(node.id);
-            while let Some(cid) = current {
-                if !visited.insert(cid.value) {
-                    return Err(format!("Cycle detected in parent chain at node {:?}", cid));
-                }
-                current = nodes.get(cid.value).and_then(|n| n.parent);
-            }
-        }
-
-        // Cycle detection: Sibling chains
-        for node in nodes.iter() {
-            if let Some(first_child_id) = node.first_child {
-                let mut visited = std::collections::HashSet::new();
-                let mut current = Some(first_child_id);
-                while let Some(cid) = current {
-                    if !visited.insert(cid.value) {
-                        return Err(format!("Cycle detected in sibling chain at node {:?}", cid));
-                    }
-                    current = nodes.get(cid.value).and_then(|n| n.next_sibling);
-                }
-            }
-        }
-
-        Ok(())
-    }
-
-}
-
+#[rustfmt::skip]
 #[cfg(test)]
 mod tests {
     use crate::Document;
@@ -670,7 +487,7 @@ mod tests {
         assert!(tree.get(&NodeId::new(total_nodes)).is_none());
 
         let validity_check = tree.validate();
-        assert!(validity_check.is_ok(), "Tree is not valid: {}", validity_check.unwrap_err());
+        assert!(validity_check.is_ok(),"Tree is not valid: {}",validity_check.unwrap_err());
     }
 
     #[test]
@@ -690,7 +507,7 @@ mod tests {
         assert!(elder_node.is_document());
 
         let validity_check = tree.validate();
-        assert!(validity_check.is_ok(), "Tree is not valid: {}", validity_check.unwrap_err());
+        assert!(validity_check.is_ok(),"Tree is not valid: {}",validity_check.unwrap_err());
     }
 
     #[test]
@@ -707,7 +524,7 @@ mod tests {
         assert!(!doc.select("#first-child, #last-child").exists());
 
         let validity_check = tree.validate();
-        assert!(validity_check.is_ok(), "Tree is not valid: {}", validity_check.unwrap_err());
+        assert!(validity_check.is_ok(),"Tree is not valid: {}",validity_check.unwrap_err());
     }
 
     #[test]
@@ -727,7 +544,7 @@ mod tests {
             .exists());
 
         let validity_check = tree.validate();
-        assert!(validity_check.is_ok(), "Tree is not valid: {}", validity_check.unwrap_err());
+        assert!(validity_check.is_ok(),"Tree is not valid: {}",validity_check.unwrap_err());
     }
 
     #[allow(deprecated)]
@@ -748,7 +565,7 @@ mod tests {
             .exists());
 
         let validity_check = tree.validate();
-        assert!(validity_check.is_ok(), "Tree is not valid: {}", validity_check.unwrap_err());
+        assert!(validity_check.is_ok(),"Tree is not valid: {}",validity_check.unwrap_err());
     }
 
     #[test]
@@ -760,7 +577,15 @@ mod tests {
                 TreeNode {
                     id: NodeId::new(1),
                     parent: None, // Orphaned node allowed
-                    ..TreeNode::new(NodeId::new(1), NodeData::Element(Element::new(QualName::new(None, ns!(), LocalName::from("div")), vec![], None, false)))
+                    ..TreeNode::new(
+                        NodeId::new(1),
+                        NodeData::Element(Element::new(
+                            QualName::new(None, ns!(), LocalName::from("div")),
+                            vec![],
+                            None,
+                            false,
+                        )),
+                    )
                 },
             ]),
         };
@@ -770,15 +595,27 @@ mod tests {
 
         let result = tree.validate();
         assert!(result.is_err());
-        assert!(result.unwrap_err().contains("Root node (NodeId(0)) must have no parent"));
+        assert!(result
+            .unwrap_err()
+            .contains("Root node (NodeId(0)) must have no parent"));
     }
 
     #[test]
     fn test_invalid_sibling_link() {
         use super::*;
         let tree = Tree::new(NodeData::Document);
-        let child1_id = tree.create_node(NodeData::Element(Element::new(QualName::new(None, ns!(), LocalName::from("p")), vec![], None, false)));
-        let child2_id = tree.create_node(NodeData::Element(Element::new(QualName::new(None, ns!(), LocalName::from("p")), vec![], None, false)));
+        let child1_id = tree.create_node(NodeData::Element(Element::new(
+            QualName::new(None, ns!(), LocalName::from("p")),
+            vec![],
+            None,
+            false,
+        )));
+        let child2_id = tree.create_node(NodeData::Element(Element::new(
+            QualName::new(None, ns!(), LocalName::from("p")),
+            vec![],
+            None,
+            false,
+        )));
 
         // Add incorrect sibling links
         {
@@ -811,12 +648,18 @@ mod tests {
 
         // Create two orphan nodes that point to each other as parent
         let tree = Tree::new(NodeData::Document);
-        let x_id = tree.create_node(NodeData::Element(
-            Element::new(QualName::new(None, ns!(), LocalName::from("div")), Vec::new(), None, false),
-        ));
-        let y_id = tree.create_node(NodeData::Element(
-            Element::new(QualName::new(None, ns!(), LocalName::from("span")), Vec::new(), None, false),
-        ));
+        let x_id = tree.create_node(NodeData::Element(Element::new(
+            QualName::new(None, ns!(), LocalName::from("div")),
+            Vec::new(),
+            None,
+            false,
+        )));
+        let y_id = tree.create_node(NodeData::Element(Element::new(
+            QualName::new(None, ns!(), LocalName::from("span")),
+            Vec::new(),
+            None,
+            false,
+        )));
         {
             let mut nodes = tree.nodes.borrow_mut();
             nodes[x_id.value].parent = Some(y_id);
@@ -825,5 +668,4 @@ mod tests {
         let err = tree.validate().unwrap_err();
         assert!(err.contains("Cycle detected"));
     }
-
 }
diff --git a/src/dom_tree/validation.rs b/src/dom_tree/validation.rs
new file mode 100644
index 0000000..f582ca2
--- /dev/null
+++ b/src/dom_tree/validation.rs
@@ -0,0 +1,214 @@
+// Validation methods
+use super::tree::Tree;
+
+impl Tree {
+    /// Validates the structural integrity of the tree.
+    ///
+    /// This function checks for:
+    ///
+    /// 1. **Root Uniqueness**:
+    ///    - Ensures exactly one root node exists (`parent == None`) and it is `NodeId(0)`.
+    ///
+    /// 2. **Parent-Child Link Consistency**:
+    ///    - `first_child.parent == Self`
+    ///    - `first_child.prev_sibling == None`
+    ///    - `last_child.parent == Self`
+    ///    - `last_child.next_sibling == None`
+    ///    - All children in the chain from `first_child` to `last_child` have correct `parent`, `prev_sibling`, and `next_sibling` references.
+    ///
+    /// 3. **Sibling Link Consistency**:
+    ///    - `prev_sibling.next_sibling == Self`
+    ///    - `next_sibling.prev_sibling == Self`
+    ///
+    /// 4. **Valid Node References**:
+    ///    - All `NodeId` references must point to valid nodes within the tree.
+    ///
+    ///
+    /// 5. **Cycle Detection**:
+    ///    - No cycles in parent chains (traverse `parent` links).
+    ///    - No cycles in sibling chains (traverse `next_sibling` links).
+    ///
+    /// Returns:
+    /// - `Ok(())` if the tree structure is valid.
+    /// - `Err(String)` with a descriptive message if any inconsistency or cycle is detected.
+    ///
+    /// Orphaned nodes (non-root nodes with `parent == None`) are allowed and not considered invalid.
+    pub fn validate(&self) -> Result<(), String> {
+        let nodes = self.nodes.borrow();
+
+        // Root uniqueness check
+        let root_node = nodes
+            .first()
+            .ok_or_else(|| "Root node (NodeId(0)) missing".to_string())?;
+        if root_node.parent.is_some() {
+            return Err("Root node (NodeId(0)) must have no parent".to_string());
+        }
+
+        // Node parent-child / sibling link consistency checks
+        for node in nodes.iter() {
+            let id = node.id;
+
+            // Validate self-cycles
+            if node.parent == Some(id) {
+                return Err(format!("Node {:?} references itself as parent", id));
+            }
+            if node.prev_sibling == Some(id) {
+                return Err(format!("Node {:?} references itself as prev_sibling", id));
+            }
+            if node.next_sibling == Some(id) {
+                return Err(format!("Node {:?} references itself as next_sibling", id));
+            }
+            if node.first_child == Some(id) {
+                return Err(format!("Node {:?} references itself as first_child", id));
+            }
+            if node.last_child == Some(id) {
+                return Err(format!("Node {:?} references itself as last_child", id));
+            }
+
+            // Validate first_child linkage
+            if let Some(first_child_id) = node.first_child {
+                let first_child = nodes.get(first_child_id.value).ok_or_else(|| {
+                    format!(
+                        "Invalid first_child {:?} reference at node {:?}",
+                        first_child_id, id
+                    )
+                })?;
+
+                if first_child.parent != Some(id) {
+                    return Err(format!("first_child's parent does not match node {:?}", id));
+                }
+
+                if first_child.prev_sibling.is_some() {
+                    return Err(format!(
+                        "first_child {:?} of node {:?} has non-None prev_sibling",
+                        first_child_id, id
+                    ));
+                }
+
+                // Validate child chain
+                let mut current_id = Some(first_child_id);
+                let mut last_seen = None;
+                while let Some(cid) = current_id {
+                    let current = nodes
+                        .get(cid.value)
+                        .ok_or_else(|| format!("Invalid child reference at node {:?}", cid))?;
+
+                    if current.parent != Some(id) {
+                        return Err(format!(
+                            "Child {:?} has incorrect parent, expected {:?}",
+                            cid, id
+                        ));
+                    }
+
+                    if let Some(prev) = current.prev_sibling {
+                        let prev_node = nodes.get(prev.value).ok_or_else(|| {
+                            format!("Invalid prev_sibling reference at node {:?}", prev)
+                        })?;
+                        if prev_node.next_sibling != Some(cid) {
+                            return Err(format!(
+                                "prev_sibling {:?} next_sibling mismatch with {:?}",
+                                prev, cid
+                            ));
+                        }
+                    }
+
+                    if let Some(next) = current.next_sibling {
+                        let next_node = nodes.get(next.value).ok_or_else(|| {
+                            format!("Invalid next_sibling reference at node {:?}", next)
+                        })?;
+                        if next_node.prev_sibling != Some(cid) {
+                            return Err(format!(
+                                "next_sibling {:?} prev_sibling mismatch with {:?}",
+                                next, cid
+                            ));
+                        }
+                        current_id = Some(next);
+                    } else {
+                        last_seen = Some(cid);
+                        current_id = None;
+                    }
+                }
+
+                if node.last_child != last_seen {
+                    return Err(format!(
+                        "last_child mismatch at node {:?}, expected {:?}, found {:?}",
+                        id, node.last_child, last_seen
+                    ));
+                }
+            }
+
+            // Validate last_child linkage
+            if let Some(last_child_id) = node.last_child {
+                let last_child = nodes
+                    .get(last_child_id.value)
+                    .ok_or_else(|| format!("Invalid last_child reference at node {:?}", id))?;
+
+                if last_child.parent != Some(id) {
+                    return Err(format!("last_child's parent does not match node {:?}", id));
+                }
+
+                if last_child.next_sibling.is_some() {
+                    return Err(format!(
+                        "last_child {:?} of node {:?} has non-None next_sibling",
+                        last_child_id, id
+                    ));
+                }
+            }
+
+            // Validate sibling links
+            if let Some(prev_sibling_id) = node.prev_sibling {
+                let prev_sibling = nodes
+                    .get(prev_sibling_id.value)
+                    .ok_or_else(|| format!("Invalid prev_sibling reference at node {:?}", id))?;
+
+                if prev_sibling.next_sibling != Some(id) {
+                    return Err(format!(
+                        "prev_sibling {:?} does not link back to {:?}",
+                        prev_sibling_id, id
+                    ));
+                }
+            }
+
+            if let Some(next_sibling_id) = node.next_sibling {
+                let next_sibling = nodes
+                    .get(next_sibling_id.value)
+                    .ok_or_else(|| format!("Invalid next_sibling reference at node {:?}", id))?;
+
+                if next_sibling.prev_sibling != Some(id) {
+                    return Err(format!(
+                        "next_sibling {:?} does not link back to {:?}",
+                        next_sibling_id, id
+                    ));
+                }
+            }
+        }
+
+        // Cycle detection: Parent chains
+        for node in nodes.iter() {
+            let mut visited = std::collections::HashSet::new();
+            let mut current = Some(node.id);
+            while let Some(cid) = current {
+                if !visited.insert(cid.value) {
+                    return Err(format!("Cycle detected in parent chain at node {:?}", cid));
+                }
+                current = nodes.get(cid.value).and_then(|n| n.parent);
+            }
+        }
+
+        // Cycle detection: Sibling chains
+        for node in nodes.iter() {
+            if let Some(first_child_id) = node.first_child {
+                let mut visited = std::collections::HashSet::new();
+                let mut current = Some(first_child_id);
+                while let Some(cid) = current {
+                    if !visited.insert(cid.value) {
+                        return Err(format!("Cycle detected in sibling chain at node {:?}", cid));
+                    }
+                    current = nodes.get(cid.value).and_then(|n| n.next_sibling);
+                }
+            }
+        }
+
+        Ok(())
+    }
+}
diff --git a/src/node/node_ref.rs b/src/node/node_ref.rs
index 3e60ba8..1d1c79b 100644
--- a/src/node/node_ref.rs
+++ b/src/node/node_ref.rs
@@ -209,7 +209,6 @@ impl NodeRef<'_> {
     pub fn append_child<P: NodeIdProver>(&self, id_provider: P) {
         let new_child_id = id_provider.node_id();
         let mut nodes = self.tree.nodes.borrow_mut();
-        TreeNodeOps::remove_from_parent(nodes.deref_mut(), new_child_id);
         TreeNodeOps::append_child_of(nodes.deref_mut(), &self.id, new_child_id);
     }
 
@@ -354,7 +353,9 @@ impl NodeRef<'_> {
             &mut borrowed_nodes,
             fragment.tree,
             |tree_nodes, new_node_id| {
-                f(tree_nodes, new_node_id, self);
+                if TreeNodeOps::is_valid_node_id(tree_nodes, &new_node_id) {
+                    f(tree_nodes, new_node_id, self);
+                }
             },
         );
     }
@@ -367,14 +368,14 @@ impl NodeRef<'_> {
 
         // Insert wrapper before self in the parent
         TreeNodeOps::insert_before_of(&mut nodes, &self.id, wrapper_id);
-
         // Move self into wrapper as the only child
-        TreeNodeOps::remove_from_parent(&mut nodes, &self.id);
         TreeNodeOps::append_child_of(&mut nodes, wrapper_id, &self.id);
     }
 
     /// Wraps the current node with the given HTML fragment.
     /// The outermost node of the fragment becomes the new parent of the current node.
+    ///
+    /// **Important:** The HTML fragment must be a **one**, valid HTML element.
     pub fn wrap_html<T>(&self, html: T)
     where
         T: Into<StrTendril>,
@@ -382,8 +383,6 @@ impl NodeRef<'_> {
         self.merge_html_with_fn(html, |tree_nodes, wrapper_id, node| {
             // Insert wrapper before the node
             TreeNodeOps::insert_before_of(tree_nodes, &node.id, &wrapper_id);
-            // Remove node from current parent
-            TreeNodeOps::remove_from_parent(tree_nodes, &node.id);
             // Append node into wrapper
             TreeNodeOps::append_child_of(tree_nodes, &wrapper_id, &node.id);
         });
diff --git a/src/selection.rs b/src/selection.rs
index 2ff4ae6..07f8d66 100644
--- a/src/selection.rs
+++ b/src/selection.rs
@@ -493,7 +493,9 @@ impl Selection<'_> {
     pub fn set_html<T: Into<StrTendril>>(&self, html: T) {
         self.merge_html_with_fn(html, |tree_nodes, new_node_id, node| {
             TreeNodeOps::reparent_children_of(tree_nodes, &node.id, None);
-            TreeNodeOps::append_children_of(tree_nodes, &node.id, &new_node_id);
+            if TreeNodeOps::is_valid_node_id(tree_nodes, &new_node_id) {
+                TreeNodeOps::append_children_of(tree_nodes, &node.id, &new_node_id);
+            }
         });
     }
 
@@ -503,7 +505,9 @@ impl Selection<'_> {
     /// This follows the same rules as `append`.
     pub fn replace_with_html<T: Into<StrTendril>>(&self, html: T) {
         self.merge_html_with_fn(html, |tree_nodes, new_node_id, node| {
-            TreeNodeOps::insert_siblings_before(tree_nodes, &node.id, &new_node_id);
+            if TreeNodeOps::is_valid_node_id(tree_nodes, &new_node_id) {
+                TreeNodeOps::insert_siblings_before(tree_nodes, &node.id, &new_node_id);
+            }
             TreeNodeOps::remove_from_parent(tree_nodes, &node.id);
         });
     }
@@ -511,28 +515,36 @@ impl Selection<'_> {
     /// Parses the html and appends it to the set of matched elements.
     pub fn append_html<T: Into<StrTendril>>(&self, html: T) {
         self.merge_html_with_fn(html, |tree_nodes, new_node_id, node| {
-            TreeNodeOps::append_children_of(tree_nodes, &node.id, &new_node_id);
+            if TreeNodeOps::is_valid_node_id(tree_nodes, &new_node_id) {
+                TreeNodeOps::append_children_of(tree_nodes, &node.id, &new_node_id);
+            }
         });
     }
 
     /// Parses the html and prepends it to the set of matched elements.
     pub fn prepend_html<T: Into<StrTendril>>(&self, html: T) {
         self.merge_html_with_fn(html, |tree_nodes, new_node_id, node| {
-            TreeNodeOps::prepend_children_of(tree_nodes, &node.id, &new_node_id);
+            if TreeNodeOps::is_valid_node_id(tree_nodes, &new_node_id) {
+                TreeNodeOps::prepend_children_of(tree_nodes, &node.id, &new_node_id);
+            }
         });
     }
 
     /// Parses the html and inserts it before the set of matched elements.
     pub fn before_html<T: Into<StrTendril>>(&self, html: T) {
         self.merge_html_with_fn(html, |tree_nodes, new_node_id, node| {
-            TreeNodeOps::insert_siblings_before(tree_nodes, &node.id, &new_node_id);
+            if TreeNodeOps::is_valid_node_id(tree_nodes, &new_node_id) {
+                TreeNodeOps::insert_siblings_before(tree_nodes, &node.id, &new_node_id);
+            }
         });
     }
 
     /// Parses the html and inserts it after the set of matched elements.
     pub fn after_html<T: Into<StrTendril>>(&self, html: T) {
         self.merge_html_with_fn(html, |tree_nodes, new_node_id, node| {
-            TreeNodeOps::insert_siblings_after(tree_nodes, &node.id, &new_node_id);
+            if TreeNodeOps::is_valid_node_id(tree_nodes, &new_node_id) {
+                TreeNodeOps::insert_siblings_after(tree_nodes, &node.id, &new_node_id);
+            }
         });
     }
 
diff --git a/tests/html-dom.rs b/tests/html-dom.rs
index 4aa0e70..cbc8245 100644
--- a/tests/html-dom.rs
+++ b/tests/html-dom.rs
@@ -15,7 +15,11 @@ fn parse_and_serialize(input: StrTendril) -> StrTendril {
     let dom = Document::fragment(input);
 
     let validity_check = dom.tree.validate();
-    assert!(validity_check.is_ok(), "Tree is not valid: {}", validity_check.unwrap_err());
+    assert!(
+        validity_check.is_ok(),
+        "Tree is not valid: {}",
+        validity_check.unwrap_err()
+    );
 
     let root = dom.root();
     let inner: SerializableNodeRef = root.first_child().unwrap().into();
@@ -170,37 +174,9 @@ fn doctype() {
     assert_eq!(String::from_utf8(result).unwrap(), "<!DOCTYPE html>");
 
     let validity_check = dom.tree.validate();
-    assert!(validity_check.is_ok(), "Tree is not valid: {}", validity_check.unwrap_err());
+    assert!(
+        validity_check.is_ok(),
+        "Tree is not valid: {}",
+        validity_check.unwrap_err()
+    );
 }
-
-#[cfg_attr(not(target_arch = "wasm32"), test)]
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
-fn doc_render_bug() {
-    const HTML: &str = r#"<!DOCTYPE html><html><head></head><body>
-           <div id="parent">
-               <div id="child" class="child">Child</div>
-            </div>
-        </body></html>"#;
-
-    let doc = Document::from(HTML);
-
-    let sel = doc.select("#child");
-    let child = sel.nodes().first().unwrap();
-
-    // Create a wrapper directly in the same tree
-    let wrapper = doc.tree.new_element("div");
-    wrapper.set_attr("id", "wrapper");
-
-    // Insert wrapper before child in the parent
-    doc.tree.insert_before_of(&child.id, &wrapper.id);
-
-    // Move child into wrapper as the only child
-    doc.tree.append_child_of(&wrapper.id, &child.id);
-
-    // Normalize the document for printing
-    doc.normalize();
-
-    // Check to see if the structure is valid
-    let validity_check = doc.tree.validate();
-    assert!(validity_check.is_ok(), "Tree is not valid: {}", validity_check.unwrap_err());
-}
\ No newline at end of file
diff --git a/tests/node-manipulation.rs b/tests/node-manipulation.rs
index bde29f9..c7ebfb9 100644
--- a/tests/node-manipulation.rs
+++ b/tests/node-manipulation.rs
@@ -1,6 +1,6 @@
 mod data;
 
-use data::{ANCESTORS_CONTENTS, REPLACEMENT_CONTENTS};
+use data::{doc_with_siblings, ANCESTORS_CONTENTS, REPLACEMENT_CONTENTS};
 use dom_query::Document;
 
 #[cfg(target_arch = "wasm32")]
@@ -453,7 +453,7 @@ fn test_node_insert_after() {
     assert!(doc
         .select("#before-origin + #origin + #after-origin + #after-after-origin + #last")
         .exists());
-    doc.tree.validate().unwrap(); 
+    doc.tree.validate().unwrap();
 }
 
 #[cfg_attr(not(target_arch = "wasm32"), test)]
@@ -473,7 +473,7 @@ fn test_node_remove_descendants() {
                 .map(|el| el.set_attr("data-descendant", &i.to_string()))
         });
     }
-    doc.tree.validate().unwrap(); 
+    doc.tree.validate().unwrap();
 }
 
 #[cfg_attr(not(target_arch = "wasm32"), test)]
@@ -497,7 +497,7 @@ fn test_node_remove_descendants_it_panic() {
                 .map(|el| el.set_attr("data-descendant", &i.to_string()))
         });
     }
-    doc.tree.validate().unwrap(); 
+    doc.tree.validate().unwrap();
 }
 
 #[cfg_attr(not(target_arch = "wasm32"), test)]
@@ -542,7 +542,7 @@ fn test_node_normalize() {
 
     grand_node.normalize();
     assert_eq!(grand_node.children_it(false).count(), 0);
-    doc.tree.validate().unwrap(); 
+    doc.tree.validate().unwrap();
 }
 
 #[cfg_attr(not(target_arch = "wasm32"), test)]
@@ -558,7 +558,7 @@ fn test_node_before_html() {
     assert!(doc
         .select("#before-before-origin + #also-before-origin + #before-origin + #origin + #after-origin")
         .exists());
-    doc.tree.validate().unwrap(); 
+    doc.tree.validate().unwrap();
 }
 
 #[cfg_attr(not(target_arch = "wasm32"), test)]
@@ -600,7 +600,7 @@ fn test_insert_siblings_before() {
     assert!(doc
         .select("#before-0 + #before-1 + #before-origin + #origin + #after-origin")
         .exists());
-    doc.tree.validate().unwrap(); 
+    doc.tree.validate().unwrap();
 }
 
 #[cfg_attr(not(target_arch = "wasm32"), test)]
@@ -636,7 +636,7 @@ fn test_node_add_class() {
     let node = sel.nodes().first().unwrap();
     node.add_class("blue");
     assert_eq!(doc.select("#parent .blue.child").length(), 1);
-    doc.tree.validate().unwrap(); 
+    doc.tree.validate().unwrap();
 }
 
 #[cfg_attr(not(target_arch = "wasm32"), test)]
@@ -661,7 +661,7 @@ fn test_node_remove_attr() {
     let node = sel.nodes().first().unwrap();
     node.remove_attr("class");
     assert_eq!(doc.select("#parent [class]").length(), 1);
-    doc.tree.validate().unwrap(); 
+    doc.tree.validate().unwrap();
 }
 
 #[cfg_attr(not(target_arch = "wasm32"), test)]
@@ -701,7 +701,7 @@ fn test_node_remove_all_attrs() {
     let node = sel.nodes().first().unwrap();
     node.remove_all_attrs();
     assert_eq!(doc.select("#parent [class][id]").length(), 1);
-    doc.tree.validate().unwrap(); 
+    doc.tree.validate().unwrap();
 }
 
 #[cfg_attr(not(target_arch = "wasm32"), test)]
@@ -733,7 +733,7 @@ fn test_node_strip_elements() {
     node.strip_elements(&["div"]);
     assert_eq!(doc.select("body div").length(), 0);
     assert_eq!(doc.select("body").text().matches("Child").count(), 2);
-    doc.tree.validate().unwrap(); 
+    doc.tree.validate().unwrap();
 }
 
 #[cfg_attr(not(target_arch = "wasm32"), test)]
@@ -757,7 +757,7 @@ fn test_node_wrap_node() {
     // Parent should still have two children, one being the wrapper
     assert_eq!(doc.select("#parent > *").length(), 2);
 
-    doc.tree.validate().unwrap(); 
+    doc.tree.validate().unwrap();
 }
 
 #[cfg_attr(not(target_arch = "wasm32"), test)]
@@ -843,5 +843,17 @@ fn test_node_unwrap_node_noop_if_no_parent() {
     assert_eq!(doc.select("html").length(), 1);
     assert_eq!(doc.select("#great-ancestor").length(), 1);
 
-    doc.tree.validate().unwrap(); 
+    doc.tree.validate().unwrap();
+}
+
+#[cfg_attr(not(target_arch = "wasm32"), test)]
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+fn test_set_html_empty() {
+    let doc = doc_with_siblings();
+    let main_sel = doc.select("#main");
+    let main_node = main_sel.nodes().first().unwrap();
+    main_node.set_html("");
+    assert_eq!(doc.select("#main").length(), 1);
+    assert_eq!(doc.select("#main").children().length(), 0);
+    doc.tree.validate().unwrap();
 }
diff --git a/tests/selection-manipulation.rs b/tests/selection-manipulation.rs
index 3b840a4..eb95369 100644
--- a/tests/selection-manipulation.rs
+++ b/tests/selection-manipulation.rs
@@ -19,7 +19,7 @@ fn test_replace_with_html() {
     sel.replace_with_html(r#"<div class="replace"></div>"#);
 
     assert_eq!(doc.select(".replace").length(), 2);
-    doc.tree.validate().unwrap(); 
+    doc.tree.validate().unwrap();
 }
 
 #[cfg_attr(not(target_arch = "wasm32"), test)]
@@ -33,7 +33,7 @@ fn test_set_html() {
 
     let html: &str = &q.text();
     assert_eq!(html, "testtest");
-    doc.tree.validate().unwrap(); 
+    doc.tree.validate().unwrap();
 }
 
 #[cfg_attr(not(target_arch = "wasm32"), test)]
@@ -43,7 +43,7 @@ fn test_set_html_no_match() {
     let q = doc.select("#notthere");
     q.set_html(r#"<div id="replace">test</div>"#);
     assert_eq!(doc.select("#replace").length(), 0);
-    doc.tree.validate().unwrap(); 
+    doc.tree.validate().unwrap();
 }
 
 #[cfg_attr(not(target_arch = "wasm32"), test)]
@@ -54,7 +54,7 @@ fn test_set_html_empty() {
     q.set_html("");
     assert_eq!(doc.select("#main").length(), 1);
     assert_eq!(doc.select("#main").children().length(), 0);
-    doc.tree.validate().unwrap(); 
+    doc.tree.validate().unwrap();
 }
 
 #[cfg_attr(not(target_arch = "wasm32"), test)]
@@ -95,7 +95,7 @@ fn remove_descendant_attributes() {
 
     assert!(main_sel.has_attr("style"));
 
-    doc.tree.validate().unwrap(); 
+    doc.tree.validate().unwrap();
 }
 
 #[cfg_attr(not(target_arch = "wasm32"), test)]
@@ -112,7 +112,7 @@ fn test_append_html_multiple() {
         2
     );
 
-    doc.tree.validate().unwrap(); 
+    doc.tree.validate().unwrap();
 }
 
 #[cfg_attr(not(target_arch = "wasm32"), test)]
@@ -124,7 +124,7 @@ fn test_append_html_multiple_elements_to_multiple() {
     q.append_html(r#"<span>1</span><span>2</span>"#);
 
     assert_eq!(doc.select(r#"div span"#).length(), 4);
-    doc.tree.validate().unwrap(); 
+    doc.tree.validate().unwrap();
 }
 
 #[cfg_attr(not(target_arch = "wasm32"), test)]


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>src/selection.rs (1)</summary><blockquote>

`536-540`: **Consistent validation pattern applied across HTML manipulation methods.**

The validation pattern is properly applied here, just like in the other manipulation methods. There's an extra blank line (539) that could be removed for consistency with the other similar methods.

```diff
    if TreeNodeOps::is_valid_node_id(tree_nodes, &new_node_id) {
        TreeNodeOps::insert_siblings_before(tree_nodes, &node.id, &new_node_id);
    }
-    
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a9e98b7de738f590a15a6411b7b0faeaa585022a and 2734dddd05722b3bd56bb87ba4ba2279e527882e.

</details>

<details>
<summary>📒 Files selected for processing (9)</summary>

* `src/dom_tree.rs` (1 hunks)
* `src/dom_tree/ops.rs` (2 hunks)
* `src/dom_tree/tree.rs` (8 hunks)
* `src/dom_tree/validation.rs` (1 hunks)
* `src/node/node_ref.rs` (1 hunks)
* `src/selection.rs` (2 hunks)
* `tests/html-dom.rs` (3 hunks)
* `tests/node-manipulation.rs` (13 hunks)
* `tests/selection-manipulation.rs` (7 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>src/node/node_ref.rs (1)</summary>

```
Learnt from: niklak
PR: niklak/dom_query#46
File: src/dom_tree/tree.rs:388-395
Timestamp: 2024-11-30T12:43:02.309Z
Learning: In `merge_with_fn` function in `src/dom_tree/tree.rs`, the `new_node_id` calculation is correct because during the merge, node IDs are adjusted, and the first new node added will have an ID equal to the previous tree size.
```

</details>

</details><details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>src/node/node_ref.rs (1)</summary><blockquote>

<details>
<summary>src/dom_tree/ops.rs (1)</summary>

* `is_valid_node_id` (162-164)

</details>

</blockquote></details>
<details>
<summary>src/selection.rs (2)</summary><blockquote>

<details>
<summary>src/dom_tree/ops.rs (6)</summary>

* `is_valid_node_id` (162-164)
* `append_children_of` (342-350)
* `insert_siblings_before` (320-327)
* `remove_from_parent` (369-403)
* `prepend_children_of` (353-365)
* `insert_siblings_after` (330-339)

</details>
<details>
<summary>src/node/node_ref.rs (8)</summary>

* `insert_siblings_before` (251-254)
* `remove_from_parent` (177-179)
* `append_html` (284-291)
* `html` (580-582)
* `prepend_html` (294-301)
* `before_html` (304-311)
* `after_html` (314-321)
* `insert_siblings_after` (258-261)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>src/dom_tree/tree.rs</summary>

[warning] 491-492: src/dom_tree/tree.rs#L491-L492
Added lines #L491 - L492 were not covered by tests

---

[warning] 515-516: src/dom_tree/tree.rs#L515-L516
Added lines #L515 - L516 were not covered by tests

---

[warning] 536-537: src/dom_tree/tree.rs#L536-L537
Added lines #L536 - L537 were not covered by tests

---

[warning] 560-561: src/dom_tree/tree.rs#L560-L561
Added lines #L560 - L561 were not covered by tests

---

[warning] 585-586: src/dom_tree/tree.rs#L585-L586
Added lines #L585 - L586 were not covered by tests

</details>
<details>
<summary>src/dom_tree/validation.rs</summary>

[warning] 58-58: src/dom_tree/validation.rs#L58
Added line #L58 was not covered by tests

---

[warning] 61-61: src/dom_tree/validation.rs#L61
Added line #L61 was not covered by tests

---

[warning] 64-64: src/dom_tree/validation.rs#L64
Added line #L64 was not covered by tests

---

[warning] 67-67: src/dom_tree/validation.rs#L67
Added line #L67 was not covered by tests

---

[warning] 70-70: src/dom_tree/validation.rs#L70
Added line #L70 was not covered by tests

---

[warning] 76-79: src/dom_tree/validation.rs#L76-L79
Added lines #L76 - L79 were not covered by tests

---

[warning] 83-83: src/dom_tree/validation.rs#L83
Added line #L83 was not covered by tests

---

[warning] 87-90: src/dom_tree/validation.rs#L87-L90
Added lines #L87 - L90 were not covered by tests

---

[warning] 102-105: src/dom_tree/validation.rs#L102-L105
Added lines #L102 - L105 were not covered by tests

---

[warning] 110-110: src/dom_tree/validation.rs#L110
Added line #L110 was not covered by tests

---

[warning] 113-116: src/dom_tree/validation.rs#L113-L116
Added lines #L113 - L116 were not covered by tests

---

[warning] 122-122: src/dom_tree/validation.rs#L122
Added line #L122 was not covered by tests

---

[warning] 138-141: src/dom_tree/validation.rs#L138-L141
Added lines #L138 - L141 were not covered by tests

---

[warning] 152-152: src/dom_tree/validation.rs#L152
Added line #L152 was not covered by tests

---

[warning] 156-159: src/dom_tree/validation.rs#L156-L159
Added lines #L156 - L159 were not covered by tests

---

[warning] 170-173: src/dom_tree/validation.rs#L170-L173
Added lines #L170 - L173 were not covered by tests

---

[warning] 183-186: src/dom_tree/validation.rs#L183-L186
Added lines #L183 - L186 were not covered by tests

---

[warning] 210-210: src/dom_tree/validation.rs#L210
Added line #L210 was not covered by tests

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: test

</details>

<details>
<summary>🔇 Additional comments (17)</summary><blockquote>

<details>
<summary>src/dom_tree.rs (1)</summary>

`5-5`: **New validation module added successfully.**

The addition of the validation module aligns with the PR objective of modularizing tree validation logic. This module will host the validation functionality previously in `tree.rs`.

</details>
<details>
<summary>tests/selection-manipulation.rs (1)</summary>

`22-22`: **Code formatting improvement.**

Removing trailing whitespace improves code readability and consistency.



Also applies to: 36-36, 46-46, 57-57, 98-98, 115-115, 127-127

</details>
<details>
<summary>src/node/node_ref.rs (1)</summary>

`357-359`: **Added node ID validation to prevent invalid operations.**

This change adds a crucial validation guard that prevents tree operations on invalid node IDs, improving robustness by ensuring the closure is only called when the new node ID is valid. This defensive programming approach helps prevent potential bugs that could arise from manipulating invalid nodes.

</details>
<details>
<summary>tests/node-manipulation.rs (3)</summary>

`3-3`: **Updated import to include doc_with_siblings helper.**

Good addition to support the new test case for empty HTML handling.

---

`456-456`: **Code formatting improvement.**

Removing trailing whitespace improves code readability and consistency.



Also applies to: 477-477, 500-500, 545-545, 603-603, 639-639, 664-664, 704-704, 736-736, 760-760, 846-846

---

`849-859`: **Added test for setting empty HTML content.**

This is a valuable addition that tests an important edge case - setting HTML content to an empty string. The test verifies that the parent node remains intact while all children are removed. This helps ensure that the library handles empty content correctly.

</details>
<details>
<summary>src/dom_tree/ops.rs (2)</summary>

`161-164`: **Well-implemented validation method to ensure node ID integrity.**

The new `is_valid_node_id` method provides a clean way to validate node references before performing tree operations. It correctly checks both the existence of the node and that its ID matches the expected value, which will help prevent operations on invalid nodes.

---

`342-343`: **Improved parameter naming for better code clarity.**

Renaming the parameter from `new_node_id` to `new_child_id` better reflects its purpose and aligns with the method's functionality. The adjusted initialization using `copied()` maintains the same behavior while making the code more readable.

</details>
<details>
<summary>src/selection.rs (1)</summary>

`496-498`: **Good defensive programming to prevent operations on invalid nodes.**

Adding validation checks before tree operations is an excellent safety improvement. This prevents potential corruption of the tree structure when working with invalid node IDs, making the code more robust against errors.

</details>
<details>
<summary>src/dom_tree/tree.rs (2)</summary>

`489-493`: **Improved test assertion formatting for better readability.**

The multi-line formatting of assertion error messages makes the test code more readable and maintainable. This is a good practice for complex error messages.



Also applies to: 514-517, 535-538, 559-562, 584-587

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 491-492: src/dom_tree/tree.rs#L491-L492
Added lines #L491 - L492 were not covered by tests

</details>

</details>

---

`599-607`: **Enhanced test code clarity with expanded node creation calls.**

Expanding the node creation calls in the test code provides better visibility into the test data structure. This makes the tests more maintainable and easier to understand.



Also applies to: 626-638

</details>
<details>
<summary>src/dom_tree/validation.rs (2)</summary>

`4-41`: **Excellent comprehensive validation implementation with clear structure.**

The new validation module provides thorough structural integrity checks for the DOM tree. The validation logic is well-organized into distinct categories (root uniqueness, link consistency, cycle detection), making it maintainable and easy to understand.

---

`5-41`: **Detailed documentation greatly enhances code clarity and usability.**

The documentation for the `validate` method is exemplary - providing clear explanations of each validation category, parameters, return values, and behavior. This level of detail makes the code much more maintainable and helps other developers understand the validation process.

</details>
<details>
<summary>tests/html-dom.rs (4)</summary>

`18-22`: **Improved assertion formatting for better error reporting.**

The multi-line assertion format provides much clearer error messages by including the actual validation error details when tests fail, making debugging easier.



Also applies to: 177-181

---

`186-187`: **Good test refactoring with appropriate expectation.**

Renaming from `doc_render_bug` to `test_issue_node_append_the_next_sibling` provides a clearer description of what's being tested. The addition of `#[should_panic]` correctly marks that this test is expected to fail due to the invalid tree state created during the test.

---

`199-235`: **Excellent documentation of DOM tree operations and their consequences.**

The detailed comments clearly explain:
1. The tree operations being performed
2. Why they lead to an invalid tree state
3. Alternative approaches to safely achieve the same result

This documentation is invaluable for understanding DOM tree manipulation rules and will help future developers avoid similar issues.

---

`239-243`: **Consistent use of multi-line assertion formatting.**

This final assertion follows the same improved formatting pattern as the others, ensuring consistent error reporting throughout the tests.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T12:14:55Z
user: login: niklak
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-25T12:19:58Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>src/dom_tree/tree.rs (1)</summary><blockquote>

`1-672`: **Refactoring validation logic is a positive architectural improvement.**

Moving the validation logic to a dedicated module (`src/dom_tree/validation.rs`) while maintaining the same interface is a good architectural decision. This refactoring:

1. Improves separation of concerns
2. Makes the code more maintainable
3. Allows for more focused testing of validation logic
4. Keeps the public API stable by maintaining the same method signature

The PR preserves all test cases, ensuring the validation functionality continues to work as expected after the refactoring.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 2734dddd05722b3bd56bb87ba4ba2279e527882e and 1f728002ee78b50feb75a06af12f10d08517eb29.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `CHANGELOG.md` (1 hunks)
* `src/dom_tree/tree.rs` (9 hunks)
* `src/selection.rs` (2 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* CHANGELOG.md

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* src/selection.rs

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: test
* GitHub Check: test-wasm

</details>

<details>
<summary>🔇 Additional comments (9)</summary><blockquote>

<details>
<summary>src/dom_tree/tree.rs (9)</summary>

`459-459`: **Good addition of the rustfmt skip directive.**

Adding the `#[rustfmt::skip]` attribute helps maintain custom formatting in the test module, which is appropriate for test cases where precise visual layout can aid readability.

---

`490-490`: **Improved error message formatting in assertion.**

The error message formatting has been enhanced to show the actual validation error when a test fails, which makes debugging much easier.

---

`510-510`: **Consistent error message formatting in validation tests.**

This follows the same improved pattern as other assertions in the file, helping maintain consistency across all validation test cases.

---

`527-527`: **Maintaining consistent error message formatting.**

The improved error message pattern continues to be applied consistently throughout the test suite.

---

`568-568`: **Consistent error message format maintained.**

The same improved error message formatting is applied here as well, making all test failures more informative.

---

`580-589`: **Enhanced readability with multi-line formatting.**

Breaking down the TreeNode initialization into multiple lines improves code readability and makes the structure clearer.

---

`599-600`: **Improved assertion error message formatting.**

Breaking the assertion error check into multiple lines enhances readability while maintaining the same functionality.

---

`607-618`: **Improved readability with consistent formatting.**

The node creation calls have been reformatted with one argument per line, which makes the code more readable and consistent with the rest of the codebase.

---

`651-662`: **Consistent formatting for node creation.**

This formatting change follows the same pattern as other similar code in the file, maintaining consistency throughout the test code.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T12:38:21Z
user: login: phayes
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T13:05:17Z
user: login: phayes
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T13:22:52Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>src/dom_tree/validation.rs (1)</summary><blockquote>

`53-69`: **Add targeted tests for validation error paths**

Current tests only exercise the "OK" branches of `validate()`. To improve robustness, please add negative‐path tests for critical failure modes, for example:

• Self‐reference checks  
  - A node whose `parent`, `first_child`, `last_child`, `prev_sibling`, or `next_sibling` equals its own `id` should produce an `Err`.  
• Invalid link references  
  - `first_child` or `last_child` pointing to a non‐existent node index.  
  - `first_child.prev_sibling` or `last_child.next_sibling` non‐`None`.  
  - Mismatched sibling back‐links (`prev_sibling.next_sibling` and `next_sibling.prev_sibling`).  
• Child‐chain consistency  
  - `first_child.parent` not matching the parent node.  
  - `last_child` not matching the last node in the child chain.  
• Cycle detection  
  - Parent‐chain cycles (a node repeating in its own ancestry).  
  - Sibling‐chain cycles (a node repeating in its own next‐sibling chain).

For each scenario, create or extend tests that deliberately build an invalid `DomTree`, call `.validate()`, and assert `.is_err()` or use `.expect_err("…")`. This will ensure all major error‐handling paths in `src/dom_tree/validation.rs` are exercised.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 55-55: src/dom_tree/validation.rs#L55
Added line #L55 was not covered by tests

---

[warning] 58-58: src/dom_tree/validation.rs#L58
Added line #L58 was not covered by tests

---

[warning] 61-61: src/dom_tree/validation.rs#L61
Added line #L61 was not covered by tests

---

[warning] 64-64: src/dom_tree/validation.rs#L64
Added line #L64 was not covered by tests

---

[warning] 67-67: src/dom_tree/validation.rs#L67
Added line #L67 was not covered by tests

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>src/dom_tree/validation.rs (1)</summary><blockquote>

`1-216`: **Consider adding benchmarks for validation performance.**

Since tree validation could potentially be an expensive operation on large DOM trees, consider adding benchmarks to measure the performance impact. This would help ensure the validation doesn't become a bottleneck, especially in debug builds where you might be validating more frequently.

```rust
#[cfg(test)]
mod bench {
    use test::Bencher;
    use super::*;

    #[bench]
    fn bench_validate_small_tree(b: &mut Bencher) {
        // Setup a small tree
        let tree = /* create small tree */;
        b.iter(|| {
            tree.validate().unwrap();
        });
    }

    #[bench]
    fn bench_validate_large_tree(b: &mut Bencher) {
        // Setup a large tree
        let tree = /* create large tree */;
        b.iter(|| {
            tree.validate().unwrap();
        });
    }
}
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 55-55: src/dom_tree/validation.rs#L55
Added line #L55 was not covered by tests

---

[warning] 58-58: src/dom_tree/validation.rs#L58
Added line #L58 was not covered by tests

---

[warning] 61-61: src/dom_tree/validation.rs#L61
Added line #L61 was not covered by tests

---

[warning] 64-64: src/dom_tree/validation.rs#L64
Added line #L64 was not covered by tests

---

[warning] 67-67: src/dom_tree/validation.rs#L67
Added line #L67 was not covered by tests

---

[warning] 73-76: src/dom_tree/validation.rs#L73-L76
Added lines #L73 - L76 were not covered by tests

---

[warning] 80-80: src/dom_tree/validation.rs#L80
Added line #L80 was not covered by tests

---

[warning] 84-87: src/dom_tree/validation.rs#L84-L87
Added lines #L84 - L87 were not covered by tests

---

[warning] 99-102: src/dom_tree/validation.rs#L99-L102
Added lines #L99 - L102 were not covered by tests

---

[warning] 107-107: src/dom_tree/validation.rs#L107
Added line #L107 was not covered by tests

---

[warning] 110-113: src/dom_tree/validation.rs#L110-L113
Added lines #L110 - L113 were not covered by tests

---

[warning] 119-119: src/dom_tree/validation.rs#L119
Added line #L119 was not covered by tests

---

[warning] 135-138: src/dom_tree/validation.rs#L135-L138
Added lines #L135 - L138 were not covered by tests

---

[warning] 149-149: src/dom_tree/validation.rs#L149
Added line #L149 was not covered by tests

---

[warning] 153-156: src/dom_tree/validation.rs#L153-L156
Added lines #L153 - L156 were not covered by tests

---

[warning] 167-170: src/dom_tree/validation.rs#L167-L170
Added lines #L167 - L170 were not covered by tests

---

[warning] 180-183: src/dom_tree/validation.rs#L180-L183
Added lines #L180 - L183 were not covered by tests

---

[warning] 207-207: src/dom_tree/validation.rs#L207
Added line #L207 was not covered by tests

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1f728002ee78b50feb75a06af12f10d08517eb29 and d90b3a83259bfb7a1e8bb3114365a587532fe8d4.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `src/dom_tree/validation.rs` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>src/dom_tree/validation.rs</summary>

[warning] 55-55: src/dom_tree/validation.rs#L55
Added line #L55 was not covered by tests

---

[warning] 58-58: src/dom_tree/validation.rs#L58
Added line #L58 was not covered by tests

---

[warning] 61-61: src/dom_tree/validation.rs#L61
Added line #L61 was not covered by tests

---

[warning] 64-64: src/dom_tree/validation.rs#L64
Added line #L64 was not covered by tests

---

[warning] 67-67: src/dom_tree/validation.rs#L67
Added line #L67 was not covered by tests

---

[warning] 73-76: src/dom_tree/validation.rs#L73-L76
Added lines #L73 - L76 were not covered by tests

---

[warning] 80-80: src/dom_tree/validation.rs#L80
Added line #L80 was not covered by tests

---

[warning] 84-87: src/dom_tree/validation.rs#L84-L87
Added lines #L84 - L87 were not covered by tests

---

[warning] 99-102: src/dom_tree/validation.rs#L99-L102
Added lines #L99 - L102 were not covered by tests

---

[warning] 107-107: src/dom_tree/validation.rs#L107
Added line #L107 was not covered by tests

---

[warning] 110-113: src/dom_tree/validation.rs#L110-L113
Added lines #L110 - L113 were not covered by tests

---

[warning] 119-119: src/dom_tree/validation.rs#L119
Added line #L119 was not covered by tests

---

[warning] 135-138: src/dom_tree/validation.rs#L135-L138
Added lines #L135 - L138 were not covered by tests

---

[warning] 149-149: src/dom_tree/validation.rs#L149
Added line #L149 was not covered by tests

---

[warning] 153-156: src/dom_tree/validation.rs#L153-L156
Added lines #L153 - L156 were not covered by tests

---

[warning] 167-170: src/dom_tree/validation.rs#L167-L170
Added lines #L167 - L170 were not covered by tests

---

[warning] 180-183: src/dom_tree/validation.rs#L180-L183
Added lines #L180 - L183 were not covered by tests

---

[warning] 207-207: src/dom_tree/validation.rs#L207
Added line #L207 was not covered by tests

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: test-wasm
* GitHub Check: test

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>src/dom_tree/validation.rs (5)</summary>

`5-38`: **Well-documented validation function.**

The comprehensive documentation clearly explains what aspects of the tree structure are being validated, which is excellent for maintainability and understanding.

---

`41-47`: **Root node validation looks good.**

The root uniqueness check properly verifies that the root node exists at index 0 and has no parent.

---

`71-140`: **Thorough child chain validation.**

The implementation thoroughly validates the entire child chain, including parent references, sibling links, and consistency between the declared last child and the actual end of the chain.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 73-76: src/dom_tree/validation.rs#L73-L76
Added lines #L73 - L76 were not covered by tests

---

[warning] 80-80: src/dom_tree/validation.rs#L80
Added line #L80 was not covered by tests

---

[warning] 84-87: src/dom_tree/validation.rs#L84-L87
Added lines #L84 - L87 were not covered by tests

---

[warning] 99-102: src/dom_tree/validation.rs#L99-L102
Added lines #L99 - L102 were not covered by tests

---

[warning] 107-107: src/dom_tree/validation.rs#L107
Added line #L107 was not covered by tests

---

[warning] 110-113: src/dom_tree/validation.rs#L110-L113
Added lines #L110 - L113 were not covered by tests

---

[warning] 119-119: src/dom_tree/validation.rs#L119
Added line #L119 was not covered by tests

---

[warning] 135-138: src/dom_tree/validation.rs#L135-L138
Added lines #L135 - L138 were not covered by tests

</details>

</details>

---

`188-212`: **Efficient cycle detection.**

Using HashSet for tracking visited nodes is an efficient approach to detect cycles in both parent chains and sibling chains.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 207-207: src/dom_tree/validation.rs#L207
Added line #L207 was not covered by tests

</details>

</details>

---

`160-186`: **Consider using early returns for readability.**

The nested if statements with return Err could be flattened for better readability. For example:

```diff
if let Some(prev_sibling_id) = node.prev_sibling {
    let prev_sibling = nodes
        .get(prev_sibling_id.value)
        .ok_or_else(|| format!("Invalid prev_sibling reference at node {:?}", id))?;

-   if prev_sibling.next_sibling != Some(id) {
-       return Err(format!(
-           "prev_sibling {:?} does not link back to {:?}",
-           prev_sibling_id, id
-       ));
-   }
+   if prev_sibling.next_sibling != Some(id) {
+       return Err(format!(
+           "prev_sibling {:?} does not link back to {:?}",
+           prev_sibling_id, id
+       ));
+   }
}
```

Note: This suggestion isn't actually changing the code structure, just illustrating that the current approach of using early returns is good for readability.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 167-170: src/dom_tree/validation.rs#L167-L170
Added lines #L167 - L170 were not covered by tests

---

[warning] 180-183: src/dom_tree/validation.rs#L180-L183
Added lines #L180 - L183 were not covered by tests

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T13:28:20Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>CHANGELOG.md (3)</summary><blockquote>

`8-8`: **Nit: Add spacing after commas in method list for readability**  
The backticked method names are missing spaces after commas, which can hinder quick scanning. Consider updating to:  
```diff
- `NodeRef::wrap_node`,`NodeRef::wrap_html`, and `NodeRef::unwrap_node`
+ `NodeRef::wrap_node`, `NodeRef::wrap_html`, and `NodeRef::unwrap_node`
```

---

`9-9`: **Approve addition of `Tree::validate`, consider enhancing description**  
This entry correctly records the new validation API. For clarity, you might optionally expand the description to summarize what aspects it checks. For example:  
```diff
- Introduced `Tree::validate`, a useful method for validating the tree structure (by @phayes).
+ Introduced `Tree::validate`, a method for performing comprehensive integrity checks on node relationships, links, and cycles within the DOM tree (by @phayes).
```

---

`20-20`: **Nit: Reclassify or rephrase fix entry for clarity**  
The change in `TreeNodeOps` behavior is more of a behavioral change than a bug fix. You could either move this into the “Changed” section or adjust phrasing to highlight it as a bug–preventing fix. E.g.:  
```diff
### Changed
- `TreeNodeOps::append_child_of` and `TreeNodeOps::prepend_child_of` now internally call `TreeNodeOps::remove_from_parent` before reattachment to avoid stale parent/sibling links.
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~20-~20: This verb does not appear to agree with the subject. Consider using a different form.
Context: ...deOps::prepend_child_of` now internally call `TreeNodeOps::remove_from_parent` on th...

(AI_EN_LECTOR_REPLACEMENT_VERB_AGREEMENT)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a8a0e914d04274fe70cc7b3b5ba421616297abb8 and 72edd89efa672f11873f1b6fa1de963555bbf482.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `CHANGELOG.md` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>CHANGELOG.md</summary>

[uncategorized] ~20-~20: This verb does not appear to agree with the subject. Consider using a different form.
Context: ...deOps::prepend_child_of` now internally call `TreeNodeOps::remove_from_parent` on th...

(AI_EN_LECTOR_REPLACEMENT_VERB_AGREEMENT)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: test-wasm

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T14:44:57Z

Review comments:
diff_hunk: @@ -0,0 +1,219 @@
+// Validation methods
+use super::tree::Tree;
+
+impl Tree {
+    /// Validates the structural integrity of the tree.
+    ///
+    /// This function checks for:
+    ///
+    /// 1. **Root Uniqueness**:
+    ///    - Ensures exactly one root node exists (`parent == None`) and it is `NodeId(0)`.
+    ///
+    /// 2. **Parent-Child Link Consistency**:
+    ///    - `first_child.parent == Self`
+    ///    - `first_child.prev_sibling == None`
+    ///    - `last_child.parent == Self`
+    ///    - `last_child.next_sibling == None`
+    ///    - All children in the chain from `first_child` to `last_child` have correct `parent`, `prev_sibling`, and `next_sibling` references.
+    ///
+    /// 3. **Sibling Link Consistency**:
+    ///    - `prev_sibling.next_sibling == Self`
+    ///    - `next_sibling.prev_sibling == Self`
+    ///
+    /// 4. **Valid Node References**:
+    ///    - All `NodeId` references must point to valid nodes within the tree.
+    ///
+    /// 5. **Orphaned Nodes** (when `check_orphans` is `true`):
+    ///   - Nodes with `parent == None` that are not the root node are considered orphaned.
+    ///
+    /// 6. **Cycle Detection**:
+    ///    - No cycles in parent chains (traverse `parent` links).
+    ///    - No cycles in sibling chains (traverse `next_sibling` links).
+    ///
+    /// Parameters:
+    /// - `check_orphans`: If `true`, check for orphaned nodes (nodes with `parent == None` that are not the root).
+    ///
+    /// Returns:
+    /// - `Ok(())` if the tree structure is valid.
+    /// - `Err(String)` with a descriptive message if any inconsistency or cycle is detected.
+    ///
+    /// Orphaned nodes (non-root nodes with `parent == None`) are allowed and not considered invalid.
+    pub fn validate(&self) -> Result<(), String> {
+        let nodes = self.nodes.borrow();
+
+        // Root uniqueness check
+        let root_node = nodes
+            .first()
+            .ok_or_else(|| "Root node (NodeId(0)) missing".to_string())?;
+        if root_node.parent.is_some() {
+            return Err("Root node (NodeId(0)) must have no parent".to_string());
+        }
+
+        // Node parent-child / sibling link consistency checks
+        for node in nodes.iter() {
+            let id = node.id;
+
+            // Validate self-cycles
+            if node.parent == Some(id) {
+                return Err(format!("Node {:?} references itself as parent", id));
+            }
+            if node.prev_sibling == Some(id) {
+                return Err(format!("Node {:?} references itself as prev_sibling", id));
+            }
+            if node.next_sibling == Some(id) {
+                return Err(format!("Node {:?} references itself as next_sibling", id));
+            }
+            if node.first_child == Some(id) {
+                return Err(format!("Node {:?} references itself as first_child", id));
+            }
+            if node.last_child == Some(id) {
+                return Err(format!("Node {:?} references itself as last_child", id));
+            }
+
+            // Validate first_child linkage
+            if let Some(first_child_id) = node.first_child {
+                let first_child = nodes.get(first_child_id.value).ok_or_else(|| {
+                    format!(
+                        "Invalid first_child {:?} reference at node {:?}",
+                        first_child_id, id
+                    )
+                })?;
+
+                if first_child.parent != Some(id) {
+                    return Err(format!("first_child's parent does not match node {:?}", id));
+                }
+
+                if first_child.prev_sibling.is_some() {
+                    return Err(format!(
+                        "first_child {:?} of node {:?} has non-None prev_sibling",
+                        first_child_id, id
+                    ));
+                }
+
+                // Validate child chain
+                let mut current_id = Some(first_child_id);
+                let mut last_seen = None;
+                while let Some(cid) = current_id {
+                    let current = nodes
+                        .get(cid.value)
+                        .ok_or_else(|| format!("Invalid child reference at node {:?}", cid))?;
+
+                    if current.parent != Some(id) {
+                        return Err(format!(
+                            "Child {:?} has incorrect parent, expected {:?}",
+                            cid, id
+                        ));
+                    }
+
+                    if let Some(prev) = current.prev_sibling {
+                        let prev_node = nodes.get(prev.value).ok_or_else(|| {
+                            format!("Invalid prev_sibling reference at node {:?}", prev)
+                        })?;
+                        if prev_node.next_sibling != Some(cid) {
+                            return Err(format!(
+                                "prev_sibling {:?} next_sibling mismatch with {:?}",
+                                prev, cid
+                            ));
+                        }
+                    }
+
+                    if let Some(next) = current.next_sibling {
+                        let next_node = nodes.get(next.value).ok_or_else(|| {
+                            format!("Invalid next_sibling reference at node {:?}", next)
+                        })?;
+                        if next_node.prev_sibling != Some(cid) {
+                            return Err(format!(
+                                "next_sibling {:?} prev_sibling mismatch with {:?}",
+                                next, cid
+                            ));
+                        }
+                        current_id = Some(next);
+                    } else {
+                        last_seen = Some(cid);
+                        current_id = None;
+                    }
+                }
+
+                if node.last_child != last_seen {
+                    return Err(format!(
+                        "last_child mismatch at node {:?}, expected {:?}, found {:?}",
+                        id, node.last_child, last_seen
+                    ));
+                }
+            }
+
+            // Validate last_child linkage
+            if let Some(last_child_id) = node.last_child {
+                let last_child = nodes
+                    .get(last_child_id.value)
+                    .ok_or_else(|| format!("Invalid last_child reference at node {:?}", id))?;
+
+                if last_child.parent != Some(id) {
+                    return Err(format!("last_child's parent does not match node {:?}", id));
+                }
+
+                if last_child.next_sibling.is_some() {
+                    return Err(format!(
+                        "last_child {:?} of node {:?} has non-None next_sibling",
+                        last_child_id, id
+                    ));
+                }
+            }
+
+            // Validate sibling links
+            if let Some(prev_sibling_id) = node.prev_sibling {
+                let prev_sibling = nodes
+                    .get(prev_sibling_id.value)
+                    .ok_or_else(|| format!("Invalid prev_sibling reference at node {:?}", id))?;
+
+                if prev_sibling.next_sibling != Some(id) {
+                    return Err(format!(
+                        "prev_sibling {:?} does not link back to {:?}",
+                        prev_sibling_id, id
+                    ));
+                }
+            }
+
+            if let Some(next_sibling_id) = node.next_sibling {
+                let next_sibling = nodes
+                    .get(next_sibling_id.value)
+                    .ok_or_else(|| format!("Invalid next_sibling reference at node {:?}", id))?;
+
+                if next_sibling.prev_sibling != Some(id) {
+                    return Err(format!(
+                        "next_sibling {:?} does not link back to {:?}",
+                        next_sibling_id, id
+                    ));
+                }
+            }
+        }
+
+        // Cycle detection: Parent chains
+        for node in nodes.iter() {
+            let mut visited = std::collections::HashSet::new();
+            let mut current = Some(node.id);
+            while let Some(cid) = current {
+                if !visited.insert(cid.value) {
+                    return Err(format!("Cycle detected in parent chain at node {:?}", cid));
+                }
+                current = nodes.get(cid.value).and_then(|n| n.parent);
+            }
+        }
+
+        // Cycle detection: Sibling chains
+        for node in nodes.iter() {
+            if let Some(first_child_id) = node.first_child {
+                let mut visited = std::collections::HashSet::new();
+                let mut current = Some(first_child_id);
+                while let Some(cid) = current {
+                    if !visited.insert(cid.value) {
+                        return Err(format!("Cycle detected in sibling chain at node {:?}", cid));
+                    }
+                    current = nodes.get(cid.value).and_then(|n| n.next_sibling);
+                }
+            }
+        }
+
+        Ok(())
path: src/dom_tree/validation.rs
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Consider improving test coverage for validation error paths.**

The static analysis indicates many validation code paths are not covered by tests. While it's challenging to test all error conditions, adding tests for critical validation scenarios would improve robustness. Consider creating targeted tests for different failure modes, especially for the self-reference checks and cycle detection.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check which test files are testing the validation module
rg -l "validate" tests/
```

Length of output: 100

---



</details>

**Add targeted tests for validation error paths**

Current tests in `tests/selection-manipulation.rs`, `tests/node-manipulation.rs`, and `tests/html-dom.rs` only exercise the “OK” branches of `validate()`. To improve robustness, please add negative‐path tests for critical failure modes, for example:

• Self‐reference checks  
  - A node whose `parent`, `first_child`, `last_child`, `prev_sibling`, or `next_sibling` equals its own `id` should produce an `Err`.  
• Invalid link references  
  - `first_child` or `last_child` pointing to a non‐existent node index.  
  - `first_child.prev_sibling` or `last_child.next_sibling` non‐`None`.  
  - Mismatched sibling back‐links (`prev_sibling.next_sibling` and `next_sibling.prev_sibling`).  
• Child‐chain consistency  
  - `first_child.parent` not matching the parent node.  
  - `last_child` not matching the last node in the child chain.  
• Cycle detection  
  - Parent‐chain cycles (a node repeating in its own ancestry).  
  - Sibling‐chain cycles (a node repeating in its own next‐sibling chain).

For each scenario, create or extend tests that deliberately build an invalid `DomTree`, call `.validate()`, and assert `.is_err()` or use `.expect_err("…")`. This will ensure all major error‐handling paths in `src/dom_tree/validation.rs` are exercised.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 58-58: src/dom_tree/validation.rs#L58
Added line #L58 was not covered by tests

---

[warning] 61-61: src/dom_tree/validation.rs#L61
Added line #L61 was not covered by tests

---

[warning] 64-64: src/dom_tree/validation.rs#L64
Added line #L64 was not covered by tests

---

[warning] 67-67: src/dom_tree/validation.rs#L67
Added line #L67 was not covered by tests

---

[warning] 70-70: src/dom_tree/validation.rs#L70
Added line #L70 was not covered by tests

---

[warning] 76-79: src/dom_tree/validation.rs#L76-L79
Added lines #L76 - L79 were not covered by tests

---

[warning] 83-83: src/dom_tree/validation.rs#L83
Added line #L83 was not covered by tests

---

[warning] 87-90: src/dom_tree/validation.rs#L87-L90
Added lines #L87 - L90 were not covered by tests

---

[warning] 102-105: src/dom_tree/validation.rs#L102-L105
Added lines #L102 - L105 were not covered by tests

---

[warning] 110-110: src/dom_tree/validation.rs#L110
Added line #L110 was not covered by tests

---

[warning] 113-116: src/dom_tree/validation.rs#L113-L116
Added lines #L113 - L116 were not covered by tests

---

[warning] 122-122: src/dom_tree/validation.rs#L122
Added line #L122 was not covered by tests

---

[warning] 138-141: src/dom_tree/validation.rs#L138-L141
Added lines #L138 - L141 were not covered by tests

---

[warning] 152-152: src/dom_tree/validation.rs#L152
Added line #L152 was not covered by tests

---

[warning] 156-159: src/dom_tree/validation.rs#L156-L159
Added lines #L156 - L159 were not covered by tests

---

[warning] 170-173: src/dom_tree/validation.rs#L170-L173
Added lines #L170 - L173 were not covered by tests

---

[warning] 183-186: src/dom_tree/validation.rs#L183-L186
Added lines #L183 - L186 were not covered by tests

---

[warning] 210-210: src/dom_tree/validation.rs#L210
Added line #L210 was not covered by tests

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-25T12:14:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -187,20 +196,49 @@ fn doc_render_bug() {
     let sel = doc.select("#child");
     let child = sel.nodes().first().unwrap();
 
-    // Create a wrapper directly in the same tree
+    // - Create a wrapper directly in the same tree area.
+    // This element is detached from the DOM tree -- it has no parent.
     let wrapper = doc.tree.new_element("div");
     wrapper.set_attr("id", "wrapper");
 
-    // Insert wrapper before child in the parent
+    // - Insert wrapper before existing child in the parent.
+    // Under the hood `insert_before_of` will call `remove_from_parent` for the new children,
+    // because we don`t know if it is a new node without bound to the tree or not.
+    // At this point `child` becomes a next sibling of the `wrapper`.
     doc.tree.insert_before_of(&child.id, &wrapper.id);
 
-    // Move child into wrapper as the only child
+    // If we don't call `remove_from_parent` for the `child` node,
+    // it will still be the next sibling of the `wrapper` node
+    // and at the same time also a child of the `wrapper` node.
+    // In this case, it will be both the first and the last child of the `wrapper`.
+    // - Move child into wrapper as the only child
+    //doc.tree.remove_from_parent(&child.id);
     doc.tree.append_child_of(&wrapper.id, &child.id);
+    // This may seem inconvenient, but the `Tree` methods are intended more for internal use,
+    // while `NodeRef` is designed for external usage.
 
-    // Normalize the document for printing
+    // So yes, without "detaching" the child node from the tree, the `Tree` instance becomes invalid.
+
+    // **But I cannot consider this a bug**.
+
+    // Because there are ways to avoid this situation:
+    // 1. Using `Node::insert_before` and `Node::append_child` methods, which will do the dirty work for you:
+    // child.insert_before(&wrapper);
+    // wrapper.append_child(child);
+
+    // 2. Using `Node::replace_with` and `Node::append_child` methods, which also handle cleanup internally.
+    // But note that the child node will be removed from the tree twice in this case.
+    // child.replace_with(&wrapper);
+    // wrapper.append_child(child);
+
+    // - Normalize the document for printing
     doc.normalize();
 
     // Check to see if the structure is valid
     let validity_check = doc.tree.validate();
-    assert!(validity_check.is_ok(), "Tree is not valid: {}", validity_check.unwrap_err());
-}
\ No newline at end of file
+    assert!(
+        validity_check.is_ok(),
+        "Tree is not valid: {}",
+        validity_check.unwrap_err()
+    );
path: tests/html-dom.rs
user: login: niklak
body: @phayes, take a look at this please.
created_at: 2025-04-25T12:19:58Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,219 @@
+// Validation methods
+use super::tree::Tree;
+
+impl Tree {
+    /// Validates the structural integrity of the tree.
+    ///
+    /// This function checks for:
+    ///
+    /// 1. **Root Uniqueness**:
+    ///    - Ensures exactly one root node exists (`parent == None`) and it is `NodeId(0)`.
+    ///
+    /// 2. **Parent-Child Link Consistency**:
+    ///    - `first_child.parent == Self`
+    ///    - `first_child.prev_sibling == None`
+    ///    - `last_child.parent == Self`
+    ///    - `last_child.next_sibling == None`
+    ///    - All children in the chain from `first_child` to `last_child` have correct `parent`, `prev_sibling`, and `next_sibling` references.
+    ///
+    /// 3. **Sibling Link Consistency**:
+    ///    - `prev_sibling.next_sibling == Self`
+    ///    - `next_sibling.prev_sibling == Self`
+    ///
+    /// 4. **Valid Node References**:
+    ///    - All `NodeId` references must point to valid nodes within the tree.
+    ///
+    /// 5. **Orphaned Nodes** (when `check_orphans` is `true`):
+    ///   - Nodes with `parent == None` that are not the root node are considered orphaned.
+    ///
+    /// 6. **Cycle Detection**:
+    ///    - No cycles in parent chains (traverse `parent` links).
+    ///    - No cycles in sibling chains (traverse `next_sibling` links).
+    ///
+    /// Parameters:
+    /// - `check_orphans`: If `true`, check for orphaned nodes (nodes with `parent == None` that are not the root).
path: src/dom_tree/validation.rs
user: login: phayes
body: Ooops, looks like some old code-comments from my own work got left in (my mistake!). 

vlidate() used to accept a parameter called "check_orphans", but I since removed it, so this Parameters section should be removed. 
created_at: 2025-04-25T13:05:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -187,20 +196,49 @@ fn doc_render_bug() {
     let sel = doc.select("#child");
     let child = sel.nodes().first().unwrap();
 
-    // Create a wrapper directly in the same tree
+    // - Create a wrapper directly in the same tree area.
+    // This element is detached from the DOM tree -- it has no parent.
     let wrapper = doc.tree.new_element("div");
     wrapper.set_attr("id", "wrapper");
 
-    // Insert wrapper before child in the parent
+    // - Insert wrapper before existing child in the parent.
+    // Under the hood `insert_before_of` will call `remove_from_parent` for the new children,
+    // because we don`t know if it is a new node without bound to the tree or not.
+    // At this point `child` becomes a next sibling of the `wrapper`.
     doc.tree.insert_before_of(&child.id, &wrapper.id);
 
-    // Move child into wrapper as the only child
+    // If we don't call `remove_from_parent` for the `child` node,
+    // it will still be the next sibling of the `wrapper` node
+    // and at the same time also a child of the `wrapper` node.
+    // In this case, it will be both the first and the last child of the `wrapper`.
+    // - Move child into wrapper as the only child
+    //doc.tree.remove_from_parent(&child.id);
     doc.tree.append_child_of(&wrapper.id, &child.id);
+    // This may seem inconvenient, but the `Tree` methods are intended more for internal use,
+    // while `NodeRef` is designed for external usage.
 
-    // Normalize the document for printing
+    // So yes, without "detaching" the child node from the tree, the `Tree` instance becomes invalid.
+
+    // **But I cannot consider this a bug**.
+
+    // Because there are ways to avoid this situation:
+    // 1. Using `Node::insert_before` and `Node::append_child` methods, which will do the dirty work for you:
+    // child.insert_before(&wrapper);
+    // wrapper.append_child(child);
+
+    // 2. Using `Node::replace_with` and `Node::append_child` methods, which also handle cleanup internally.
+    // But note that the child node will be removed from the tree twice in this case.
+    // child.replace_with(&wrapper);
+    // wrapper.append_child(child);
+
+    // - Normalize the document for printing
     doc.normalize();
 
     // Check to see if the structure is valid
     let validity_check = doc.tree.validate();
-    assert!(validity_check.is_ok(), "Tree is not valid: {}", validity_check.unwrap_err());
-}
\ No newline at end of file
+    assert!(
+        validity_check.is_ok(),
+        "Tree is not valid: {}",
+        validity_check.unwrap_err()
+    );
path: tests/html-dom.rs
user: login: phayes
body: Left a comment on the PR.   Definitely makes sense!   And we can likely remove this entire test case then. 
created_at: 2025-04-25T13:22:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,216 @@
+// Validation methods
+use super::tree::Tree;
+
+impl Tree {
+    /// Validates the structural integrity of the tree.
+    ///
+    /// This function checks for:
+    ///
+    /// 1. **Root Uniqueness**:
+    ///    - Ensures exactly one root node exists (`parent == None`) and it is `NodeId(0)`.
+    ///
+    /// 2. **Parent-Child Link Consistency**:
+    ///    - `first_child.parent == Self`
+    ///    - `first_child.prev_sibling == None`
+    ///    - `last_child.parent == Self`
+    ///    - `last_child.next_sibling == None`
+    ///    - All children in the chain from `first_child` to `last_child` have correct `parent`, `prev_sibling`, and `next_sibling` references.
+    ///
+    /// 3. **Sibling Link Consistency**:
+    ///    - `prev_sibling.next_sibling == Self`
+    ///    - `next_sibling.prev_sibling == Self`
+    ///
+    /// 4. **Valid Node References**:
+    ///    - All `NodeId` references must point to valid nodes within the tree.
+    ///
+    /// 5. **Orphaned Nodes** (when `check_orphans` is `true`):
+    ///   - Nodes with `parent == None` that are not the root node are considered orphaned.
path: src/dom_tree/validation.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Remove reference to non-existent parameter in documentation.**

The documentation refers to a `check_orphans` parameter (`when check_orphans is true`), but this parameter doesn't exist in the function signature. According to previous comments, this parameter was removed but the documentation wasn't updated.


```diff
-/// 5. **Orphaned Nodes** (when `check_orphans` is `true`):
+/// 5. **Orphaned Nodes**:
```

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit c7c1cb3
created_at: 2025-04-25T13:28:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-25T12:12:03Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This change modularizes and enhances validation for DOM tree operations. The comprehensive `validate` method previously in `src/dom_tree/tree.rs` is removed and its logic is moved to a new `validation` module. A new public method `is_valid_node_id` is added to `TreeNodeOps` for checking node validity. Several internal methods now use this validation before performing node operations, guarding against invalid manipulations. Tests are updated for improved readability and to reflect the new validation structure, including a new test for handling empty HTML. No public API signatures are changed except for internal method refactoring.

## Changes

| Files/Paths                      | Change Summary                                                                                      |
|---------------------------------|---------------------------------------------------------------------------------------------------|
| src/dom_tree.rs                 | Added `mod validation;` to import the new validation module.                                       |
| src/dom_tree/ops.rs             | Added `is_valid_node_id` method to `TreeNodeOps`; renamed parameter in `append_children_of` for clarity; added calls to `remove_from_parent` before child insertion in some methods. |
| src/dom_tree/tree.rs            | Removed the `validate` method and its logic; reformatted some test assertions for readability.     |
| src/dom_tree/validation.rs      | Added a new `validate` method for `Tree` with comprehensive tree integrity checks, ported from the previous location. |
| src/node/node_ref.rs            | Removed calls to `remove_from_parent` in `append_child`, `wrap_node`, and `wrap_html`; added validation using `is_valid_node_id` before invoking closure in `merge_html_with_fn`. |
| src/selection.rs                | Added validation checks for node IDs before performing node operations in several HTML manipulation methods (`set_html`, `replace_with_html`, `append_html`, `prepend_html`, `before_html`, `after_html`). |
| tests/html-dom.rs               | Reformatted assertions for readability; removed `doc_render_bug` test function.                   |
| tests/node-manipulation.rs      | Imported a helper; added a new test for empty HTML; removed trailing spaces after validation calls. |
| tests/selection-manipulation.rs | Removed trailing spaces after validation calls; no logic changes.                                 |
| CHANGELOG.md                   | Updated to add `Tree::validate` method and fix punctuation in method descriptions.                  |
| .github/workflows/coverage.yml | Updated coverage ignore pattern to exclude `validation.rs` in addition to `css.rs`.                |

## Possibly related PRs

- niklak/dom_query#97: Removes the `validate` method from `src/dom_tree/tree.rs` and moves its implementation into a new `validation` module, matching the modularization and relocation of tree validation logic in this PR.  
- niklak/dom_query#92: Modifies internal calls to `TreeNodeOps::remove_from_parent` within `NodeRef` methods and adds validation checks using `is_valid_node_id`, directly relating to the methods introduced in this PR.

## Poem

> In the garden of nodes where the DOM trees grow,  
> A rabbit hops softly, validation in tow.  
> With careful new checks and a modular plan,  
> Each branch and each leaf is checked as it ran.  
> No orphaned twigs, no cycles to see—  
> The tree stands strong, as perfect as can be!  
> 🐇🌳✨

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 72edd89efa672f11873f1b6fa1de963555bbf482 and 682d9cdd719d78cc634976ae3bd623e73ccd8a9e.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `CHANGELOG.md` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>CHANGELOG.md</summary>

[uncategorized] ~17-~17: This verb does not appear to agree with the subject. Consider using a different form.
Context: ...deOps::prepend_child_of` now internally call `TreeNodeOps::remove_from_parent` on th...

(AI_EN_LECTOR_REPLACEMENT_VERB_AGREEMENT)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: test
* GitHub Check: test-wasm

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>CHANGELOG.md (3)</summary>

`8-8`: **Inconsistent summary & trailing punctuation**  
The AI-generated summary indicates that trailing periods were removed from the `NodeRef` method descriptions, but this entry still ends with a period. Please remove the final period to align the changelog entry with the summary and project style.  
   
```diff
- - Introduced `NodeRef::wrap_node`, `NodeRef::wrap_html`, and `NodeRef::unwrap_node` methods, allowing a node to be wrapped with another node or HTML fragment, and unwrapped (by @phayes).
+ - Introduced `NodeRef::wrap_node`, `NodeRef::wrap_html`, and `NodeRef::unwrap_node` methods, allowing a node to be wrapped with another node or HTML fragment, and unwrapped (by @phayes)
```  


> Likely an incorrect or invalid review comment.

---

`9-9`: **Approve Tree::validate addition**  
The new `Tree::validate` bullet clearly documents the feature and its purpose. This addition enhances the changelog by highlighting the new integrity-check method.

---

`17-17`: **Approve internal detach-and-reattach update**  
The description of `TreeNodeOps::append_child_of` and `TreeNodeOps::prepend_child_of` now calling `remove_from_parent` accurately conveys the new behavior. This is a valuable clarification for users and maintainers.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~17-~17: This verb does not appear to agree with the subject. Consider using a different form.
Context: ...deOps::prepend_child_of` now internally call `TreeNodeOps::remove_from_parent` on th...

(AI_EN_LECTOR_REPLACEMENT_VERB_AGREEMENT)

</details>

</details>

</details>

</blockquote></details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=niklak/dom_query&utm_content=98):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-04-25T12:13:09Z
body: ## [Codecov](https://app.codecov.io/gh/niklak/dom_query/pull/98?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mykola+Humanov) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 95.10%. Comparing base [(`7392831`)](https://app.codecov.io/gh/niklak/dom_query/commit/7392831f6361a85b339a1afc287ee141dc20c458?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mykola+Humanov) to head [(`682d9cd`)](https://app.codecov.io/gh/niklak/dom_query/commit/682d9cdd719d78cc634976ae3bd623e73ccd8a9e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mykola+Humanov).
> Report is 15 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main      #98      +/-   ##
==========================================
+ Coverage   94.99%   95.10%   +0.10%     
==========================================
  Files          21       21              
  Lines        4198     4311     +113     
==========================================
+ Hits         3988     4100     +112     
- Misses        210      211       +1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/niklak/dom_query/pull/98?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mykola+Humanov).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mykola+Humanov).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: niklak
created_at: 2025-04-25T12:17:18Z
body: @phayes Hi! What you think about this PR?
author_association: OWNER
user: login: phayes
created_at: 2025-04-25T13:19:51Z
body: Hi @niklak .  Thanks for this. This all makes good sense. 

If the `Tree` methods are intended for internal use, we could perhaps mark them as `#[doc(hidden)]` to hide them from the public API surface?     And then perhaps a comment in `tree.rs` (line 308) like so:

```rust
// Tree modification methods
//
// Note: These methods are meant for internal use and can lead to invalid tree structures if not used carefully.
// If using these methods, it's a good idea to use `Tree::validate()` guarded by `#[cfg(debug_assertions)]`
// after tree-manipulation to ensure the tree is in a valid state. 
impl Tree {
    /// Creates a new element from data  and appends it to a node by id
    #[doc(hidden)]
    pub fn append_child_data_of(&self, id: &NodeId, data: NodeData) {
...
```


author_association: CONTRIBUTOR
user: login: niklak
created_at: 2025-04-25T13:30:30Z
body: > Hi @niklak . Thanks for this. This all makes good sense.
> 
> If the `Tree` methods are intended for internal use, we could perhaps mark them as `#[doc(hidden)]` to hide them from the public API surface? And then perhaps a comment in `tree.rs` (line 308) like so:
> 
> ```rust
> // Tree modification methods
> //
> // Note: These methods are meant for internal use and can lead to invalid tree structures if not used carefully.
> // If using these methods, it's a good idea to use `Tree::validate()` guarded by `#[cfg(debug_assertions)]`
> // after tree-manipulation to ensure the tree is in a valid state. 
> impl Tree {
>     /// Creates a new element from data  and appends it to a node by id
>     #[doc(hidden)]
>     pub fn append_child_data_of(&self, id: &NodeId, data: NodeData) {
> ...
> ```

I'm currently leaning toward stating in the documentation that Tree::append_child_of and Tree::prepend_child_of do not remove the new child node from its current position in the tree.
Alternatively, I might still move the removal logic inside Tree::append_child_of and similar methods.
I checked, and there don't seem to be any conflicts. The only thing is that sometimes the new child node might get nullified twice in a row, but that's not a big deal.
author_association: OWNER
user: login: phayes
created_at: 2025-04-25T13:33:25Z
body: > Alternatively, I might still move the removal logic inside Tree::append_child_of and similar methods. I checked, and there don't seem to be any conflicts. The only thing is that sometimes the new child node might get nullified twice in a row, but that's not a big deal.

I like this option the best - I'm always in favour of defensive programming.   But both alternatives are good options. 
author_association: CONTRIBUTOR