use oxml::tree::{Document, NodeError, NodeKind};
use oxml::{NodeId, SerialiseError, parse};
fn find(d: &Document, local: &str) -> NodeId {
d.nodes()
.find(|n| d.element_name(*n).is_some_and(|e| e.local == local))
.unwrap_or_else(|| panic!("no element named {local}"))
}
fn built() -> (Document, NodeId) {
let mut doc = Document::empty();
let root = doc.root();
let top = doc.append_element(root, None, "top").expect("live");
(doc, top)
}
#[test]
fn invalid_and_reserved_namespace_uris_are_refused() {
let (mut doc, top) = built();
for uri in ["\0", "http://www.w3.org/2000/xmlns/"] {
assert!(doc.append_element(top, Some(uri), "x").is_err());
assert!(doc.set_attribute(top, Some(uri), "x", "v").is_err());
assert!(doc.declare_namespace(top, Some("p"), uri).is_err());
}
assert!(
doc.set_attribute(top, None, "xmlns", "urn:changed")
.is_err()
);
assert!(
doc.declare_namespace(
top,
Some("p"),
"http://www.w3.org/XML/1998/namespace"
)
.is_err()
);
}
#[test]
fn text_placement_is_checked_before_every_edit() {
let (mut doc, top) = built();
assert!(doc.append_text(doc.root(), "outside").is_err());
assert!(doc.append_text(doc.root(), "\r").is_err());
let text = doc.append_text(top, "inside").unwrap();
assert!(doc.reparent(text, doc.root()).is_err());
assert!(doc.insert_at(doc.root(), 0, text).is_err());
assert_eq!(doc.parent(text), Some(top));
let space = doc.append_text(doc.root(), " ").unwrap();
assert!(doc.set_text(space, "outside").is_err());
assert_eq!(doc.text(space), " ");
let _ = round_trip(&doc);
}
#[test]
fn fallible_serialization_requires_a_root_element() {
assert!(Document::empty().try_to_xml().is_err());
let (mut doc, top) = built();
doc.remove(top).unwrap();
assert!(doc.try_to_xml().is_err());
}
fn round_trip(doc: &Document) -> Document {
let once = doc.to_xml();
let again = parse(&once)
.unwrap_or_else(|e| panic!("output does not parse: {e}\n{once}"));
assert_eq!(again.to_xml(), once, "not a fixed point");
again
}
#[test]
fn removing_an_attribute_node_then_its_element_frees_each_slot_once() {
let (mut doc, top) = built();
let holder = doc.append_element(top, None, "holder").expect("live");
doc.set_attribute(holder, None, "k", "v").expect("live");
let attr = doc.attribute_nodes(holder)[0];
doc.remove(attr).expect("an attribute node can be removed");
assert!(
doc.attribute_nodes(holder).is_empty(),
"unlinked from owner"
);
doc.remove(holder).expect("live");
let a = doc.append_element(top, None, "a").expect("live");
let b = doc.append_element(top, None, "b").expect("live");
assert_ne!(a, b, "two allocations handed out one slot");
assert_eq!(doc.element_name(a).map(|n| n.local.as_str()), Some("a"));
assert_eq!(doc.element_name(b).map(|n| n.local.as_str()), Some("b"));
assert_eq!(doc.to_xml(), "<top><a/><b/></top>");
}
#[test]
fn removing_an_element_frees_its_namespace_nodes() {
let mut doc =
parse("<r><a xmlns:p=\"urn:p\"><p:b/></a><c/></r>").expect("valid");
let a = find(&doc, "a");
doc.remove(a).expect("live");
for id in doc.nodes() {
if let Some(NodeKind::Namespace { .. }) = doc.kind(id) {
let parent = doc.parent(id).expect("a namespace node has an owner");
assert!(
doc.kind(parent).is_some(),
"namespace node {id:?} survives its element"
);
}
}
assert_eq!(doc.len(), doc.nodes().count(), "len counts live nodes");
}
#[test]
fn only_the_root_and_elements_take_children() {
let (mut doc, top) = built();
let text = doc.append_text(top, "t").expect("live");
doc.set_attribute(top, None, "k", "v").expect("live");
let attr = doc.attribute_nodes(top)[0];
let comment = doc.append_comment(top, "c").expect("live");
for bad in [text, attr, comment] {
assert_eq!(
doc.append_element(bad, None, "e"),
Err(NodeError::NotAContainer)
);
assert_eq!(doc.append_text(bad, "x"), Err(NodeError::NotAContainer));
assert_eq!(doc.build(bad, "e").finish(), Err(NodeError::NotAContainer));
assert!(doc.children(bad).is_empty(), "nothing was attached");
}
let movable = doc.append_element(top, None, "m").expect("live");
assert_eq!(doc.reparent(movable, text), Err(NodeError::NotAContainer));
assert_eq!(
doc.parent(movable),
Some(top),
"a refused move changes nothing"
);
}
#[test]
fn the_document_root_keeps_its_one_element() {
let (mut doc, top) = built();
let inner = doc.append_element(top, None, "inner").expect("live");
let root = doc.root();
assert_eq!(doc.reparent(inner, root), Err(NodeError::RootElementExists));
assert_eq!(doc.remove(root), Err(NodeError::NotRemovable));
assert_eq!(doc.detach(root), Err(NodeError::NotRemovable));
assert_eq!(doc.reparent(root, top), Err(NodeError::NotRemovable));
doc.reparent(top, root).expect("already the root element");
assert_eq!(doc.root_element(), Some(top));
doc.remove(top).expect("live");
assert!(doc.root_element().is_none());
let fresh = doc.append_element(root, None, "fresh").expect("live");
assert_eq!(doc.root_element(), Some(fresh));
assert_eq!(doc.to_xml(), "<fresh/>");
}
#[test]
fn attribute_and_namespace_nodes_are_not_children() {
let (mut doc, top) = built();
doc.set_attribute(top, None, "k", "v").expect("live");
let attr = doc.attribute_nodes(top)[0];
let ns = doc
.declare_namespace(top, Some("p"), "urn:p")
.expect("live element");
let holder = doc.append_element(top, None, "holder").expect("live");
for not_a_child in [attr, ns] {
assert_eq!(
doc.reparent(not_a_child, holder),
Err(NodeError::NotAChild)
);
assert_eq!(
doc.insert_at(holder, 0, not_a_child),
Err(NodeError::NotAChild)
);
assert_eq!(doc.detach(not_a_child), Err(NodeError::NotAChild));
}
assert!(doc.children(holder).is_empty());
assert_eq!(
doc.to_xml(),
"<top xmlns:p=\"urn:p\" k=\"v\"><holder/></top>"
);
}
#[test]
fn names_must_be_ncnames() {
let (mut doc, top) = built();
for bad in ["", "1x", "bad name", "a:b", "<", "-a"] {
assert_eq!(
doc.append_element(top, None, bad),
Err(NodeError::InvalidName),
"element {bad:?}"
);
assert_eq!(
doc.set_attribute(top, None, bad, "v"),
Err(NodeError::InvalidName),
"attribute {bad:?}"
);
if !bad.is_empty() {
assert_eq!(
doc.declare_namespace(top, Some(bad), "urn:x"),
Err(NodeError::InvalidName),
"prefix {bad:?}"
);
}
}
for good in ["a", "_a", "a-b.c", "\u{e9}l\u{e9}ment", "a1"] {
assert!(doc.append_element(top, None, good).is_ok(), "{good:?}");
}
for reserved in ["xml", "xmlns"] {
assert_eq!(
doc.declare_namespace(top, Some(reserved), "urn:x"),
Err(NodeError::InvalidName)
);
}
assert!(doc.attribute_nodes(top).is_empty(), "nothing was stored");
let _ = round_trip(&doc);
}
#[test]
fn text_must_be_xml_characters() {
let (mut doc, top) = built();
for bad in ["\0", "a\u{fffe}", "\u{ffff}"] {
assert_eq!(doc.append_text(top, bad), Err(NodeError::InvalidText));
assert_eq!(
doc.set_attribute(top, None, "k", bad),
Err(NodeError::InvalidText)
);
}
assert!(doc.children(top).is_empty(), "nothing was stored");
for bad in ["a--b", "ends-", "\u{1}"] {
assert_eq!(doc.append_comment(top, bad), Err(NodeError::InvalidText));
}
assert_eq!(doc.append_pi(top, "xml", "d"), Err(NodeError::InvalidName));
assert_eq!(doc.append_pi(top, "t", "a?>b"), Err(NodeError::InvalidText));
let _ = doc
.append_text(top, "ok \u{1} \u{85}")
.expect("expressible");
let again = round_trip(&doc);
assert_eq!(again.text(again.root()), "ok \u{1} \u{85}");
assert!(doc.to_xml().starts_with("<?xml version=\"1.1\"?>"));
}
#[test]
fn a_control_character_in_a_namespace_uri_is_written_as_xml_11() {
let (mut doc, top) = built();
let _ = doc
.declare_namespace(top, Some("r"), "\u{1}")
.expect("expressible");
assert!(doc.to_xml().starts_with("<?xml version=\"1.1\"?>"));
let again = round_trip(&doc);
assert_eq!(again.to_xml(), doc.to_xml());
let mut doc = oxml::tree::Document::empty();
let root = doc.root();
let _ = doc
.append_element(root, Some("urn:\u{1}"), "e")
.expect("expressible");
assert!(doc.to_xml().starts_with("<?xml version=\"1.1\"?>"));
let again = round_trip(&doc);
assert_eq!(again.to_xml(), doc.to_xml());
}
#[test]
fn a_detached_node_does_not_decide_the_version() {
let (mut doc, top) = built();
let gone = doc.append_element(top, None, "gone").expect("live");
let _ = doc.append_text(gone, "\u{1}").expect("expressible");
doc.set_attribute(gone, None, "k", "\u{1}")
.expect("expressible");
let _ = doc
.declare_namespace(gone, Some("p"), "")
.expect("an unbinding");
doc.detach(gone).expect("a child can be detached");
assert_eq!(doc.to_xml(), "<top/>");
let _ = round_trip(&doc);
doc.insert_at(top, 0, gone).expect("live");
assert!(doc.to_xml().starts_with("<?xml version=\"1.1\"?>"));
let _ = round_trip(&doc);
}
#[test]
fn a_line_separator_survives_a_version_11_round_trip() {
let (mut doc, top) = built();
let _ = doc.append_text(top, "a\u{2028}b").expect("live");
doc.set_attribute(top, None, "k", "a\u{2028}b")
.expect("live");
let _ = doc
.declare_namespace(top, Some("p"), "urn:\u{2028}")
.expect("live");
let plain = round_trip(&doc);
assert!(!plain.to_xml().starts_with("<?xml"), "1.0 is enough");
let _ = doc.append_text(top, "\u{1}").expect("expressible");
let again = round_trip(&doc);
assert!(again.to_xml().starts_with("<?xml version=\"1.1\"?>"));
let top = again.root_element().expect("a root element");
assert_eq!(again.attribute(top, "k"), Some("a\u{2028}b"));
}
#[test]
fn a_carriage_return_is_refused_in_a_comment_or_pi() {
let (mut doc, top) = built();
for bad in ["a\rb", "\r\n", "\r"] {
assert_eq!(doc.append_comment(top, bad), Err(NodeError::InvalidText));
assert_eq!(doc.append_pi(top, "t", bad), Err(NodeError::InvalidText));
}
assert!(doc.children(top).is_empty(), "nothing was stored");
let _ = doc
.append_comment(top, "a\nb")
.expect("a newline is literal");
let _ = doc
.append_text(top, "a\rb")
.expect("text takes a reference");
let _ = round_trip(&doc);
}
#[test]
fn content_no_version_can_hold_is_refused() {
let (mut doc, top) = built();
let comment = doc.append_comment(top, "caf\u{85}").expect("legal");
assert_eq!(doc.try_to_xml(), Ok(doc.to_xml()), "1.0 holds it alone");
let _ = doc.append_text(top, "\u{1}").expect("expressible");
assert_eq!(
doc.try_to_xml(),
Err(SerialiseError::VersionConflict {
node: comment,
character: '\u{85}',
})
);
let once = doc.to_xml();
let again = parse(&once).expect("the output parses").to_xml();
assert_ne!(again, once, "the refusal is not over-cautious");
let (mut doc, top) = built();
let pi = doc.append_pi(top, "t", "\u{90}").expect("legal in 1.0");
let _ = doc
.declare_namespace(top, Some("p"), "")
.expect("an unbinding needs 1.1");
assert_eq!(
doc.try_to_xml(),
Err(SerialiseError::VersionConflict {
node: pi,
character: '\u{90}',
})
);
assert!(parse(&doc.to_xml()).is_err(), "the refusal is real");
let parsed = parse("<?xml version=\"1.1\"?><r><!--a\u{2028}b--></r>")
.expect("well-formed 1.1");
assert_eq!(parsed.try_to_xml(), Ok(parsed.to_xml()));
}
#[test]
fn a_namespaced_element_gets_a_declaration_when_written() {
let (mut doc, top) = built();
let e = doc.append_element(top, Some("urn:x"), "e").expect("live");
doc.set_attribute(e, Some("urn:y"), "a", "1").expect("live");
let again = round_trip(&doc);
let e2 = find(&again, "e");
assert_eq!(
again.element_name(e2).and_then(|n| n.namespace.as_deref()),
Some("urn:x")
);
assert_eq!(again.attribute_ns(e2, Some("urn:y"), "a"), Some("1"));
assert_eq!(again.attribute_ns(e2, None, "a"), None);
}
#[test]
fn a_declared_prefix_is_used_by_names_under_it() {
let (mut doc, top) = built();
let _ = doc
.declare_namespace(top, Some("x"), "urn:x")
.expect("live");
let _ = doc.append_element(top, Some("urn:x"), "e").expect("live");
assert_eq!(doc.to_xml(), "<top xmlns:x=\"urn:x\"><x:e/></top>");
let _ = doc
.declare_namespace(top, Some("x"), "urn:z")
.expect("live");
assert_eq!(doc.namespace_nodes(top).len(), 1);
let out = doc.to_xml();
assert!(out.contains("xmlns:x=\"urn:z\""), "{out}");
let again = round_trip(&doc);
assert_eq!(
again
.element_name(find(&again, "e"))
.and_then(|n| n.namespace.as_deref()),
Some("urn:x")
);
}
#[test]
fn a_default_namespace_is_undeclared_for_an_unqualified_child() {
let mut doc = parse("<a xmlns=\"urn:d\"><b/></a>").expect("valid");
let a = doc.root_element().expect("root element");
let _ = doc.append_element(a, None, "plain").expect("live");
let again = round_trip(&doc);
assert_eq!(
again
.element_name(find(&again, "plain"))
.map(|n| n.namespace.clone()),
Some(None),
"{}",
doc.to_xml()
);
}
#[test]
fn reparenting_out_of_a_declaration_scope_still_round_trips() {
let mut doc =
parse("<doc><x xmlns:m=\"urn:m\"><m:b m:k=\"v\"/></x><y/></doc>")
.expect("valid");
let b = find(&doc, "b");
let y = find(&doc, "y");
doc.reparent(b, y).expect("live");
let again = round_trip(&doc);
let b2 = find(&again, "b");
assert_eq!(
again.element_name(b2).and_then(|n| n.namespace.as_deref()),
Some("urn:m")
);
assert_eq!(again.attribute_ns(b2, Some("urn:m"), "k"), Some("v"));
assert_eq!(
again
.element_name(again.parent(b2).expect("has parent"))
.map(|n| n.local.as_str()),
Some("y")
);
}
#[test]
fn the_builder_can_make_namespaced_children() {
let mut doc = Document::empty();
let root = doc.root();
let _ = doc
.build(root, "top")
.child_ns("urn:x", "e", |e| {
let _ = e.attr_ns("urn:x", "a", "1").text("t");
})
.finish()
.expect("live");
let again = round_trip(&doc);
let e = find(&again, "e");
assert_eq!(
again.element_name(e).and_then(|n| n.namespace.as_deref()),
Some("urn:x")
);
assert_eq!(again.attribute_ns(e, Some("urn:x"), "a"), Some("1"));
}
#[test]
fn attributes_are_matched_by_expanded_name_not_prefix() {
let mut doc = parse(
"<r xmlns:p=\"urn:p\" xmlns:q=\"urn:p\"><f p:a=\"1\"/><g q:a=\"2\"/></r>",
)
.expect("valid");
let g = find(&doc, "g");
assert_eq!(doc.attribute_ns(g, Some("urn:p"), "a"), Some("2"));
doc.set_attribute(g, Some("urn:p"), "a", "3").expect("live");
assert_eq!(doc.attribute_nodes(g).len(), 1, "replaced, not duplicated");
assert_eq!(doc.attribute_ns(g, Some("urn:p"), "a"), Some("3"));
assert_eq!(doc.remove_attribute(g, Some("urn:p"), "a"), Ok(true));
assert!(doc.attribute_nodes(g).is_empty());
let _ = round_trip(&doc);
}
fn written_order(doc: &Document) -> Vec<String> {
let xml = doc.to_xml();
xml.split('<')
.filter(|s| !s.is_empty() && !s.starts_with('/') && !s.starts_with('?'))
.map(|s| {
s.trim_end_matches("/>")
.trim_end_matches('>')
.split(' ')
.next()
.unwrap_or("")
.to_owned()
})
.collect()
}
fn keyed_order(doc: &mut Document) -> Vec<String> {
let order = doc.document_order().to_vec();
let mut elements: Vec<(u32, String)> = doc
.nodes()
.filter(|n| doc.is_element(*n))
.map(|n| {
let key = order[n.index()];
assert_ne!(key, u32::MAX, "a reachable element has a key");
(key, doc.element_name(n).expect("element").local.clone())
})
.collect();
elements.sort_unstable();
elements.into_iter().map(|(_, n)| n).collect()
}
#[test]
fn document_order_follows_the_tree_not_the_arena() {
let (mut doc, top) = built();
let a = doc.append_element(top, None, "a").expect("live");
let b = doc.append_element(top, None, "b").expect("live");
let c = doc.append_element(b, None, "c").expect("live");
assert!(!doc.is_order_dirty() || !doc.document_order().is_empty());
doc.reparent(a, c).expect("live");
assert!(doc.is_order_dirty(), "a move invalidates the order");
assert_eq!(keyed_order(&mut doc), written_order(&doc));
assert!(!doc.is_order_dirty());
doc.remove(b).expect("live");
let d = doc.append_element(top, None, "d").expect("live");
doc.insert_at(top, 0, d).expect("live");
let _ = doc.append_element(top, None, "e").expect("live");
assert_eq!(keyed_order(&mut doc), written_order(&doc));
assert_eq!(written_order(&doc), ["top", "d", "e"]);
let names: Vec<_> = doc
.descendants_of(doc.root())
.filter_map(|n| doc.element_name(n).map(|e| e.local.clone()))
.collect();
assert_eq!(names, ["top", "d", "e"]);
}
#[test]
fn an_unmutated_document_is_in_arena_order() {
let doc = parse("<a><b/><c><d/></c></a>").expect("valid");
assert!(!doc.is_order_dirty());
for id in doc.nodes() {
assert_eq!(doc.order_key(id), Some(id.index() as u32));
}
}
#[test]
fn insert_at_places_a_child_by_index() {
let (mut doc, top) = built();
let a = doc.append_element(top, None, "a").expect("live");
let c = doc.append_element(top, None, "c").expect("live");
let b = doc.append_element(a, None, "b").expect("live");
doc.insert_at(top, 1, b).expect("live");
assert_eq!(doc.children(top), [a, b, c]);
assert_eq!(doc.parent(b), Some(top));
assert!(doc.children(a).is_empty());
doc.insert_at(top, 99, a).expect("live");
assert_eq!(doc.children(top), [b, c, a]);
assert_eq!(doc.insert_at(a, 0, top), Err(NodeError::WouldCycle));
let _ = round_trip(&doc);
}
#[test]
fn set_text_replaces_in_place_and_text_content_borrows() {
let (mut doc, top) = built();
let t = doc.append_text(top, "one").expect("live");
assert_eq!(doc.text_content(top), Some("one"));
doc.set_text(t, "two").expect("a text node");
assert_eq!(doc.text_content(top), Some("two"));
assert_eq!(doc.text_content(t), Some("two"));
assert_eq!(doc.set_text(top, "x"), Err(NodeError::NotText));
assert_eq!(doc.set_text(t, "\0"), Err(NodeError::InvalidText));
assert_eq!(
doc.text_content(t),
Some("two"),
"a refusal changes nothing"
);
let _ = doc.append_element(top, None, "e").expect("live");
assert_eq!(doc.text_content(top), None, "no longer a single text child");
let empty = doc.append_element(top, None, "empty").expect("live");
assert_eq!(doc.text_content(empty), Some(""));
}
#[test]
fn comments_and_processing_instructions_can_be_appended() {
let (mut doc, top) = built();
let _ = doc.append_comment(top, " note ").expect("live");
let _ = doc.append_pi(top, "target", "data").expect("live");
let _ = doc.append_pi(top, "bare", "").expect("live");
let _ = doc
.append_comment(doc.root(), "trailing")
.expect("root is a container");
assert_eq!(
doc.to_xml(),
"<top><!-- note --><?target data?><?bare?></top><!--trailing-->"
);
let _ = round_trip(&doc);
}
#[test]
fn detach_unlinks_without_freeing() {
let (mut doc, top) = built();
let a = doc.append_element(top, None, "a").expect("live");
let _ = doc.append_text(a, "kept").expect("live");
doc.detach(a).expect("live");
assert!(doc.children(top).is_empty());
assert_eq!(doc.parent(a), None);
assert_eq!(doc.text(a), "kept", "the subtree is intact");
assert_eq!(doc.to_xml(), "<top/>", "and not written");
let _ = doc.document_order();
assert_eq!(doc.order_key(a), None, "and has no document position");
doc.reparent(a, top)
.expect("a detached subtree can come back");
assert_eq!(doc.to_xml(), "<top><a>kept</a></top>");
doc.detach(a).expect("live");
doc.compact();
assert_eq!(doc.text(a), "kept", "compaction keeps detached subtrees");
doc.remove(a).expect("a detached subtree can be removed");
assert_eq!(doc.len(), doc.nodes().count());
}
#[test]
fn compact_drops_the_garbage_and_changes_nothing_visible() {
let mut doc = parse("<r><a/><b x=\"1\"/><c/></r>").expect("valid");
let r = doc.root_element().expect("root element");
let b = find(&doc, "b");
for _ in 0..100 {
let e = doc.append_element(r, None, "e").expect("live");
doc.remove(e).expect("live");
}
doc.set_attribute(b, None, "y", "2").expect("live");
let before = doc.to_xml();
let ids: Vec<_> = doc.nodes().collect();
doc.compact();
assert_eq!(doc.to_xml(), before);
assert_eq!(doc.nodes().collect::<Vec<_>>(), ids, "ids are stable");
assert_eq!(doc.attribute(b, "y"), Some("2"));
assert!(!doc.is_order_dirty());
assert_eq!(keyed_order(&mut doc), written_order(&doc));
}
#[test]
fn a_cloned_document_is_independent() {
let (mut doc, top) = built();
let a = doc.append_element(top, None, "a").expect("live");
let copy = doc.clone();
doc.remove(a).expect("live");
assert_eq!(copy.to_xml(), "<top><a/></top>");
assert_eq!(doc.to_xml(), "<top/>");
}