use dom_query::{Document, LocalName, NodeRef, Selection};
pub fn same_tree(a: &NodeRef, b: &NodeRef) -> bool {
std::ptr::eq(a.tree, b.tree)
}
pub fn tag_name(node: &NodeRef) -> String {
node.qual_name_ref()
.map(|name| name.local.as_ref().to_ascii_uppercase())
.unwrap_or_default()
}
pub fn local_name(node: &NodeRef) -> Option<LocalName> {
node.qual_name_ref().map(|name| name.local.clone())
}
pub fn tag_name_is(node: &NodeRef, name: &str) -> bool {
node.qual_name_ref()
.is_some_and(|qual_name| qual_name.local.as_ref().eq_ignore_ascii_case(name))
}
pub fn tag_name_in(node: &NodeRef, names: &[&str]) -> bool {
node.qual_name_ref().is_some_and(|qual_name| {
let local = qual_name.local.as_ref();
names.iter().any(|name| local.eq_ignore_ascii_case(name))
})
}
pub fn text(node: &NodeRef) -> String {
node.text().to_string()
}
pub fn attr(node: &NodeRef, name: &str) -> Option<String> {
node.attr(name).map(|value| value.to_string())
}
pub fn attrs(node: &NodeRef) -> Vec<(String, String)> {
node.attrs()
.into_iter()
.map(|attr| (attr.name.local.to_string(), attr.value.to_string()))
.collect()
}
pub fn is_whitespace_text(node: &NodeRef) -> bool {
node.is_text() && node.text().trim().is_empty()
}
pub fn select<'a>(document: &'a Document, css: &str) -> Vec<NodeRef<'a>> {
document.select(css).nodes().to_vec()
}
pub fn elements_by_tag_name<'a>(node: &NodeRef<'a>, tag: &str) -> Vec<NodeRef<'a>> {
let all_tags = tag == "*";
node.descendants_it()
.filter(|descendant| descendant.is_element() && (all_tags || tag_name_is(descendant, tag)))
.collect()
}
pub fn first_element_by_tag_name<'a>(node: &NodeRef<'a>, tag: &str) -> Option<NodeRef<'a>> {
node.descendants_it()
.find(|descendant| tag_name_is(descendant, tag))
}
pub fn has_any_descendant_tag(node: &NodeRef, tag_names: &[&str]) -> bool {
node.descendants_it()
.any(|descendant| tag_name_in(&descendant, tag_names))
}
pub fn has_text_or_descendant_tag(node: &NodeRef, tag_names: &[&str]) -> bool {
node.descendants_it().any(|descendant| {
tag_name_in(&descendant, tag_names)
|| (descendant.is_text() && !descendant.text().trim().is_empty())
})
}
pub fn has_ancestor_in(node: &NodeRef, nodes: &[NodeRef]) -> bool {
node.ancestors_it(None)
.any(|ancestor| nodes.iter().any(|candidate| candidate.id == ancestor.id))
}
pub fn next_node_within<'a>(
node: &NodeRef<'a>,
root: &NodeRef<'a>,
ignore_self_and_kids: bool,
) -> Option<NodeRef<'a>> {
if !ignore_self_and_kids && let Some(first_child) = node.first_child() {
return Some(first_child);
}
let mut node = *node;
while node.id != root.id {
if let Some(next_sibling) = node.next_sibling() {
return Some(next_sibling);
}
node = node.parent()?;
}
None
}
pub fn remove_and_next_within<'a>(node: &NodeRef<'a>, root: &NodeRef<'a>) -> Option<NodeRef<'a>> {
let next_node = next_node_within(node, root, true);
node.remove_from_parent();
next_node
}
pub fn append_child(parent: &NodeRef, child: &NodeRef) {
debug_assert!(same_tree(parent, child), "append_child across documents");
parent.append_child(child);
}
pub fn replace_with(old: &NodeRef, new: &NodeRef) {
debug_assert!(same_tree(old, new), "replace_with across documents");
old.replace_with(new);
}
pub fn move_into(target: &NodeRef, nodes: Vec<NodeRef>) {
if nodes.is_empty() {
return;
}
Selection::from(*target).append_selection(&Selection::from(nodes));
}
pub fn new_article_document() -> Document {
Document::from("<html><head></head><body><article></article></body></html>")
}
pub fn article_root(document: &Document) -> Option<NodeRef<'_>> {
document
.body()
.and_then(|body| body.first_element_child())
.filter(|node| tag_name_is(node, "article"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tag_names_are_case_insensitive() {
let document = Document::from("<div><svg><foreignObject></foreignObject></svg>text</div>");
let div = select(&document, "div")[0];
assert_eq!(tag_name(&div), "DIV");
assert!(tag_name_is(&div, "div") && tag_name_is(&div, "DIV"));
assert!(tag_name_in(&div, &["p", "div"]) && !tag_name_in(&div, &["p"]));
let foreign = first_element_by_tag_name(&div, "foreignobject").unwrap();
assert_eq!(tag_name(&foreign), "FOREIGNOBJECT");
let text_node = div.last_child().unwrap();
assert!(text_node.is_text());
assert_eq!(tag_name(&text_node), "");
assert!(!tag_name_is(&text_node, ""));
assert!(!tag_name_in(&text_node, &[""]));
}
#[test]
fn walks_the_subtree_only() {
let document = Document::from("<div id=a><p>1<b>2</b></p><p>3</p></div><div id=b>4</div>");
let root = select(&document, "#a")[0];
let mut visited = Vec::new();
let mut node = Some(root);
while let Some(current) = node {
visited.push(if current.is_text() {
text(¤t)
} else {
tag_name(¤t)
});
node = next_node_within(¤t, &root, false);
}
assert_eq!(visited, ["DIV", "P", "1", "B", "2", "P", "3"]);
let first_p = select(&document, "p")[0];
let next = remove_and_next_within(&first_p, &root).unwrap();
assert_eq!(text(&next), "3");
assert_eq!(select(&document, "p").len(), 1);
}
#[test]
fn moves_nodes_across_documents() {
let source = Document::from("<table><tr><td>cell</td></tr></table><p>rest</p>");
let target = new_article_document();
let article = article_root(&target).unwrap();
let tr = select(&source, "tr");
move_into(&article, tr);
assert_eq!(
article.html().as_ref(),
"<article><tr><td>cell</td></tr></article>"
);
assert_eq!(select(&source, "tr").len(), 0);
assert!(select(&source, "p").len() == 1);
}
#[test]
fn nested_nodes_are_moved_once() {
let source = Document::from("<div class=x><p>outer</p><div class=x>inner</div></div>");
let target = new_article_document();
let article = article_root(&target).unwrap();
let divs = select(&source, ".x");
assert!(has_ancestor_in(&divs[1], &divs));
move_into(&article, divs);
let html = article.html();
assert_eq!(html.matches("inner").count(), 1, "{html}");
assert_eq!(select(&target, "article > div").len(), 2, "{html}");
}
}