use rdom_core::{AdjacentPosition, NodeId, NodeMut};
use crate::error::{ParseError, Result};
use crate::parser::parse_into;
pub trait NodeMutHtml<'a, Ext>
where
Ext: Default + 'static,
{
fn set_inner_html(&mut self, html: &str) -> Result<()>;
fn set_outer_html(self, html: &str) -> Result<NodeId>;
fn insert_adjacent_html(&mut self, position: AdjacentPosition, html: &str) -> Result<()>;
}
impl<'a, Ext> NodeMutHtml<'a, Ext> for NodeMut<'a, Ext>
where
Ext: Default + 'static,
{
fn set_inner_html(&mut self, html: &str) -> Result<()> {
let id = self.id();
let dom = self.dom_mut();
while let Some(first) = dom.node(id).first_child().map(|n| n.id()) {
let _ = dom.drop_subtree(first);
}
parse_into(dom, html, id)?;
Ok(())
}
fn set_outer_html(self, html: &str) -> Result<NodeId> {
let id = self.id();
let dom = self.into_dom_mut();
let parent = match dom.node(id).parent_node() {
Some(p) => p.id(),
None => return Err(synth_error("set_outer_html: receiver has no parent")),
};
let staging = dom.create_document_fragment();
let parsed = parse_into(dom, html, staging)?;
if parsed.is_empty() {
let _ = dom.drop_subtree(staging);
return Err(synth_error("set_outer_html: parsed fragment is empty"));
}
for &child in &parsed {
dom.insert_before(parent, child, Some(id))
.map_err(|e| synth_error(&format!("set_outer_html: {e}")))?;
}
let _ = dom.drop_subtree(staging);
let _ = dom.drop_subtree(id);
Ok(parsed[0])
}
fn insert_adjacent_html(&mut self, position: AdjacentPosition, html: &str) -> Result<()> {
let id = self.id();
let dom = self.dom_mut();
let parent_of_id = dom.node(id).parent_node().map(|n| n.id());
let needs_parent = matches!(
position,
AdjacentPosition::BeforeBegin | AdjacentPosition::AfterEnd
);
if needs_parent && parent_of_id.is_none() {
return Err(synth_error(
"insert_adjacent_html: BeforeBegin/AfterEnd require a parent",
));
}
let (insertion_parent, reference) = match position {
AdjacentPosition::BeforeBegin => (parent_of_id.unwrap(), Some(id)),
AdjacentPosition::AfterBegin => (id, dom.node(id).first_child().map(|n| n.id())),
AdjacentPosition::BeforeEnd => (id, None),
AdjacentPosition::AfterEnd => (
parent_of_id.unwrap(),
dom.node(id).next_sibling().map(|n| n.id()),
),
};
let staging = dom.create_document_fragment();
let parsed = parse_into(dom, html, staging)?;
for child in parsed {
dom.insert_before(insertion_parent, child, reference)
.map_err(|e| synth_error(&format!("insert_adjacent_html: {e}")))?;
}
let _ = dom.drop_subtree(staging);
Ok(())
}
}
fn synth_error(msg: &str) -> ParseError {
ParseError::new(msg.to_string(), 1, 1, 0)
}
#[cfg(test)]
mod tests {
use super::*;
use rdom_core::{Dom, ListenerOptions};
fn child_tags(dom: &Dom, parent: NodeId) -> Vec<String> {
dom.node(parent)
.child_nodes()
.map(|n| n.tag_name().unwrap_or(n.node_name()).to_string())
.collect()
}
fn s(t: &str) -> String {
t.to_string()
}
#[test]
fn set_inner_html_replaces_children() {
let mut dom: Dom = Dom::new();
let parent = dom.create_element("div");
let old = dom.create_element("span");
dom.append_child(parent, old).unwrap();
dom.node_mut(parent)
.set_inner_html("<p>hi</p><em>bye</em>")
.unwrap();
let tags = child_tags(&dom, parent);
assert_eq!(tags, vec![s("p"), s("em")]);
}
#[test]
fn set_inner_html_clears_when_input_empty() {
let mut dom: Dom = Dom::new();
let parent = dom.create_element("div");
let old = dom.create_element("span");
dom.append_child(parent, old).unwrap();
dom.node_mut(parent).set_inner_html("").unwrap();
assert!(!dom.node(parent).has_child_nodes());
assert!(!dom.contains(old));
}
#[test]
fn set_inner_html_drops_abortsignal_listeners_on_removed_children() {
let mut dom: Dom = Dom::new();
let parent = dom.create_element("div");
let child = dom.create_element("button");
dom.append_child(parent, child).unwrap();
dom.add_event_listener(child, "click", ListenerOptions::default(), |_| {})
.unwrap();
assert_eq!(dom.listener_count(child), 1);
dom.node_mut(parent).set_inner_html("<p>new</p>").unwrap();
assert_eq!(dom.listener_count(child), 0);
}
#[test]
fn set_inner_html_propagates_parse_error_strictly() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let err = dom
.node_mut(el)
.set_inner_html("<div><span></p></div>")
.unwrap_err();
assert!(err.msg.to_lowercase().contains("mismatch"));
}
#[test]
fn set_outer_html_replaces_self_in_parent_and_returns_first_id() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let target = dom.create_element("span");
let sibling = dom.create_element("span");
dom.append_child(root, target).unwrap();
dom.append_child(root, sibling).unwrap();
let new_id = dom
.node_mut(target)
.set_outer_html("<p>a</p><em>b</em>")
.unwrap();
assert_eq!(dom.node(new_id).tag_name(), Some("p"));
let tags = child_tags(&dom, root);
assert_eq!(
tags,
vec!["p".to_string(), "em".to_string(), "span".to_string()]
);
assert!(!dom.contains(target));
}
#[test]
fn set_outer_html_errors_when_receiver_has_no_parent() {
let mut dom: Dom = Dom::new();
let detached = dom.create_element("div");
let err = dom
.node_mut(detached)
.set_outer_html("<p>x</p>")
.unwrap_err();
assert!(err.msg.contains("no parent"));
assert!(dom.contains(detached));
}
#[test]
fn set_outer_html_errors_on_empty_parsed_fragment() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let target = dom.create_element("div");
dom.append_child(root, target).unwrap();
let err = dom.node_mut(target).set_outer_html("").unwrap_err();
assert!(err.msg.contains("empty"));
assert!(dom.contains(target));
assert_eq!(dom.node(target).parent_node().map(|p| p.id()), Some(root));
}
#[test]
fn set_outer_html_propagates_parse_error_strictly() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let target = dom.create_element("div");
dom.append_child(root, target).unwrap();
let err = dom
.node_mut(target)
.set_outer_html("<a><b></a>")
.unwrap_err();
assert!(err.msg.to_lowercase().contains("mismatch"));
assert!(dom.contains(target));
}
#[test]
fn insert_adjacent_html_before_begin() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let target = dom.create_element("span");
dom.append_child(root, target).unwrap();
dom.node_mut(target)
.insert_adjacent_html(AdjacentPosition::BeforeBegin, "<a></a><b></b>")
.unwrap();
let tags = child_tags(&dom, root);
assert_eq!(tags, vec![s("a"), s("b"), s("span")]);
}
#[test]
fn insert_adjacent_html_after_begin() {
let mut dom: Dom = Dom::new();
let parent = dom.create_element("div");
let existing = dom.create_element("span");
dom.append_child(parent, existing).unwrap();
dom.node_mut(parent)
.insert_adjacent_html(AdjacentPosition::AfterBegin, "<a></a><b></b>")
.unwrap();
let tags = child_tags(&dom, parent);
assert_eq!(tags, vec![s("a"), s("b"), s("span")]);
}
#[test]
fn insert_adjacent_html_before_end() {
let mut dom: Dom = Dom::new();
let parent = dom.create_element("div");
let existing = dom.create_element("span");
dom.append_child(parent, existing).unwrap();
dom.node_mut(parent)
.insert_adjacent_html(AdjacentPosition::BeforeEnd, "<a></a><b></b>")
.unwrap();
let tags = child_tags(&dom, parent);
assert_eq!(tags, vec![s("span"), s("a"), s("b")]);
}
#[test]
fn insert_adjacent_html_after_end() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let target = dom.create_element("span");
let tail = dom.create_element("span");
dom.append_child(root, target).unwrap();
dom.append_child(root, tail).unwrap();
dom.node_mut(target)
.insert_adjacent_html(AdjacentPosition::AfterEnd, "<a></a><b></b>")
.unwrap();
let tags = child_tags(&dom, root);
assert_eq!(tags, vec![s("span"), s("a"), s("b"), s("span")]);
}
#[test]
fn insert_adjacent_html_before_begin_errors_without_parent() {
let mut dom: Dom = Dom::new();
let detached = dom.create_element("div");
let err = dom
.node_mut(detached)
.insert_adjacent_html(AdjacentPosition::BeforeBegin, "<p/>")
.unwrap_err();
assert!(err.msg.contains("parent"));
}
#[test]
fn insert_adjacent_html_after_end_errors_without_parent() {
let mut dom: Dom = Dom::new();
let detached = dom.create_element("div");
let err = dom
.node_mut(detached)
.insert_adjacent_html(AdjacentPosition::AfterEnd, "<p/>")
.unwrap_err();
assert!(err.msg.contains("parent"));
}
#[test]
fn insert_adjacent_html_propagates_parse_error_strictly() {
let mut dom: Dom = Dom::new();
let parent = dom.create_element("div");
let err = dom
.node_mut(parent)
.insert_adjacent_html(AdjacentPosition::BeforeEnd, "<a><b></a>")
.unwrap_err();
assert!(err.msg.to_lowercase().contains("mismatch"));
}
}