use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NodeId(pub usize);
#[derive(Debug, Clone, PartialEq)]
pub enum NodeKind {
Element,
Attribute,
}
#[derive(Debug, Clone)]
pub struct Node {
pub kind: NodeKind,
pub name: String,
pub value: String,
pub parent: Option<NodeId>,
pub children: Vec<NodeId>,
pub attributes: Vec<NodeId>,
}
#[derive(Debug, Clone, Default)]
pub struct Instance {
nodes: Vec<Node>,
root: Option<NodeId>,
order: BTreeMap<NodeId, usize>,
}
impl Instance {
pub fn new() -> Self {
Self::default()
}
pub fn root(&self) -> Option<NodeId> {
self.root
}
pub fn node(&self, id: NodeId) -> &Node {
&self.nodes[id.0]
}
pub fn node_mut(&mut self, id: NodeId) -> &mut Node {
&mut self.nodes[id.0]
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn create_element(&mut self, name: &str, value: &str) -> NodeId {
let id = NodeId(self.nodes.len());
self.nodes.push(Node {
kind: NodeKind::Element,
name: name.to_string(),
value: value.to_string(),
parent: None,
children: Vec::new(),
attributes: Vec::new(),
});
id
}
pub fn append_child(&mut self, parent: NodeId, child: NodeId) {
self.nodes[child.0].parent = Some(parent);
self.nodes[parent.0].children.push(child);
}
pub fn set_attribute(&mut self, element: NodeId, name: &str, value: &str) {
let id = NodeId(self.nodes.len());
self.nodes.push(Node {
kind: NodeKind::Attribute,
name: name.to_string(),
value: value.to_string(),
parent: Some(element),
children: Vec::new(),
attributes: Vec::new(),
});
self.nodes[element.0].attributes.push(id);
}
pub fn set_root(&mut self, id: NodeId) {
self.root = Some(id);
self.reindex();
}
pub fn reindex(&mut self) {
self.order.clear();
let Some(root) = self.root else { return };
let mut counter = 0usize;
let mut stack = vec![root];
while let Some(id) = stack.pop() {
self.order.insert(id, counter);
counter += 1;
for attribute in self.nodes[id.0].attributes.clone() {
self.order.insert(attribute, counter);
counter += 1;
}
let mut children = self.nodes[id.0].children.clone();
children.reverse();
stack.extend(children);
}
}
pub fn document_order(&self, id: NodeId) -> usize {
self.order.get(&id).copied().unwrap_or(usize::MAX)
}
pub fn string_value(&self, id: NodeId) -> String {
let node = &self.nodes[id.0];
if node.children.is_empty() {
return node.value.clone();
}
let mut out = node.value.clone();
for descendant in self.descendants(id) {
out.push_str(&self.nodes[descendant.0].value);
}
out
}
pub fn children(&self, id: NodeId) -> Vec<NodeId> {
self.nodes[id.0].children.clone()
}
pub fn parent(&self, id: NodeId) -> Option<NodeId> {
self.nodes[id.0].parent
}
pub fn attributes(&self, id: NodeId) -> Vec<NodeId> {
self.nodes[id.0].attributes.clone()
}
pub fn descendants(&self, id: NodeId) -> Vec<NodeId> {
let mut out = Vec::new();
let mut stack: Vec<NodeId> = self.nodes[id.0].children.iter().rev().copied().collect();
while let Some(current) = stack.pop() {
out.push(current);
let mut children = self.nodes[current.0].children.clone();
children.reverse();
stack.extend(children);
}
out.sort_by_key(|n| self.document_order(*n));
out
}
pub fn ancestors(&self, id: NodeId) -> Vec<NodeId> {
let mut out = Vec::new();
let mut current = self.nodes[id.0].parent;
while let Some(node) = current {
out.push(node);
current = self.nodes[node.0].parent;
}
out
}
pub fn path_of(&self, id: NodeId) -> String {
let mut parts: Vec<String> = Vec::new();
let mut current = Some(id);
while let Some(node) = current {
let name = &self.nodes[node.0].name;
let step = match self.nodes[node.0].parent {
Some(parent) => {
let twins: Vec<NodeId> = self.nodes[parent.0]
.children
.iter()
.copied()
.filter(|child| self.nodes[child.0].name == *name)
.collect();
match twins.len() {
0 | 1 => name.clone(),
_ => {
let position = twins.iter().position(|c| *c == node).unwrap_or(0) + 1;
format!("{name}[{position}]")
}
}
}
None => name.clone(),
};
parts.push(step);
current = self.nodes[node.0].parent;
}
parts.reverse();
format!("/{}", parts.join("/"))
}
pub fn insert_after(&mut self, sibling: NodeId, child: NodeId) {
let Some(parent) = self.nodes[sibling.0].parent else {
return;
};
self.nodes[child.0].parent = Some(parent);
let at = self.nodes[parent.0]
.children
.iter()
.position(|c| *c == sibling)
.map(|i| i + 1)
.unwrap_or(self.nodes[parent.0].children.len());
self.nodes[parent.0].children.insert(at, child);
}
pub fn detach(&mut self, id: NodeId) {
let Some(parent) = self.nodes[id.0].parent else {
return;
};
self.nodes[parent.0].children.retain(|c| *c != id);
self.nodes[id.0].parent = None;
}
pub fn from_xml(xml: &str) -> Result<Self, String> {
let mut instance = Instance::new();
let mut stack: Vec<NodeId> = Vec::new();
let mut root = None;
let mut chars = xml.char_indices().peekable();
let bytes = xml.as_bytes();
while let Some((i, c)) = chars.next() {
if c != '<' {
continue;
}
if xml[i..].starts_with("<?") || xml[i..].starts_with("<!") {
continue;
}
let end = close_of_tag(xml, i).ok_or("unterminated tag")?;
let inner = &xml[i + 1..end];
while chars.peek().is_some_and(|(j, _)| *j <= end) {
chars.next();
}
if let Some(name) = inner.strip_prefix('/') {
let closed = stack.pop().ok_or("closing tag without opening")?;
let expected = local_name(name.trim());
if instance.node(closed).name != expected {
return Err(format!(
"closing </{expected}> does not match open <{}>",
instance.node(closed).name
));
}
continue;
}
let self_closing = inner.ends_with('/');
let inner = inner.trim_end_matches('/');
let mut parts = inner.split_whitespace();
let name = local_name(parts.next().unwrap_or_default());
let id = instance.create_element(&name, "");
for (key, value) in parse_attributes(inner) {
instance.set_attribute(id, &local_name(&key), &value);
}
match stack.last() {
Some(parent) => instance.append_child(*parent, id),
None => {
if root.is_some() {
return Err("more than one root element".into());
}
root = Some(id);
}
}
if self_closing {
continue;
}
let text_start = end + 1;
let text_end = xml[text_start..]
.find('<')
.map(|n| text_start + n)
.unwrap_or(bytes.len());
let text = &xml[text_start..text_end];
if !text.trim().is_empty() {
instance.node_mut(id).value = unescape(text);
}
stack.push(id);
}
if !stack.is_empty() {
return Err("unclosed elements".into());
}
let root = root.ok_or("no root element")?;
instance.set_root(root);
Ok(instance)
}
}
fn close_of_tag(xml: &str, start: usize) -> Option<usize> {
let mut quote: Option<char> = None;
for (offset, c) in xml[start..].char_indices() {
match (quote, c) {
(Some(q), c) if c == q => quote = None,
(Some(_), _) => {}
(None, '"') | (None, '\'') => quote = Some(c),
(None, '>') => return Some(start + offset),
(None, _) => {}
}
}
None
}
fn local_name(qname: &str) -> String {
match qname.rsplit_once(':') {
Some((_, local)) => local.to_string(),
None => qname.to_string(),
}
}
fn parse_attributes(inner: &str) -> Vec<(String, String)> {
let mut out = Vec::new();
let mut rest = match inner.find(char::is_whitespace) {
Some(i) => &inner[i..],
None => return out,
};
while let Some(eq) = rest.find('=') {
let key = rest[..eq].trim().to_string();
let after = &rest[eq + 1..];
let quote = match after.trim_start().chars().next() {
Some(q @ ('"' | '\'')) => q,
_ => break,
};
let start = after.find(quote).unwrap() + 1;
let Some(len) = after[start..].find(quote) else {
break;
};
let value = &after[start..start + len];
if !key.is_empty() {
out.push((key, unescape(value)));
}
rest = &after[start + len + 1..];
}
out
}
fn unescape(text: &str) -> String {
text.replace("<", "<")
.replace(">", ">")
.replace(""", "\"")
.replace("'", "'")
.replace("&", "&")
}
impl Instance {
pub fn instance_named(&self, id: &str) -> Option<NodeId> {
let root = self.root()?;
let mut candidates = vec![root];
candidates.extend(self.descendants(root));
for node in candidates {
if self.node(node).name != "instance" {
continue;
}
let named = self
.attributes(node)
.into_iter()
.any(|a| self.node(a).name == "id" && self.node(a).value == id);
if named {
return Some(node);
}
}
None
}
pub fn adopt(&mut self, source: &Instance, node: NodeId) -> NodeId {
let created = self.create_element(&source.node(node).name, &source.node(node).value);
for attribute in source.attributes(node) {
let attr = source.node(attribute);
self.set_attribute(created, &attr.name, &attr.value);
}
for child in source.children(node) {
let copied = self.adopt(source, child);
self.append_child(created, copied);
}
created
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builds_a_tree_with_paths_and_order() {
let instance =
Instance::from_xml(r#"<data id="f"><a>1</a><g><b>2</b></g><a>3</a></data>"#).unwrap();
let root = instance.root().unwrap();
assert_eq!(instance.node(root).name, "data");
assert_eq!(instance.attributes(root).len(), 1);
let children = instance.children(root);
assert_eq!(children.len(), 3);
assert_eq!(instance.string_value(children[0]), "1");
assert_eq!(instance.path_of(children[0]), "/data/a[1]");
assert_eq!(instance.path_of(children[2]), "/data/a[2]");
let b = instance.children(children[1])[0];
assert_eq!(instance.path_of(b), "/data/g/b");
assert!(instance.document_order(children[0]) < instance.document_order(b));
assert!(instance.document_order(b) < instance.document_order(children[2]));
}
#[test]
fn entities_and_self_closing_tags() {
let instance = Instance::from_xml(r#"<data><a>x & y</a><b/><c>z</c></data>"#).unwrap();
let children = instance.children(instance.root().unwrap());
assert_eq!(instance.string_value(children[0]), "x & y");
assert_eq!(instance.string_value(children[1]), "");
assert_eq!(instance.string_value(children[2]), "z");
}
#[test]
fn a_greater_than_inside_an_attribute_is_not_the_end_of_the_tag() {
let instance =
Instance::from_xml(r#"<data note="a > b and c < d"><x>1</x></data>"#).unwrap();
let root = instance.root().unwrap();
assert_eq!(
instance
.attributes(root)
.iter()
.map(|a| instance.node(*a).value.clone())
.collect::<Vec<_>>(),
vec!["a > b and c < d"]
);
assert_eq!(instance.children(root).len(), 1);
}
#[test]
fn a_broken_document_is_an_error_not_a_shrug() {
assert!(Instance::from_xml("<data><a></b></data>").is_err());
assert!(Instance::from_xml("<data><a>").is_err());
assert!(Instance::from_xml("no tags here").is_err());
}
}