pub mod parse;
pub mod serialize;
pub use parse::parse_xdocument;
pub use serialize::{
serialize_document, serialize_element, serialize_element_sha1_hex,
serialize_element_structure_sha1_hex,
};
use std::sync::Arc;
thread_local! {
static STR_POOL: std::cell::RefCell<std::collections::HashSet<Arc<str>>> =
std::cell::RefCell::new(std::collections::HashSet::new());
}
const STR_POOL_MAX: usize = 16_384;
fn intern_str(s: &str) -> Arc<str> {
STR_POOL.with(|pool| {
{
let p = pool.borrow();
if let Some(existing) = p.get(s) {
return existing.clone();
}
if p.len() >= STR_POOL_MAX {
return Arc::from(s);
}
}
let arc: Arc<str> = Arc::from(s);
pool.borrow_mut().insert(arc.clone());
arc
})
}
#[derive(Clone)]
pub struct XNamespace {
name: Arc<str>,
}
impl XNamespace {
pub fn get(namespace_name: &str) -> XNamespace {
XNamespace {
name: intern_str(namespace_name),
}
}
pub fn none() -> XNamespace {
XNamespace::get("")
}
pub fn xmlns() -> XNamespace {
XNamespace::get("http://www.w3.org/2000/xmlns/")
}
pub fn xml() -> XNamespace {
XNamespace::get("http://www.w3.org/XML/1998/namespace")
}
pub fn name(&self, local: &str) -> XName {
XName::get(local, &self.name)
}
pub fn namespace_name(&self) -> &str {
&self.name
}
}
impl PartialEq for XNamespace {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.name, &other.name) || self.name.as_ref() == other.name.as_ref()
}
}
impl Eq for XNamespace {}
impl std::hash::Hash for XNamespace {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.as_ref().hash(state);
}
}
impl std::fmt::Debug for XNamespace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "XNamespace({:?})", self.name.as_ref())
}
}
#[derive(Clone)]
pub struct XName {
local: Arc<str>,
namespace: XNamespace,
}
impl XName {
pub fn get(local_name: &str, namespace_name: &str) -> XName {
XName {
local: intern_str(local_name),
namespace: XNamespace::get(namespace_name),
}
}
pub fn from_clark(expanded: &str) -> XName {
if expanded.starts_with('{') {
let close = expanded
.find('}')
.filter(|&i| i > 0)
.unwrap_or_else(|| panic!("Invalid expanded name: {expanded}"));
XName::get(&expanded[close + 1..], &expanded[1..close])
} else {
XName::get(expanded, "")
}
}
pub fn local_name(&self) -> &str {
&self.local
}
pub fn namespace(&self) -> &XNamespace {
&self.namespace
}
pub fn namespace_name(&self) -> &str {
self.namespace.namespace_name()
}
pub fn clark(&self) -> String {
if self.namespace.name.is_empty() {
self.local.to_string()
} else {
format!("{{{}}}{}", self.namespace.name, self.local)
}
}
}
impl PartialEq for XName {
fn eq(&self, other: &Self) -> bool {
(Arc::ptr_eq(&self.local, &other.local) || self.local.as_ref() == other.local.as_ref())
&& self.namespace == other.namespace
}
}
impl Eq for XName {}
impl std::hash::Hash for XName {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.local.as_ref().hash(state);
self.namespace.hash(state);
}
}
impl std::fmt::Debug for XName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "XName({:?})", self.clark())
}
}
use std::any::Any;
use std::collections::HashMap;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct NodeId(pub u32);
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct XDeclaration {
pub version: Option<String>,
pub encoding: Option<String>,
pub standalone: Option<String>,
}
#[derive(Clone, Debug)]
struct Attr {
name: XName,
value: String,
}
#[derive(Clone, Debug)]
struct PiData {
target: String,
data: String,
}
enum NodeKind {
Element {
name: XName,
},
Text(String),
Comment(String),
Pi(Box<PiData>),
Document {
declaration: Option<Box<XDeclaration>>,
},
}
struct NodeData {
kind: NodeKind,
parent: Option<NodeId>,
content: Vec<NodeId>,
attrs: Vec<Attr>,
}
impl NodeData {
fn new(kind: NodeKind) -> Self {
NodeData {
kind,
parent: None,
content: Vec::new(),
attrs: Vec::new(),
}
}
}
#[derive(Default)]
pub struct Dom {
nodes: Vec<NodeData>,
annotations: HashMap<NodeId, Vec<Box<dyn Any>>>,
}
impl Dom {
pub fn new() -> Self {
Dom {
nodes: Vec::new(),
annotations: HashMap::new(),
}
}
fn alloc(&mut self, kind: NodeKind) -> NodeId {
let id = NodeId(self.nodes.len() as u32);
self.nodes.push(NodeData::new(kind));
id
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn node_capacity(&self) -> usize {
self.nodes.capacity()
}
#[cfg(test)]
pub fn shrink_arena_to_fit(&mut self) {
self.nodes.shrink_to_fit();
}
pub fn with_scratch<R>(&mut self, f: impl FnOnce(&mut Dom) -> R) -> R {
let checkpoint = self.nodes.len();
let out = f(self);
self.nodes.truncate(checkpoint);
if !self.annotations.is_empty() {
self.annotations.retain(|k, _| (k.0 as usize) < checkpoint);
}
out
}
fn data(&self, id: NodeId) -> &NodeData {
&self.nodes[id.0 as usize]
}
fn data_mut(&mut self, id: NodeId) -> &mut NodeData {
&mut self.nodes[id.0 as usize]
}
pub fn new_document(&mut self) -> NodeId {
self.alloc(NodeKind::Document { declaration: None })
}
pub fn new_element(&mut self, name: XName) -> NodeId {
self.alloc(NodeKind::Element { name })
}
pub fn new_text(&mut self, value: &str) -> NodeId {
self.alloc(NodeKind::Text(value.to_string()))
}
pub fn new_comment(&mut self, value: &str) -> NodeId {
self.alloc(NodeKind::Comment(value.to_string()))
}
pub fn new_pi(&mut self, target: &str, data: &str) -> NodeId {
self.alloc(NodeKind::Pi(Box::new(PiData {
target: target.to_string(),
data: data.to_string(),
})))
}
pub fn is_element(&self, id: NodeId) -> bool {
matches!(self.data(id).kind, NodeKind::Element { .. })
}
pub fn is_text(&self, id: NodeId) -> bool {
matches!(self.data(id).kind, NodeKind::Text(_))
}
pub fn is_document(&self, id: NodeId) -> bool {
matches!(self.data(id).kind, NodeKind::Document { .. })
}
pub fn is_comment(&self, id: NodeId) -> bool {
matches!(self.data(id).kind, NodeKind::Comment(_))
}
pub fn is_pi(&self, id: NodeId) -> bool {
matches!(self.data(id).kind, NodeKind::Pi(_))
}
pub fn name(&self, id: NodeId) -> Option<XName> {
match &self.data(id).kind {
NodeKind::Element { name } => Some(name.clone()),
_ => None,
}
}
pub fn name_is(&self, id: NodeId, name: &XName) -> bool {
matches!(&self.data(id).kind, NodeKind::Element { name: n } if n == name)
}
pub fn set_name(&mut self, id: NodeId, new_name: XName) {
if let NodeKind::Element { name } = &mut self.data_mut(id).kind {
*name = new_name;
}
}
pub fn text_value(&self, id: NodeId) -> Option<&str> {
match &self.data(id).kind {
NodeKind::Text(v) | NodeKind::Comment(v) => Some(v),
_ => None,
}
}
pub fn set_text_value(&mut self, id: NodeId, value: &str) {
match &mut self.data_mut(id).kind {
NodeKind::Text(v) | NodeKind::Comment(v) => *v = value.to_string(),
_ => {}
}
}
pub fn pi_target(&self, id: NodeId) -> Option<&str> {
match &self.data(id).kind {
NodeKind::Pi(pi) => Some(&pi.target),
_ => None,
}
}
pub fn pi_data(&self, id: NodeId) -> Option<&str> {
match &self.data(id).kind {
NodeKind::Pi(pi) => Some(&pi.data),
_ => None,
}
}
pub fn declaration(&self, id: NodeId) -> Option<&XDeclaration> {
match &self.data(id).kind {
NodeKind::Document { declaration } => declaration.as_deref(),
_ => None,
}
}
pub fn set_declaration(&mut self, id: NodeId, decl: Option<XDeclaration>) {
if let NodeKind::Document { declaration } = &mut self.data_mut(id).kind {
*declaration = decl.map(Box::new);
}
}
pub fn parent(&self, id: NodeId) -> Option<NodeId> {
self.data(id).parent
}
pub fn nodes(&self, id: NodeId) -> Vec<NodeId> {
self.data(id).content.clone()
}
pub fn first_node(&self, id: NodeId) -> Option<NodeId> {
self.data(id).content.first().copied()
}
pub fn last_node(&self, id: NodeId) -> Option<NodeId> {
self.data(id).content.last().copied()
}
pub fn child_count(&self, id: NodeId) -> usize {
self.data(id).content.len()
}
pub fn child_at(&self, id: NodeId, i: usize) -> NodeId {
self.data(id).content[i]
}
pub fn elements(&self, id: NodeId, filter: Option<&XName>) -> Vec<NodeId> {
self.data(id)
.content
.iter()
.copied()
.filter(|&c| match (&self.data(c).kind, filter) {
(NodeKind::Element { name }, Some(f)) => name == f,
(NodeKind::Element { .. }, None) => true,
_ => false,
})
.collect()
}
pub fn element(&self, id: NodeId, filter: &XName) -> Option<NodeId> {
self.data(id)
.content
.iter()
.copied()
.find(|&c| matches!(&self.data(c).kind, NodeKind::Element { name } if name == filter))
}
pub fn descendants(&self, id: NodeId, filter: Option<&XName>) -> Vec<NodeId> {
let mut out = Vec::new();
self.for_each_descendant_element(id, filter, |c| out.push(c));
out
}
pub fn for_each_descendant_element(
&self,
id: NodeId,
filter: Option<&XName>,
mut visit: impl FnMut(NodeId),
) {
let mut stack: Vec<(NodeId, usize)> = vec![(id, 0)];
while let Some((node, i)) = stack.last_mut() {
let n = self.child_count(*node);
if *i >= n {
stack.pop();
continue;
}
let c = self.child_at(*node, *i);
*i += 1;
if !self.is_element(c) {
continue;
}
let matches = match filter {
None => true,
Some(f) => self.name_is(c, f),
};
if matches {
visit(c);
}
stack.push((c, 0));
}
}
pub fn descendant_nodes(&self, id: NodeId) -> Vec<NodeId> {
let mut out = Vec::new();
self.walk_descendant_nodes(id, &mut out);
out
}
fn walk_descendant_nodes(&self, id: NodeId, out: &mut Vec<NodeId>) {
for &c in &self.data(id).content {
out.push(c);
if !self.data(c).content.is_empty() {
self.walk_descendant_nodes(c, out);
}
}
}
pub fn descendants_and_self(&self, id: NodeId, filter: Option<&XName>) -> Vec<NodeId> {
let mut out = Vec::new();
self.for_each_descendant_and_self(id, filter, |c| out.push(c));
out
}
pub fn for_each_descendant_and_self(
&self,
id: NodeId,
filter: Option<&XName>,
mut visit: impl FnMut(NodeId),
) {
if let NodeKind::Element { name } = &self.data(id).kind
&& filter.is_none_or(|f| name == f)
{
visit(id);
}
self.for_each_descendant_element(id, filter, visit);
}
pub fn ancestors(&self, id: NodeId, filter: Option<&XName>) -> Vec<NodeId> {
let mut out = Vec::new();
let mut p = self.data(id).parent;
while let Some(pid) = p {
if let NodeKind::Element { name } = &self.data(pid).kind
&& filter.is_none_or(|f| name == f)
{
out.push(pid);
}
p = self.data(pid).parent;
}
out
}
pub fn ancestors_and_self(&self, id: NodeId, filter: Option<&XName>) -> Vec<NodeId> {
let mut out = Vec::new();
let mut cur = Some(id);
while let Some(c) = cur {
match &self.data(c).kind {
NodeKind::Element { name } => {
if filter.is_none_or(|f| name == f) {
out.push(c);
}
cur = match self.data(c).parent {
Some(p) if self.is_element(p) => Some(p),
_ => None,
};
}
_ => break,
}
}
out
}
pub fn document(&self, id: NodeId) -> Option<NodeId> {
let mut cur = Some(id);
while let Some(c) = cur {
if self.is_document(c) {
return Some(c);
}
cur = self.data(c).parent;
}
None
}
pub fn root(&self, doc: NodeId) -> Option<NodeId> {
self.data(doc)
.content
.iter()
.copied()
.find(|&c| self.is_element(c))
}
fn index_in_parent(&self, id: NodeId) -> Option<(NodeId, usize)> {
let p = self.data(id).parent?;
let idx = self.data(p).content.iter().position(|&c| c == id)?;
Some((p, idx))
}
pub fn nodes_after_self(&self, id: NodeId) -> Vec<NodeId> {
match self.index_in_parent(id) {
Some((p, idx)) => self.data(p).content[idx + 1..].to_vec(),
None => Vec::new(),
}
}
pub fn nodes_before_self(&self, id: NodeId) -> Vec<NodeId> {
match self.index_in_parent(id) {
Some((p, idx)) => self.data(p).content[..idx].to_vec(),
None => Vec::new(),
}
}
pub fn next_element(&self, id: NodeId) -> Option<NodeId> {
self.nodes_after_self(id)
.into_iter()
.find(|&n| self.is_element(n))
}
pub fn has_elements(&self, id: NodeId) -> bool {
self.data(id).content.iter().any(|&c| self.is_element(c))
}
pub fn has_attributes(&self, id: NodeId) -> bool {
!self.data(id).attrs.is_empty()
}
pub fn attribute(&self, id: NodeId, name: &XName) -> Option<&str> {
self.data(id)
.attrs
.iter()
.find(|a| &a.name == name)
.map(|a| a.value.as_str())
}
pub fn attributes(&self, id: NodeId) -> Vec<(XName, String)> {
self.data(id)
.attrs
.iter()
.map(|a| (a.name.clone(), a.value.clone()))
.collect()
}
pub fn attr_count(&self, id: NodeId) -> usize {
self.data(id).attrs.len()
}
pub fn attr_at(&self, id: NodeId, i: usize) -> (&XName, &str) {
let a = &self.data(id).attrs[i];
(&a.name, a.value.as_str())
}
pub fn set_attribute_value(&mut self, id: NodeId, name: &XName, value: Option<&str>) {
let attrs = &mut self.data_mut(id).attrs;
match value {
None => attrs.retain(|a| &a.name != name),
Some(v) => {
if let Some(a) = attrs.iter_mut().find(|a| &a.name == name) {
a.value = v.to_string();
} else {
attrs.push(Attr {
name: name.clone(),
value: v.to_string(),
});
}
}
}
}
pub fn is_namespace_declaration(&self, name: &XName) -> bool {
name.namespace_name() == "http://www.w3.org/2000/xmlns/"
|| (name.namespace_name().is_empty() && name.local_name() == "xmlns")
}
fn detach(&mut self, id: NodeId) {
if let Some((p, idx)) = self.index_in_parent(id) {
self.data_mut(p).content.remove(idx);
self.data_mut(id).parent = None;
}
}
fn materialize(&mut self, node: NodeId) -> NodeId {
if self.data(node).parent.is_some() {
self.clone_subtree(node)
} else {
node
}
}
fn validate_attachment(&self, parent: NodeId, node: NodeId) {
if !self.is_element(parent) && !self.is_document(parent) {
panic!("cannot attach a node to a non-container parent {parent:?}");
}
if parent == node {
panic!("cannot attach a node to itself");
}
if self.data(node).parent.is_none() && self.is_ancestor_of(node, parent) {
panic!("cannot attach an ancestor beneath its own descendant");
}
}
fn is_ancestor_of(&self, ancestor: NodeId, descendant: NodeId) -> bool {
let mut cur = self.data(descendant).parent;
while let Some(id) = cur {
if id == ancestor {
return true;
}
cur = self.data(id).parent;
}
false
}
pub fn add(&mut self, parent: NodeId, node: NodeId) {
let n = self.materialize(node);
self.validate_attachment(parent, n);
self.data_mut(n).parent = Some(parent);
self.data_mut(parent).content.push(n);
}
pub fn add_text(&mut self, parent: NodeId, value: &str) -> NodeId {
let t = self.new_text(value);
self.validate_attachment(parent, t);
self.data_mut(t).parent = Some(parent);
self.data_mut(parent).content.push(t);
t
}
pub fn add_first(&mut self, parent: NodeId, node: NodeId) {
let n = self.materialize(node);
self.validate_attachment(parent, n);
self.data_mut(n).parent = Some(parent);
self.data_mut(parent).content.insert(0, n);
}
pub fn remove_nodes(&mut self, id: NodeId) {
let kids = std::mem::take(&mut self.data_mut(id).content);
for k in kids {
self.data_mut(k).parent = None;
}
}
pub fn remove(&mut self, id: NodeId) {
self.detach(id);
}
pub fn add_before_self(&mut self, reference: NodeId, node: NodeId) {
let (p, idx) = self
.index_in_parent(reference)
.expect("No parent for AddBeforeSelf");
let n = self.materialize(node);
self.validate_attachment(p, n);
self.data_mut(n).parent = Some(p);
self.data_mut(p).content.insert(idx, n);
}
pub fn add_after_self(&mut self, reference: NodeId, node: NodeId) {
let (p, idx) = self
.index_in_parent(reference)
.expect("No parent for AddAfterSelf");
let n = self.materialize(node);
self.validate_attachment(p, n);
self.data_mut(n).parent = Some(p);
self.data_mut(p).content.insert(idx + 1, n);
}
pub fn replace_with(&mut self, reference: NodeId, nodes: &[NodeId]) {
let (p, idx) = self
.index_in_parent(reference)
.expect("No parent for ReplaceWith");
let mut materialized = Vec::with_capacity(nodes.len());
for &node in nodes {
let n = self.materialize(node);
self.validate_attachment(p, n);
self.data_mut(n).parent = Some(p);
materialized.push(n);
}
self.data_mut(reference).parent = None;
self.data_mut(p).content.splice(idx..=idx, materialized);
}
pub fn value(&self, id: NodeId) -> String {
match self.value_str(id) {
std::borrow::Cow::Borrowed(s) => s.to_string(),
std::borrow::Cow::Owned(s) => s,
}
}
pub fn value_str(&self, id: NodeId) -> std::borrow::Cow<'_, str> {
let content = &self.data(id).content;
if content.len() == 1
&& let NodeKind::Text(v) = &self.data(content[0]).kind
{
return std::borrow::Cow::Borrowed(v.as_str());
}
let mut s = String::new();
self.collect_text(id, &mut s);
std::borrow::Cow::Owned(s)
}
fn collect_text(&self, id: NodeId, s: &mut String) {
for &c in &self.data(id).content {
match &self.data(c).kind {
NodeKind::Text(v) => s.push_str(v),
NodeKind::Element { .. } | NodeKind::Document { .. } => self.collect_text(c, s),
_ => {}
}
}
}
pub fn set_value(&mut self, id: NodeId, value: &str) {
self.remove_nodes(id);
self.add_text(id, value);
}
pub fn clone_subtree(&mut self, id: NodeId) -> NodeId {
let new_kind = match &self.data(id).kind {
NodeKind::Element { name } => NodeKind::Element { name: name.clone() },
NodeKind::Text(v) => NodeKind::Text(v.clone()),
NodeKind::Comment(v) => NodeKind::Comment(v.clone()),
NodeKind::Pi(pi) => NodeKind::Pi(pi.clone()),
NodeKind::Document { declaration } => NodeKind::Document {
declaration: declaration.clone(),
},
};
let n_kids = self.child_count(id);
let attrs = self.data(id).attrs.clone();
let copy = self.alloc(new_kind);
{
let d = self.data_mut(copy);
d.attrs = attrs;
d.content.reserve_exact(n_kids);
}
for i in 0..n_kids {
let k = self.child_at(id, i);
let ck = self.clone_subtree(k);
self.validate_attachment(copy, ck);
self.data_mut(ck).parent = Some(copy);
self.data_mut(copy).content.push(ck);
}
copy
}
pub fn add_annotation<T: Any + 'static>(&mut self, id: NodeId, annotation: T) {
self.annotations
.entry(id)
.or_default()
.push(Box::new(annotation));
}
pub fn annotation<T: Any + 'static>(&self, id: NodeId) -> Option<&T> {
self.annotations
.get(&id)?
.iter()
.find_map(|a| a.downcast_ref::<T>())
}
pub fn remove_annotations<T: Any + 'static>(&mut self, id: NodeId) {
if let Some(v) = self.annotations.get_mut(&id) {
v.retain(|a| !a.is::<T>());
if v.is_empty() {
self.annotations.remove(&id);
}
}
}
pub fn parse_xdocument(&mut self, xml: &str) -> NodeId {
parse::parse_xdocument(self, xml)
}
pub fn serialize_element(&self, el: NodeId) -> String {
serialize::serialize_element(self, el)
}
pub fn serialize_element_sha1_hex(&self, el: NodeId) -> String {
serialize::serialize_element_sha1_hex(self, el)
}
pub fn serialize_element_structure_sha1_hex(&self, el: NodeId) -> String {
serialize::serialize_element_structure_sha1_hex(self, el)
}
pub fn serialize_document(&self, doc: NodeId) -> String {
serialize::serialize_document(self, doc)
}
}
#[cfg(test)]
mod node_layout_tests {
use super::*;
#[test]
fn node_data_excludes_annotations_vec() {
let sz = std::mem::size_of::<NodeData>();
assert!(
sz <= 128,
"NodeData is {sz} bytes; ANN-01 requires <= 128 (annotations must live \
in the Dom side table, not inline on every node)"
);
}
#[test]
fn node_kind_rare_variants_are_boxed() {
let kind = std::mem::size_of::<NodeKind>();
let node = std::mem::size_of::<NodeData>();
assert!(
kind <= 40,
"NodeKind is {kind} bytes; NODE-KIND-01 requires <= 40 (box the rare \
Document/Pi variants so they don't size every node)"
);
assert!(
node <= 96,
"NodeData is {node} bytes; NODE-KIND-01 requires <= 96 (was 128 after \
ANN-01, 152 originally)"
);
}
}
#[cfg(test)]
mod intern_tests {
use super::*;
#[test]
fn identical_names_share_interned_arcs() {
let ns = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
let a = XName::get("p", ns);
let b = XName::get("p", ns);
assert!(
Arc::ptr_eq(&a.local, &b.local),
"PARSE-01: identical local names must share one interned Arc"
);
assert!(
Arc::ptr_eq(&a.namespace.name, &b.namespace.name),
"PARSE-01: identical namespaces must share one interned Arc"
);
assert_eq!(a, b);
assert_ne!(a, XName::get("r", ns));
}
}