use crate::dom::Dom;
use crate::dom_string_map::{DomStringMap, DomStringMapMut};
use crate::error::{DomError, Result};
use crate::node::{NodeData, NodeType};
use crate::node_id::NodeId;
use crate::node_list::NodeList;
use crate::node_or_string::NodeOrString;
use crate::token_list::{DomTokenList, DomTokenListMut};
use crate::tree::AdjacentPosition;
#[derive(Clone, Copy)]
pub struct NodeRef<'a, Ext: 'static = ()> {
pub(crate) dom: &'a Dom<Ext>,
pub(crate) id: NodeId,
}
impl<'a, Ext: 'static> NodeRef<'a, Ext> {
pub fn id(&self) -> NodeId {
self.id
}
pub fn dom(&self) -> &'a Dom<Ext> {
self.dom
}
pub fn node_type(&self) -> NodeType {
self.dom.get_node(self.id).map(|n| n.node_type()).unwrap()
}
pub fn node_name(&self) -> &'a str {
let n = self.dom.get_node(self.id).unwrap();
match &n.data {
NodeData::Element { tag, .. } => tag,
NodeData::Text { .. } => "#text",
NodeData::Comment { .. } => "#comment",
NodeData::Fragment => "#document-fragment",
}
}
pub fn tag_name(&self) -> Option<&'a str> {
self.dom.get_node(self.id).and_then(|n| n.tag_name())
}
pub fn ext(&self) -> Option<&'a Ext> {
match &self.dom.get_node(self.id)?.data {
NodeData::Element { ext, .. } => Some(ext),
_ => None,
}
}
pub fn node_value(&self) -> Option<&'a str> {
match &self.dom.get_node(self.id)?.data {
NodeData::Text { data } | NodeData::Comment { data } => Some(data),
_ => None,
}
}
pub fn data(&self) -> Option<&'a str> {
self.node_value()
}
pub fn text_content(&self) -> String {
self.dom.text_content(self.id)
}
pub fn parent_node(&self) -> Option<NodeRef<'a, Ext>> {
let p = self.dom.get_node(self.id)?.parent?;
Some(NodeRef {
dom: self.dom,
id: p,
})
}
pub fn parent_element(&self) -> Option<NodeRef<'a, Ext>> {
let mut current = self.parent_node();
while let Some(p) = current {
if p.node_type() == NodeType::Element {
return Some(p);
}
current = p.parent_node();
}
None
}
pub fn first_child(&self) -> Option<NodeRef<'a, Ext>> {
let f = self.dom.get_node(self.id)?.first_child?;
Some(NodeRef {
dom: self.dom,
id: f,
})
}
pub fn last_child(&self) -> Option<NodeRef<'a, Ext>> {
let l = self.dom.get_node(self.id)?.last_child?;
Some(NodeRef {
dom: self.dom,
id: l,
})
}
pub fn previous_sibling(&self) -> Option<NodeRef<'a, Ext>> {
let p = self.dom.get_node(self.id)?.prev_sibling?;
Some(NodeRef {
dom: self.dom,
id: p,
})
}
pub fn next_sibling(&self) -> Option<NodeRef<'a, Ext>> {
let n = self.dom.get_node(self.id)?.next_sibling?;
Some(NodeRef {
dom: self.dom,
id: n,
})
}
pub fn has_child_nodes(&self) -> bool {
self.dom
.get_node(self.id)
.and_then(|n| n.first_child)
.is_some()
}
pub fn child_nodes(&self) -> ChildIter<'a, Ext> {
ChildIter {
dom: self.dom,
next: self.dom.get_node(self.id).and_then(|n| n.first_child),
}
}
pub fn first_element_child(&self) -> Option<NodeRef<'a, Ext>> {
let mut c = self.first_child();
while let Some(n) = c {
if n.node_type() == NodeType::Element {
return Some(n);
}
c = n.next_sibling();
}
None
}
pub fn last_element_child(&self) -> Option<NodeRef<'a, Ext>> {
let mut c = self.last_child();
while let Some(n) = c {
if n.node_type() == NodeType::Element {
return Some(n);
}
c = n.previous_sibling();
}
None
}
pub fn previous_element_sibling(&self) -> Option<NodeRef<'a, Ext>> {
let mut s = self.previous_sibling();
while let Some(n) = s {
if n.node_type() == NodeType::Element {
return Some(n);
}
s = n.previous_sibling();
}
None
}
pub fn next_element_sibling(&self) -> Option<NodeRef<'a, Ext>> {
let mut s = self.next_sibling();
while let Some(n) = s {
if n.node_type() == NodeType::Element {
return Some(n);
}
s = n.next_sibling();
}
None
}
pub fn children(&self) -> ElementChildIter<'a, Ext> {
ElementChildIter {
inner: self.child_nodes(),
}
}
pub fn child_element_count(&self) -> usize {
self.children().count()
}
pub fn id_attr(&self) -> Option<&'a str> {
self.get_attribute("id")
}
pub fn get_attribute(&self, key: &str) -> Option<&'a str> {
self.dom.get_attribute(self.id, key)
}
pub fn has_attribute(&self, key: &str) -> bool {
self.dom.has_attribute(self.id, key)
}
pub fn has_class(&self, class: &str) -> bool {
self.dom.has_class(self.id, class)
}
pub fn attributes(&self) -> impl Iterator<Item = (&'a str, &'a str)> {
self.dom.attributes(self.id)
}
pub fn class_name(&self) -> &'a str {
self.get_attribute("class").unwrap_or("")
}
pub fn class_list(&self) -> DomTokenList {
DomTokenList::from_tokens(self.dom.class_list(self.id).map(str::to_owned))
}
pub fn contains(&self, other: NodeId) -> bool {
self.dom.is_ancestor(self.id, other)
}
pub fn is_same_node(&self, other: NodeId) -> bool {
self.id == other
}
pub fn is_connected(&self) -> bool {
self.dom.is_ancestor(self.dom.root(), self.id)
}
pub fn get_root_node(&self) -> NodeRef<'a, Ext> {
let mut cur = self.id;
loop {
match self.dom.get_node(cur).and_then(|n| n.parent) {
Some(p) => cur = p,
None => {
return NodeRef {
dom: self.dom,
id: cur,
};
}
}
}
}
pub fn matches(&self, selector: &str) -> bool {
self.dom.matches(self.id, selector).unwrap_or(false)
}
pub fn closest(&self, selector: &str) -> Option<NodeRef<'a, Ext>> {
self.dom
.closest(self.id, selector)
.ok()
.flatten()
.map(|id| NodeRef { dom: self.dom, id })
}
pub fn query_selector(&self, selector: &str) -> Option<NodeRef<'a, Ext>> {
self.dom
.query_selector_in(self.id, selector)
.ok()
.flatten()
.filter(|&id| id != self.id)
.map(|id| NodeRef { dom: self.dom, id })
}
pub fn query_selector_all(&self, selector: &str) -> NodeList<'a, Ext> {
let ids = self
.dom
.query_selector_all_in(self.id, selector)
.unwrap_or_default()
.into_iter()
.filter(|&id| id != self.id);
NodeList::from_ids(self.dom, ids)
}
pub fn dataset(&self) -> DomStringMap<'a, Ext> {
DomStringMap::new(NodeRef {
dom: self.dom,
id: self.id,
})
}
pub fn tab_index(&self) -> Option<i32> {
self.get_attribute("tabindex")?.parse().ok()
}
pub fn hidden(&self) -> bool {
self.has_attribute("hidden")
}
pub fn content_editable(&self) -> &'a str {
self.get_attribute("contenteditable").unwrap_or("inherit")
}
pub fn inner_html(&self) -> String {
self.dom.inner_markup(self.id)
}
pub fn outer_html(&self) -> String {
self.dom.outer_markup(self.id)
}
}
pub struct NodeMut<'a, Ext: 'static = ()> {
pub(crate) dom: &'a mut Dom<Ext>,
pub(crate) id: NodeId,
}
impl<'a, Ext> NodeMut<'a, Ext> {
pub fn id(&self) -> NodeId {
self.id
}
pub fn as_ref(&self) -> NodeRef<'_, Ext> {
NodeRef {
dom: self.dom,
id: self.id,
}
}
pub fn dom_mut(&mut self) -> &mut Dom<Ext> {
self.dom
}
pub fn into_dom_mut(self) -> &'a mut Dom<Ext> {
self.dom
}
pub fn ext_mut(&mut self) -> Option<&mut Ext> {
match &mut self.dom.get_node_mut(self.id)?.data {
NodeData::Element { ext, .. } => Some(ext),
_ => None,
}
}
}
impl<'a, Ext: 'static> NodeMut<'a, Ext> {
pub fn set_attribute(&mut self, key: &str, value: &str) -> Result<()> {
self.dom.set_attribute(self.id, key, value)
}
pub fn remove_attribute(&mut self, key: &str) -> Result<bool> {
self.dom.remove_attribute(self.id, key)
}
pub fn toggle_attribute(&mut self, key: &str) -> Result<bool> {
self.dom.toggle_attribute(self.id, key)
}
pub fn set_id(&mut self, value: &str) -> Result<()> {
self.dom.set_id(self.id, value)
}
pub fn add_class(&mut self, class: &str) -> Result<()> {
self.dom.add_class(self.id, class)
}
pub fn remove_class(&mut self, class: &str) -> Result<bool> {
self.dom.remove_class(self.id, class)
}
pub fn toggle_class(&mut self, class: &str) -> Result<bool> {
self.dom.toggle_class(self.id, class)
}
pub fn replace_class(&mut self, old: &str, new: &str) -> Result<bool> {
self.dom.replace_class(self.id, old, new)
}
pub fn set_class_name(&mut self, value: &str) -> Result<()> {
let existing: Vec<String> = self.dom.class_list(self.id).map(str::to_owned).collect();
for cls in &existing {
self.dom.remove_class(self.id, cls)?;
}
self.dom.set_attribute(self.id, "class", value)?;
for tok in value.split_whitespace() {
self.dom.add_class(self.id, tok)?;
}
Ok(())
}
pub fn class_list_mut(&mut self) -> DomTokenListMut<'_, Ext> {
DomTokenListMut::new(NodeMut {
dom: &mut *self.dom,
id: self.id,
})
}
pub fn dataset_mut(&mut self) -> DomStringMapMut<'_, Ext> {
DomStringMapMut::new(NodeMut {
dom: &mut *self.dom,
id: self.id,
})
}
pub fn set_tab_index(&mut self, value: i32) -> Result<()> {
self.set_attribute("tabindex", &value.to_string())
}
pub fn set_hidden(&mut self, value: bool) -> Result<()> {
if value {
self.set_attribute("hidden", "")
} else {
self.remove_attribute("hidden").map(|_| ())
}
}
pub fn set_content_editable(&mut self, value: &str) -> Result<()> {
self.set_attribute("contenteditable", value)
}
pub fn toggle_attribute_force(&mut self, name: &str, force: Option<bool>) -> Result<bool> {
match force {
Some(true) => {
if !self.as_ref().has_attribute(name) {
self.set_attribute(name, "")?;
}
Ok(true)
}
Some(false) => {
self.remove_attribute(name)?;
Ok(false)
}
None => self.toggle_attribute(name),
}
}
pub fn append_child(&mut self, child: NodeId) -> Result<()> {
self.dom.append_child(self.id, child)
}
pub fn prepend_child(&mut self, child: NodeId) -> Result<()> {
self.dom.prepend_child(self.id, child)
}
pub fn remove_child(&mut self, child: NodeId) -> Result<()> {
self.dom.remove_child(self.id, child)
}
pub fn replace_child(&mut self, old: NodeId, new: NodeId) -> Result<()> {
self.dom.replace_child(self.id, old, new)
}
pub fn insert_before(&mut self, new: NodeId, reference: Option<NodeId>) -> Result<()> {
self.dom.insert_before(self.id, new, reference)
}
pub fn insert_adjacent(&mut self, position: AdjacentPosition, new: NodeId) -> Result<()> {
self.dom.insert_adjacent(self.id, position, new)
}
pub fn clear_children(&mut self) -> Result<()> {
self.dom.clear_children(self.id)
}
pub fn append(&mut self, children: impl IntoIterator<Item = NodeOrString>) -> Result<()> {
let parent = self.id;
for item in children {
let new_id = match item {
NodeOrString::Node(n) => n,
NodeOrString::Text(s) => self.dom.create_text_node(&s),
};
self.dom.append_child(parent, new_id)?;
}
Ok(())
}
pub fn prepend(&mut self, children: impl IntoIterator<Item = NodeOrString>) -> Result<()> {
let parent = self.id;
let reference = self.dom.get_node(parent).and_then(|n| n.first_child);
for item in children {
let new_id = match item {
NodeOrString::Node(n) => n,
NodeOrString::Text(s) => self.dom.create_text_node(&s),
};
self.dom.insert_before(parent, new_id, reference)?;
}
Ok(())
}
pub fn before(&mut self, siblings: impl IntoIterator<Item = NodeOrString>) -> Result<()> {
let id = self.id;
let parent = match self.dom.get_node(id).and_then(|n| n.parent) {
Some(p) => p,
None => return Ok(()),
};
for item in siblings {
let new_id = match item {
NodeOrString::Node(n) => n,
NodeOrString::Text(s) => self.dom.create_text_node(&s),
};
self.dom.insert_before(parent, new_id, Some(id))?;
}
Ok(())
}
pub fn after(&mut self, siblings: impl IntoIterator<Item = NodeOrString>) -> Result<()> {
let id = self.id;
if self.dom.get_node(id).and_then(|n| n.parent).is_none() {
return Ok(());
}
let mut cursor = id;
for item in siblings {
let new_id = match item {
NodeOrString::Node(n) => n,
NodeOrString::Text(s) => self.dom.create_text_node(&s),
};
self.dom
.insert_adjacent(cursor, AdjacentPosition::AfterEnd, new_id)?;
cursor = new_id;
}
Ok(())
}
pub fn replace_children(
&mut self,
children: impl IntoIterator<Item = NodeOrString>,
) -> Result<()> {
let parent = self.id;
self.dom.clear_children(parent)?;
for item in children {
let new_id = match item {
NodeOrString::Node(n) => n,
NodeOrString::Text(s) => self.dom.create_text_node(&s),
};
self.dom.append_child(parent, new_id)?;
}
Ok(())
}
pub fn replace_with(self, siblings: impl IntoIterator<Item = NodeOrString>) -> Result<()> {
let NodeMut { dom, id } = self;
let parent = match dom.get_node(id).and_then(|n| n.parent) {
Some(p) => p,
None => return Ok(()),
};
for item in siblings {
let new_id = match item {
NodeOrString::Node(n) => n,
NodeOrString::Text(s) => dom.create_text_node(&s),
};
dom.insert_before(parent, new_id, Some(id))?;
}
dom.remove_child(parent, id)?;
Ok(())
}
pub fn remove_self(self) -> Result<()> {
let NodeMut { dom, id } = self;
if let Some(parent) = dom.get_node(id).and_then(|n| n.parent) {
dom.remove_child(parent, id)?;
}
Ok(())
}
pub fn set_node_value(&mut self, data: &str) -> Result<()> {
let id = self.id;
let old = match &self.dom.node_or_err(id)?.data {
NodeData::Text { data: d } | NodeData::Comment { data: d } => d.clone(),
NodeData::Element { .. } => {
return Err(DomError::WrongNodeType {
expected: "Text or Comment",
got: NodeType::Element,
});
}
NodeData::Fragment => {
return Err(DomError::WrongNodeType {
expected: "Text or Comment",
got: NodeType::Fragment,
});
}
};
if old == data {
return Ok(());
}
match &mut self.dom.node_mut_or_err(id)?.data {
NodeData::Text { data: d } | NodeData::Comment { data: d } => {
*d = data.to_string();
}
_ => unreachable!("type-checked above"),
}
self.dom
.fire_mutation(crate::Mutation::CharacterDataChanged {
id,
old,
new: data.to_string(),
});
Ok(())
}
pub fn set_data(&mut self, data: &str) -> Result<()> {
self.set_node_value(data)
}
pub fn edit_text(&mut self, start: usize, end: usize, replacement: &str) -> Result<()> {
let id = self.id;
let data = match &self.dom.node_or_err(id)?.data {
NodeData::Text { data: d } | NodeData::Comment { data: d } => d.clone(),
NodeData::Element { .. } => {
return Err(DomError::WrongNodeType {
expected: "Text or Comment",
got: NodeType::Element,
});
}
NodeData::Fragment => {
return Err(DomError::WrongNodeType {
expected: "Text or Comment",
got: NodeType::Fragment,
});
}
};
if start > data.len() || !data.is_char_boundary(start) {
return Err(DomError::InvalidOffset {
node: id,
offset: start,
});
}
let end = end.max(start);
if end > data.len() || !data.is_char_boundary(end) {
return Err(DomError::InvalidOffset {
node: id,
offset: end,
});
}
let mut new_data = String::with_capacity(data.len() - (end - start) + replacement.len());
new_data.push_str(&data[..start]);
new_data.push_str(replacement);
new_data.push_str(&data[end..]);
self.set_node_value(&new_data)
}
}
impl<'a, Ext: Default> NodeMut<'a, Ext> {
pub fn set_text_content(&mut self, text: &str) -> Result<()> {
self.dom.set_text_content(self.id, text)
}
}
pub struct ChildIter<'a, Ext: 'static> {
dom: &'a Dom<Ext>,
next: Option<NodeId>,
}
impl<'a, Ext: 'static> Iterator for ChildIter<'a, Ext> {
type Item = NodeRef<'a, Ext>;
fn next(&mut self) -> Option<Self::Item> {
let current = self.next?;
self.next = self.dom.get_node(current).and_then(|n| n.next_sibling);
Some(NodeRef {
dom: self.dom,
id: current,
})
}
}
pub struct ElementChildIter<'a, Ext: 'static> {
inner: ChildIter<'a, Ext>,
}
impl<'a, Ext: 'static> Iterator for ElementChildIter<'a, Ext> {
type Item = NodeRef<'a, Ext>;
fn next(&mut self) -> Option<Self::Item> {
self.inner
.by_ref()
.find(|n| n.node_type() == NodeType::Element)
}
}
impl<Ext> Dom<Ext> {
pub fn node(&self, id: NodeId) -> NodeRef<'_, Ext> {
NodeRef { dom: self, id }
}
pub fn node_mut(&mut self, id: NodeId) -> NodeMut<'_, Ext> {
NodeMut { dom: self, id }
}
pub fn root_ref(&self) -> NodeRef<'_, Ext> {
self.node(self.root())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build(dom: &mut Dom) -> (NodeId, NodeId, NodeId, NodeId) {
let root = dom.root();
let a = dom.create_element("a");
let b = dom.create_element("b");
let c = dom.create_element("c");
dom.append_child(root, a).unwrap();
dom.append_child(root, b).unwrap();
dom.append_child(root, c).unwrap();
(root, a, b, c)
}
#[test]
fn dom_accessor_returns_owning_dom() {
let mut dom: Dom = Dom::new();
let (root, a, _b, _c) = build(&mut dom);
let node = dom.node(a);
let via_dom = node.dom();
assert_eq!(via_dom.root(), root);
assert_eq!(via_dom.node(a).parent_node().map(|p| p.id()), Some(root));
}
#[test]
fn navigation_via_noderef() {
let mut dom: Dom = Dom::new();
let (root, a, b, c) = build(&mut dom);
let r = dom.node(root);
assert_eq!(r.first_child().unwrap().id(), a);
assert_eq!(r.last_child().unwrap().id(), c);
assert!(r.has_child_nodes());
let br = dom.node(b);
assert_eq!(br.previous_sibling().unwrap().id(), a);
assert_eq!(br.next_sibling().unwrap().id(), c);
assert_eq!(br.parent_node().unwrap().id(), root);
}
#[test]
fn child_nodes_iterator_yields_all_in_order() {
let mut dom: Dom = Dom::new();
let (root, a, b, c) = build(&mut dom);
let ids: Vec<NodeId> = dom.node(root).child_nodes().map(|n| n.id()).collect();
assert_eq!(ids, vec![a, b, c]);
}
#[test]
fn element_child_iter_skips_text_and_comment() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let a = dom.create_element("a");
let t = dom.create_text_node("hi");
let c = dom.create_comment("note");
let b = dom.create_element("b");
dom.append_child(root, a).unwrap();
dom.append_child(root, t).unwrap();
dom.append_child(root, c).unwrap();
dom.append_child(root, b).unwrap();
let ids: Vec<NodeId> = dom.node(root).children().map(|n| n.id()).collect();
assert_eq!(ids, vec![a, b]);
assert_eq!(dom.node(root).first_element_child().unwrap().id(), a);
assert_eq!(dom.node(root).last_element_child().unwrap().id(), b);
assert_eq!(dom.node(root).child_element_count(), 2);
}
#[test]
fn node_name_matches_spec() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let t = dom.create_text_node("hi");
let cm = dom.create_comment("note");
let frag = dom.create_document_fragment();
assert_eq!(dom.node(el).node_name(), "div");
assert_eq!(dom.node(t).node_name(), "#text");
assert_eq!(dom.node(cm).node_name(), "#comment");
assert_eq!(dom.node(frag).node_name(), "#document-fragment");
}
#[test]
fn node_value_for_text_and_comment_only() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let t = dom.create_text_node("hi");
let cm = dom.create_comment("note");
assert_eq!(dom.node(t).node_value(), Some("hi"));
assert_eq!(dom.node(cm).node_value(), Some("note"));
assert_eq!(dom.node(el).node_value(), None);
}
#[test]
fn set_node_value_updates_text() {
let mut dom: Dom = Dom::new();
let t = dom.create_text_node("old");
dom.node_mut(t).set_node_value("new").unwrap();
assert_eq!(dom.node(t).node_value(), Some("new"));
}
#[test]
fn set_node_value_errors_on_element() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
assert!(matches!(
dom.node_mut(el).set_node_value("x").unwrap_err(),
DomError::WrongNodeType { .. }
));
}
#[test]
fn contains_includes_self_and_descendants() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(root, a).unwrap();
dom.append_child(a, b).unwrap();
assert!(dom.node(root).contains(a));
assert!(dom.node(root).contains(b));
assert!(dom.node(a).contains(b));
assert!(!dom.node(b).contains(a));
}
#[test]
fn noderef_mutation_via_nodemut() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
dom.node_mut(el).set_attribute("class", "hero").unwrap();
dom.node_mut(el).add_class("active").unwrap();
assert_eq!(dom.node(el).get_attribute("class"), Some("active hero"));
assert!(dom.node(el).has_class("active"));
assert!(dom.node(el).has_class("hero"));
}
#[test]
fn is_connected_true_for_root_and_attached_nodes() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let el = dom.create_element("div");
dom.append_child(root, el).unwrap();
assert!(dom.node(root).is_connected());
assert!(dom.node(el).is_connected());
}
#[test]
fn is_connected_false_for_detached_subtree() {
let mut dom: Dom = Dom::new();
let detached = dom.create_element("div");
let child = dom.create_element("span");
dom.append_child(detached, child).unwrap();
assert!(!dom.node(detached).is_connected());
assert!(!dom.node(child).is_connected());
}
#[test]
fn get_root_node_returns_doc_root_for_connected() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let el = dom.create_element("div");
let leaf = dom.create_element("span");
dom.append_child(el, leaf).unwrap();
dom.append_child(root, el).unwrap();
assert_eq!(dom.node(leaf).get_root_node().id(), root);
assert_eq!(dom.node(root).get_root_node().id(), root);
}
#[test]
fn get_root_node_returns_detached_subtree_root() {
let mut dom: Dom = Dom::new();
let outer = dom.create_element("div");
let inner = dom.create_element("span");
dom.append_child(outer, inner).unwrap();
assert_eq!(dom.node(inner).get_root_node().id(), outer);
assert_eq!(dom.node(outer).get_root_node().id(), outer);
}
#[test]
fn class_name_returns_raw_attribute_or_empty_string() {
let mut dom: Dom = Dom::new();
let bare = dom.create_element("div");
let styled = dom.create_element("div");
dom.node_mut(styled)
.set_attribute("class", "hero active")
.unwrap();
assert_eq!(dom.node(bare).class_name(), "");
assert_eq!(dom.node(styled).class_name(), "hero active");
}
#[test]
fn class_list_returns_dom_token_list_snapshot() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
dom.node_mut(el).add_class("foo").unwrap();
dom.node_mut(el).add_class("bar").unwrap();
let list: DomTokenList = dom.node(el).class_list();
assert_eq!(list.len(), 2);
assert!(list.contains("foo"));
assert!(list.contains("bar"));
assert!(!list.contains("baz"));
let collected: Vec<&str> = list.iter().collect();
assert_eq!(collected, ["bar", "foo"]); }
#[test]
fn matches_simple_selectors() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("button");
dom.node_mut(el).set_id("go").unwrap();
dom.node_mut(el).add_class("primary").unwrap();
assert!(dom.node(el).matches("button"));
assert!(dom.node(el).matches("#go"));
assert!(dom.node(el).matches(".primary"));
assert!(dom.node(el).matches("button.primary#go"));
assert!(!dom.node(el).matches("div"));
}
#[test]
fn matches_returns_false_on_invalid_selector() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
assert!(!dom.node(el).matches("!!!not a selector"));
}
#[test]
fn closest_returns_self_when_self_matches() {
let mut dom: Dom = Dom::new();
let outer = dom.create_element("section");
let inner = dom.create_element("div");
dom.node_mut(inner).add_class("target").unwrap();
dom.append_child(outer, inner).unwrap();
let hit = dom.node(inner).closest(".target").unwrap();
assert_eq!(hit.id(), inner);
}
#[test]
fn closest_walks_up_ancestors() {
let mut dom: Dom = Dom::new();
let form = dom.create_element("form");
let label = dom.create_element("label");
let input = dom.create_element("input");
dom.append_child(label, input).unwrap();
dom.append_child(form, label).unwrap();
let hit = dom.node(input).closest("form").unwrap();
assert_eq!(hit.id(), form);
}
#[test]
fn closest_returns_none_in_detached_subtree_with_no_match() {
let mut dom: Dom = Dom::new();
let detached = dom.create_element("div");
let child = dom.create_element("span");
dom.append_child(detached, child).unwrap();
assert!(dom.node(child).closest("form").is_none());
}
#[test]
fn closest_returns_none_on_invalid_selector() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
assert!(dom.node(el).closest("!!!").is_none());
}
#[test]
fn query_selector_finds_first_descendant() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let section = dom.create_element("section");
let a = dom.create_element("p");
dom.node_mut(a).add_class("hit").unwrap();
let b = dom.create_element("p");
dom.node_mut(b).add_class("hit").unwrap();
dom.append_child(section, a).unwrap();
dom.append_child(section, b).unwrap();
dom.append_child(root, section).unwrap();
let hit = dom.node(section).query_selector(".hit").unwrap();
assert_eq!(hit.id(), a);
}
#[test]
fn query_selector_returns_none_when_no_descendant_matches() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
assert!(dom.node(el).query_selector(".missing").is_none());
}
#[test]
fn query_selector_excludes_self_per_spec() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
dom.node_mut(el).add_class("foo").unwrap();
assert!(dom.node(el).query_selector(".foo").is_none());
}
#[test]
fn query_selector_all_returns_node_list_in_document_order() {
let mut dom: Dom = Dom::new();
let root = dom.create_element("section");
let a = dom.create_element("p");
let b = dom.create_element("p");
let c = dom.create_element("p");
dom.append_child(root, a).unwrap();
dom.append_child(root, b).unwrap();
dom.append_child(root, c).unwrap();
let list = dom.node(root).query_selector_all("p");
assert_eq!(list.len(), 3);
let ids: Vec<NodeId> = list.iter().map(|n| n.id()).collect();
assert_eq!(ids, vec![a, b, c]);
}
#[test]
fn query_selector_all_returns_empty_on_invalid_selector() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let list = dom.node(el).query_selector_all("!!!");
assert_eq!(list.len(), 0);
}
#[test]
fn set_class_name_replaces_full_class_attribute() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
dom.node_mut(el).add_class("old").unwrap();
dom.node_mut(el).set_class_name("hero active").unwrap();
assert_eq!(dom.node(el).class_name(), "hero active");
let list = dom.node(el).class_list();
assert!(!list.contains("old"));
assert!(list.contains("hero"));
assert!(list.contains("active"));
assert_eq!(list.len(), 2);
}
#[test]
fn set_class_name_empty_clears_classes() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
dom.node_mut(el).add_class("foo").unwrap();
dom.node_mut(el).set_class_name("").unwrap();
assert_eq!(dom.node(el).class_name(), "");
assert_eq!(dom.node(el).class_list().len(), 0);
}
#[test]
fn class_list_mut_returns_mutating_handle() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
{
let mut nm = dom.node_mut(el);
let mut list = nm.class_list_mut();
list.add("foo").unwrap();
list.toggle("bar", Some(true)).unwrap();
}
assert!(dom.node(el).has_class("foo"));
assert!(dom.node(el).has_class("bar"));
}
#[test]
fn toggle_attribute_force_true_is_force_add() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("input");
assert!(
dom.node_mut(el)
.toggle_attribute_force("disabled", Some(true))
.unwrap()
);
assert!(dom.node(el).has_attribute("disabled"));
dom.node_mut(el).set_attribute("disabled", "1").unwrap();
assert!(
dom.node_mut(el)
.toggle_attribute_force("disabled", Some(true))
.unwrap()
);
assert_eq!(dom.node(el).get_attribute("disabled"), Some("1"));
}
#[test]
fn toggle_attribute_force_false_is_force_remove() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("input");
dom.node_mut(el).set_attribute("disabled", "").unwrap();
assert!(
!dom.node_mut(el)
.toggle_attribute_force("disabled", Some(false))
.unwrap()
);
assert!(!dom.node(el).has_attribute("disabled"));
assert!(
!dom.node_mut(el)
.toggle_attribute_force("disabled", Some(false))
.unwrap()
);
}
#[test]
fn toggle_attribute_force_none_flips_state() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("input");
assert!(
dom.node_mut(el)
.toggle_attribute_force("disabled", None)
.unwrap()
);
assert!(dom.node(el).has_attribute("disabled"));
assert!(
!dom.node_mut(el)
.toggle_attribute_force("disabled", None)
.unwrap()
);
assert!(!dom.node(el).has_attribute("disabled"));
}
#[test]
fn toggle_attribute_force_errors_on_non_element() {
let mut dom: Dom = Dom::new();
let t = dom.create_text_node("hi");
assert!(
dom.node_mut(t)
.toggle_attribute_force("disabled", Some(true))
.is_err()
);
}
use crate::NodeOrString;
fn child_ids<Ext: 'static>(dom: &Dom<Ext>, parent: NodeId) -> Vec<NodeId> {
dom.node(parent).child_nodes().map(|n| n.id()).collect()
}
#[test]
fn append_variadic_inserts_in_order_with_text_coercion() {
let mut dom: Dom = Dom::new();
let parent = dom.create_element("p");
let strong = dom.create_element("strong");
let em = dom.create_element("em");
dom.node_mut(parent)
.append([
"hello ".into(),
strong.into(),
" mid ".into(),
em.into(),
NodeOrString::Text("!".into()),
])
.unwrap();
let ids = child_ids(&dom, parent);
assert_eq!(ids.len(), 5);
assert_eq!(ids[1], strong);
assert_eq!(ids[3], em);
assert_eq!(dom.node(ids[0]).node_value(), Some("hello "));
assert_eq!(dom.node(ids[2]).node_value(), Some(" mid "));
assert_eq!(dom.node(ids[4]).node_value(), Some("!"));
}
#[test]
fn prepend_variadic_inserts_in_order_before_existing_children() {
let mut dom: Dom = Dom::new();
let parent = dom.create_element("ul");
let existing = dom.create_element("li");
dom.append_child(parent, existing).unwrap();
let a = dom.create_element("li");
let b = dom.create_element("li");
dom.node_mut(parent)
.prepend([a.into(), "x".into(), b.into()])
.unwrap();
let ids = child_ids(&dom, parent);
assert_eq!(ids.len(), 4);
assert_eq!(ids[0], a);
assert_eq!(dom.node(ids[1]).node_value(), Some("x"));
assert_eq!(ids[2], b);
assert_eq!(ids[3], existing);
}
#[test]
fn before_inserts_siblings_in_order_preceding_self() {
let mut dom: Dom = Dom::new();
let parent = dom.create_element("div");
let target = dom.create_element("span");
let after = dom.create_element("span");
dom.append_child(parent, target).unwrap();
dom.append_child(parent, after).unwrap();
let a = dom.create_element("p");
let b = dom.create_element("p");
dom.node_mut(target)
.before([a.into(), "hi".into(), b.into()])
.unwrap();
let ids = child_ids(&dom, parent);
assert_eq!(ids.len(), 5);
assert_eq!(ids[0], a);
assert_eq!(dom.node(ids[1]).node_value(), Some("hi"));
assert_eq!(ids[2], b);
assert_eq!(ids[3], target);
assert_eq!(ids[4], after);
}
#[test]
fn after_inserts_siblings_in_order_following_self() {
let mut dom: Dom = Dom::new();
let parent = dom.create_element("div");
let before = dom.create_element("span");
let target = dom.create_element("span");
let tail = dom.create_element("span");
dom.append_child(parent, before).unwrap();
dom.append_child(parent, target).unwrap();
dom.append_child(parent, tail).unwrap();
let a = dom.create_element("p");
let b = dom.create_element("p");
dom.node_mut(target)
.after([a.into(), "hi".into(), b.into()])
.unwrap();
let ids = child_ids(&dom, parent);
assert_eq!(ids.len(), 6);
assert_eq!(ids[0], before);
assert_eq!(ids[1], target);
assert_eq!(ids[2], a);
assert_eq!(dom.node(ids[3]).node_value(), Some("hi"));
assert_eq!(ids[4], b);
assert_eq!(ids[5], tail);
}
#[test]
fn before_after_silently_noop_on_parentless_node() {
let mut dom: Dom = Dom::new();
let detached = dom.create_element("div");
let extra = dom.create_element("p");
dom.node_mut(detached).before([extra.into()]).unwrap();
dom.node_mut(detached).after([extra.into()]).unwrap();
assert!(dom.node(detached).parent_node().is_none());
}
#[test]
fn replace_children_clears_existing_and_appends_new() {
let mut dom: Dom = Dom::new();
let parent = dom.create_element("div");
let old_a = dom.create_element("a");
let old_b = dom.create_element("b");
dom.append_child(parent, old_a).unwrap();
dom.append_child(parent, old_b).unwrap();
let new_a = dom.create_element("i");
let new_b = dom.create_element("u");
dom.node_mut(parent)
.replace_children([new_a.into(), "mid".into(), new_b.into()])
.unwrap();
let ids = child_ids(&dom, parent);
assert_eq!(ids.len(), 3);
assert_eq!(ids[0], new_a);
assert_eq!(dom.node(ids[1]).node_value(), Some("mid"));
assert_eq!(ids[2], new_b);
}
#[test]
fn replace_with_inserts_siblings_then_detaches_self() {
let mut dom: Dom = Dom::new();
let parent = dom.create_element("div");
let target = dom.create_element("span");
let tail = dom.create_element("span");
dom.append_child(parent, target).unwrap();
dom.append_child(parent, tail).unwrap();
let r1 = dom.create_element("p");
let r2 = dom.create_element("p");
dom.node_mut(target)
.replace_with([r1.into(), "mid".into(), r2.into()])
.unwrap();
let ids = child_ids(&dom, parent);
assert_eq!(ids.len(), 4);
assert_eq!(ids[0], r1);
assert_eq!(dom.node(ids[1]).node_value(), Some("mid"));
assert_eq!(ids[2], r2);
assert_eq!(ids[3], tail);
assert!(dom.node(target).parent_node().is_none());
}
#[test]
fn replace_with_on_parentless_node_is_silent_noop() {
let mut dom: Dom = Dom::new();
let detached = dom.create_element("div");
let extra = dom.create_element("p");
dom.node_mut(detached).replace_with([extra.into()]).unwrap();
assert!(dom.node(detached).parent_node().is_none());
assert!(dom.node(extra).parent_node().is_none());
}
#[test]
fn remove_self_detaches_from_parent() {
let mut dom: Dom = Dom::new();
let parent = dom.create_element("div");
let target = dom.create_element("span");
let sibling = dom.create_element("span");
dom.append_child(parent, target).unwrap();
dom.append_child(parent, sibling).unwrap();
dom.node_mut(target).remove_self().unwrap();
let ids = child_ids(&dom, parent);
assert_eq!(ids, vec![sibling]);
assert!(dom.node(target).parent_node().is_none());
}
#[test]
fn remove_self_on_parentless_node_is_silent_noop() {
let mut dom: Dom = Dom::new();
let detached = dom.create_element("div");
dom.node_mut(detached).remove_self().unwrap();
assert!(dom.contains(detached));
}
#[test]
fn dataset_round_trips_camelcase_to_kebab() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
{
let mut nm = dom.node_mut(el);
let mut ds = nm.dataset_mut();
ds.set("fooBar", "yes").unwrap();
ds.set("x", "1").unwrap();
}
assert_eq!(dom.node(el).get_attribute("data-foo-bar"), Some("yes"));
assert_eq!(dom.node(el).get_attribute("data-x"), Some("1"));
let ds = dom.node(el).dataset();
assert_eq!(ds.get("fooBar"), Some("yes"));
assert_eq!(ds.get("x"), Some("1"));
assert_eq!(ds.len(), 2);
}
#[test]
fn tab_index_returns_parsed_attribute_or_none() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("input");
assert_eq!(dom.node(el).tab_index(), None);
dom.node_mut(el).set_tab_index(0).unwrap();
assert_eq!(dom.node(el).tab_index(), Some(0));
dom.node_mut(el).set_tab_index(-1).unwrap();
assert_eq!(dom.node(el).tab_index(), Some(-1));
dom.node_mut(el)
.set_attribute("tabindex", "not a number")
.unwrap();
assert_eq!(dom.node(el).tab_index(), None);
}
#[test]
fn hidden_reads_attribute_presence() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
assert!(!dom.node(el).hidden());
dom.node_mut(el).set_hidden(true).unwrap();
assert!(dom.node(el).hidden());
assert!(dom.node(el).has_attribute("hidden"));
dom.node_mut(el).set_hidden(false).unwrap();
assert!(!dom.node(el).hidden());
assert!(!dom.node(el).has_attribute("hidden"));
}
#[test]
fn content_editable_defaults_to_inherit() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
assert_eq!(dom.node(el).content_editable(), "inherit");
dom.node_mut(el).set_content_editable("true").unwrap();
assert_eq!(dom.node(el).content_editable(), "true");
dom.node_mut(el)
.set_content_editable("plaintext-only")
.unwrap();
assert_eq!(dom.node(el).content_editable(), "plaintext-only");
}
#[test]
fn inner_html_and_outer_html_getters_delegate_to_markup() {
let mut dom: Dom = Dom::new();
let parent = dom.create_element("div");
dom.node_mut(parent).set_id("hero").unwrap();
let child = dom.create_element("span");
let text = dom.create_text_node("hi");
dom.append_child(child, text).unwrap();
dom.append_child(parent, child).unwrap();
assert_eq!(dom.node(parent).inner_html(), "<span>hi</span>");
assert_eq!(
dom.node(parent).outer_html(),
r#"<div id="hero"><span>hi</span></div>"#
);
}
}