dom-cat 0.1.0

Persistent DOM model: arena-backed Node tree with mutation API and CSS-selector matching. Consumes html-cat trees; selectors via css-cat. No mut, no Rc/Arc, no interior mutability, no panics, exhaustive matches. Third sub-crate of a Servo-replacement webview runtime targeting Tauri.
//! Persistent mutation API: `append_child`, `remove_child`,
//! `set_attribute`, `remove_attribute`, `replace_text`.
//!
//! Every mutation returns a fresh [`Document`] threaded through the
//! arena.  No `mut` and no interior mutability are used.

use crate::document::Document;
use crate::error::Error;
use crate::node::{Arena, Node, NodeId};

/// Append `child` to `parent`'s child list.
///
/// # Errors
///
/// [`Error::NodeNotFound`] if either id is absent, [`Error::NotAnElement`]
/// if `parent` is not an element or document, [`Error::AlreadyParented`]
/// if `child` already has a parent.
pub fn append_child(doc: Document, parent: NodeId, child: NodeId) -> Result<Document, Error> {
    let parent_node = doc
        .get(parent)
        .cloned()
        .ok_or(Error::NodeNotFound { id: parent.raw() })?;
    let child_node = doc
        .get(child)
        .cloned()
        .ok_or(Error::NodeNotFound { id: child.raw() })?;
    if child_node.parent().is_some() {
        return Err(Error::AlreadyParented { child: child.raw() });
    }
    if !matches!(parent_node, Node::Element(_) | Node::Document(_)) {
        return Err(Error::NotAnElement { id: parent.raw() });
    }
    let new_children: Vec<NodeId> = parent_node
        .children()
        .iter()
        .copied()
        .chain(std::iter::once(child))
        .collect();
    let updated_parent = parent_node.with_children(new_children);
    let updated_child = child_node.with_parent(Some(parent));
    let arena = doc.arena().clone();
    let arena = store_or_fail(arena, parent, updated_parent)?;
    let arena = store_or_fail(arena, child, updated_child)?;
    Ok(doc.with_arena(arena))
}

/// Remove `child` from `parent`'s child list and the arena.
///
/// # Errors
///
/// [`Error::NodeNotFound`] if either id is absent.
pub fn remove_child(doc: Document, parent: NodeId, child: NodeId) -> Result<Document, Error> {
    let parent_node = doc
        .get(parent)
        .cloned()
        .ok_or(Error::NodeNotFound { id: parent.raw() })?;
    let new_children: Vec<NodeId> = parent_node
        .children()
        .iter()
        .copied()
        .filter(|id| *id != child)
        .collect();
    let updated_parent = parent_node.with_children(new_children);
    let arena = doc.arena().clone();
    let arena = store_or_fail(arena, parent, updated_parent)?;
    let arena = remove_subtree(arena, child);
    Ok(doc.with_arena(arena))
}

fn remove_subtree(arena: Arena, root: NodeId) -> Arena {
    let descendants = arena
        .get(root)
        .map_or(Vec::new(), |n| n.children().to_vec());
    let arena = descendants.into_iter().fold(arena, remove_subtree);
    arena.remove(root).unwrap_or_else(|a| a)
}

/// Set (or insert) `name` to `value` on `element_id`.
///
/// # Errors
///
/// [`Error::NodeNotFound`] / [`Error::NotAnElement`].
pub fn set_attribute(
    doc: Document,
    element_id: NodeId,
    name: &str,
    value: &str,
) -> Result<Document, Error> {
    let element_data = element_data_of(&doc, element_id)?;
    let updated_attrs: Vec<(String, String)> = element_data
        .attributes()
        .iter()
        .filter(|(k, _)| !k.eq_ignore_ascii_case(name))
        .cloned()
        .chain(std::iter::once((name.to_owned(), value.to_owned())))
        .collect();
    let updated = Node::Element(element_data.with_attributes(updated_attrs));
    let arena = store_or_fail(doc.arena().clone(), element_id, updated)?;
    Ok(doc.with_arena(arena))
}

/// Remove `name` from `element_id`'s attributes if present.
///
/// # Errors
///
/// [`Error::NodeNotFound`] / [`Error::NotAnElement`].
pub fn remove_attribute(doc: Document, element_id: NodeId, name: &str) -> Result<Document, Error> {
    let element_data = element_data_of(&doc, element_id)?;
    let updated_attrs: Vec<(String, String)> = element_data
        .attributes()
        .iter()
        .filter(|(k, _)| !k.eq_ignore_ascii_case(name))
        .cloned()
        .collect();
    let updated = Node::Element(element_data.with_attributes(updated_attrs));
    let arena = store_or_fail(doc.arena().clone(), element_id, updated)?;
    Ok(doc.with_arena(arena))
}

/// Replace the content of a text node.
///
/// # Errors
///
/// [`Error::NodeNotFound`] if `text_id` is missing or not a text node.
pub fn replace_text(doc: Document, text_id: NodeId, content: &str) -> Result<Document, Error> {
    let node = doc
        .get(text_id)
        .cloned()
        .ok_or(Error::NodeNotFound { id: text_id.raw() })?;
    match node {
        Node::Text(data) => {
            let updated = Node::Text(data.with_content(content.to_owned()));
            let arena = store_or_fail(doc.arena().clone(), text_id, updated)?;
            Ok(doc.with_arena(arena))
        }
        Node::Document(_) | Node::Element(_) | Node::Comment(_) => {
            Err(Error::NotAnElement { id: text_id.raw() })
        }
    }
}

fn element_data_of(doc: &Document, id: NodeId) -> Result<crate::node::ElementData, Error> {
    match doc.get(id) {
        Some(Node::Element(e)) => Ok(e.clone()),
        Some(_other) => Err(Error::NotAnElement { id: id.raw() }),
        None => Err(Error::NodeNotFound { id: id.raw() }),
    }
}

fn store_or_fail(arena: Arena, id: NodeId, node: Node) -> Result<Arena, Error> {
    arena
        .store(id, node)
        .map_err(|_| Error::NodeNotFound { id: id.raw() })
}