use std::collections::HashMap;
use crate::error::Error;
use super::parse::{Attribute, Document, Element, ElementId, ElementPath, Node, QName, XML_NS};
impl Document {
pub(crate) fn new(root: Element) -> Result<Self, Error> {
let mut paths: Vec<ElementPath> = Vec::new();
let mut id_index: HashMap<String, ElementId> = HashMap::new();
let mut root = root;
let mut current_path: ElementPath = Vec::new();
renumber(&mut root, &mut current_path, &mut paths, &mut id_index)?;
Ok(Document {
root,
id_index,
paths,
})
}
}
fn renumber(
element: &mut Element,
current_path: &mut ElementPath,
paths: &mut Vec<ElementPath>,
id_index: &mut HashMap<String, ElementId>,
) -> Result<(), Error> {
let new_id = ElementId(
u32::try_from(paths.len())
.map_err(|_err| Error::XmlEmit("element id exceeds u32::MAX".to_string()))?,
);
element.id = new_id;
paths.push(current_path.clone());
for attr in &element.attributes {
let is_id_attr = (attr.qname.namespace.is_none() && attr.qname.local == "ID")
|| (attr.qname.namespace.as_deref() == Some(XML_NS) && attr.qname.local == "id");
if is_id_attr {
if id_index.contains_key(&attr.value) {
return Err(Error::XmlParse("duplicate ID".to_string()));
}
id_index.insert(attr.value.clone(), new_id);
}
}
for (child_idx, child) in element.children.iter_mut().enumerate() {
if let Node::Element(child_elem) = child {
let idx = u32::try_from(child_idx)
.map_err(|_err| Error::XmlEmit("child index exceeds u32::MAX".to_string()))?;
current_path.push(idx);
renumber(child_elem, current_path, paths, id_index)?;
current_path.pop();
}
}
Ok(())
}
impl Element {
pub(crate) fn build(qname: QName) -> ElementBuilder {
ElementBuilder {
qname,
namespaces: Vec::new(),
attributes: Vec::new(),
children: Vec::new(),
}
}
#[cfg(feature = "ecp")]
pub(crate) fn push_attribute(&mut self, name: QName, value: impl Into<String>) {
self.attributes.push(Attribute {
qname: name,
source_prefix: None,
value: value.into(),
});
}
}
pub(crate) struct ElementBuilder {
qname: QName,
namespaces: Vec<(Option<String>, String)>,
attributes: Vec<Attribute>,
children: Vec<Node>,
}
impl ElementBuilder {
pub(crate) fn with_attribute(mut self, name: QName, value: impl Into<String>) -> Self {
self.attributes.push(Attribute {
qname: name,
source_prefix: None,
value: value.into(),
});
self
}
pub(crate) fn with_namespace(mut self, prefix: Option<String>, uri: impl Into<String>) -> Self {
self.namespaces.push((prefix, uri.into()));
self
}
pub(crate) fn with_child(mut self, node: Node) -> Self {
self.children.push(node);
self
}
pub(crate) fn with_text(mut self, text: impl Into<String>) -> Self {
self.children.push(Node::Text(text.into()));
self
}
pub(crate) fn finish(self) -> Element {
Element {
qname: self.qname,
source_prefix: None,
namespaces_declared_here: self.namespaces,
attributes: self.attributes,
children: self.children,
id: ElementId(0), }
}
}
pub(crate) fn emit_document(doc: &Document) -> Result<String, Error> {
emit_element(doc.root())
}
pub(crate) fn emit_element(element: &Element) -> Result<String, Error> {
let mut out = String::new();
let mut ns_stack: Vec<Vec<(Option<String>, String)>> = Vec::new();
emit_element_inner(element, &mut out, &mut ns_stack)?;
Ok(out)
}
fn emit_element_inner(
element: &Element,
out: &mut String,
ns_stack: &mut Vec<Vec<(Option<String>, String)>>,
) -> Result<(), Error> {
ns_stack.push(element.namespaces_declared_here.clone());
out.push('<');
let elem_prefix: Option<String> =
resolve_prefix_for_emit(element.qname.namespace.as_deref(), ns_stack)?.map(str::to_owned);
write_qualified_name(out, elem_prefix.as_deref(), &element.qname.local);
for (prefix, uri) in &element.namespaces_declared_here {
out.push(' ');
match prefix {
None => out.push_str("xmlns"),
Some(p) => {
out.push_str("xmlns:");
out.push_str(p);
}
}
out.push_str("=\"");
push_attr_escaped(out, uri);
out.push('"');
}
for attr in &element.attributes {
out.push(' ');
let attr_prefix: Option<String> =
resolve_prefix_for_attribute(attr.qname.namespace.as_deref(), ns_stack)?
.map(str::to_owned);
write_qualified_name(out, attr_prefix.as_deref(), &attr.qname.local);
out.push_str("=\"");
push_attr_escaped(out, &attr.value);
out.push('"');
}
if element.children.is_empty() {
out.push_str("/>");
} else {
out.push('>');
for child in &element.children {
match child {
Node::Element(e) => emit_element_inner(e, out, ns_stack)?,
Node::Text(t) => push_text_escaped(out, t),
Node::Comment(c) => {
if c.contains("--") || c.ends_with('-') {
return Err(Error::XmlEmit(
"comment content forms invalid XML comment".to_string(),
));
}
out.push_str("<!--");
out.push_str(c);
out.push_str("-->");
}
}
}
out.push_str("</");
write_qualified_name(out, elem_prefix.as_deref(), &element.qname.local);
out.push('>');
}
ns_stack.pop();
Ok(())
}
fn resolve_prefix_for_emit<'a>(
uri: Option<&str>,
ns_stack: &'a [Vec<(Option<String>, String)>],
) -> Result<Option<&'a str>, Error> {
let Some(uri) = uri else {
return Ok(None);
};
if uri == XML_NS {
return Ok(Some("xml"));
}
for layer in ns_stack.iter().rev() {
for (prefix, declared_uri) in layer.iter().rev() {
if declared_uri == uri {
return Ok(prefix.as_deref());
}
}
}
Err(Error::XmlEmit(format!(
"no namespace prefix in scope for URI {uri}"
)))
}
fn resolve_prefix_for_attribute<'a>(
uri: Option<&str>,
ns_stack: &'a [Vec<(Option<String>, String)>],
) -> Result<Option<&'a str>, Error> {
let Some(uri) = uri else {
return Ok(None);
};
if uri == XML_NS {
return Ok(Some("xml"));
}
for layer in ns_stack.iter().rev() {
for (prefix, declared_uri) in layer.iter().rev() {
if declared_uri == uri && prefix.is_some() {
return Ok(prefix.as_deref());
}
}
}
Err(Error::XmlEmit(format!(
"no non-default namespace prefix in scope for attribute URI {uri}"
)))
}
fn write_qualified_name(out: &mut String, prefix: Option<&str>, local: &str) {
if let Some(p) = prefix {
out.push_str(p);
out.push(':');
}
out.push_str(local);
}
fn push_text_escaped(out: &mut String, text: &str) {
for ch in text.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'\r' => out.push_str(" "),
c => out.push(c),
}
}
}
fn push_attr_escaped(out: &mut String, value: &str) {
for ch in value.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\t' => out.push_str("	"),
'\n' => out.push_str(" "),
'\r' => out.push_str(" "),
c => out.push(c),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(xml: &str) -> Document {
Document::parse(xml.as_bytes()).expect("parse")
}
#[test]
fn emit_round_trip_simple() {
let xml = r"<root><child>hello</child></root>";
let doc = parse(xml);
let out = emit_document(&doc).unwrap();
let doc2 = Document::parse(out.as_bytes()).unwrap();
assert_eq!(doc2.root().qname().local(), "root");
let child = doc2.root().child_element(None, "child").unwrap();
assert_eq!(child.text_content(), "hello");
}
#[test]
fn emit_preserves_namespaces() {
let xml = r#"<a:root xmlns:a="urn:a" xmlns="urn:default" a:k="v"><inner/></a:root>"#;
let doc = parse(xml);
let out = emit_document(&doc).unwrap();
assert!(out.contains(r#"xmlns:a="urn:a""#));
assert!(out.contains(r#"xmlns="urn:default""#));
assert!(out.contains(r#"a:k="v""#));
let doc2 = Document::parse(out.as_bytes()).unwrap();
assert_eq!(doc2.root().qname().namespace(), Some("urn:a"));
assert_eq!(doc2.root().attribute(Some("urn:a"), "k"), Some("v"));
}
#[test]
fn emit_escapes_text_and_attributes() {
let root = Element::build(QName::new(None, "root"))
.with_attribute(QName::new(None, "k"), "a\"<&\nb")
.with_text("<&>")
.finish();
let doc = Document::new(root).unwrap();
let out = emit_document(&doc).unwrap();
assert!(out.contains(r#"k="a"<& b""#), "got: {out}");
assert!(out.contains("<&>"), "got: {out}");
}
#[test]
fn emit_self_closing_for_empty_element() {
let root = Element::build(QName::new(None, "x")).finish();
let doc = Document::new(root).unwrap();
let out = emit_document(&doc).unwrap();
assert_eq!(out, "<x/>");
}
#[test]
fn emit_preserves_comments() {
let xml = r"<root><!-- keep --><x/></root>";
let doc = parse(xml);
let out = emit_document(&doc).unwrap();
assert!(out.contains("<!-- keep -->"));
}
#[test]
fn builder_id_index_populates() {
let inner = Element::build(QName::new(None, "Assertion"))
.with_attribute(QName::new(None, "ID"), "xyz")
.finish();
let outer = Element::build(QName::new(None, "Response"))
.with_attribute(QName::new(None, "ID"), "abc")
.with_child(Node::Element(inner))
.finish();
let doc = Document::new(outer).unwrap();
let abc = doc.element_by_id_attr("abc").unwrap();
let xyz = doc.element_by_id_attr("xyz").unwrap();
assert_eq!(doc.element(abc).unwrap().qname().local(), "Response");
assert_eq!(doc.element(xyz).unwrap().qname().local(), "Assertion");
}
#[test]
fn builder_rejects_duplicate_id() {
let a = Element::build(QName::new(None, "a"))
.with_attribute(QName::new(None, "ID"), "dup")
.finish();
let b = Element::build(QName::new(None, "b"))
.with_attribute(QName::new(None, "ID"), "dup")
.finish();
let root = Element::build(QName::new(None, "root"))
.with_child(Node::Element(a))
.with_child(Node::Element(b))
.finish();
let err = Document::new(root).unwrap_err();
match err {
Error::XmlParse(msg) => assert!(msg.contains("duplicate ID")),
_ => panic!("expected XmlParse"),
}
}
#[test]
fn programmatic_namespaced_emit() {
let child = Element::build(QName::new(Some("urn:a".to_owned()), "child"))
.with_attribute(QName::new(Some("urn:a".to_owned()), "k"), "v")
.finish();
let root = Element::build(QName::new(Some("urn:a".to_owned()), "root"))
.with_namespace(Some("a".to_owned()), "urn:a")
.with_child(Node::Element(child))
.finish();
let doc = Document::new(root).unwrap();
let out = emit_document(&doc).unwrap();
assert!(out.contains("<a:root"), "got: {out}");
assert!(out.contains("xmlns:a=\"urn:a\""), "got: {out}");
assert!(out.contains("<a:child"), "got: {out}");
assert!(out.contains("a:k=\"v\""), "got: {out}");
let doc2 = Document::parse(out.as_bytes()).unwrap();
assert_eq!(doc2.root().qname().namespace(), Some("urn:a"));
let child2 = doc2.root().child_element(Some("urn:a"), "child").unwrap();
assert_eq!(child2.attribute(Some("urn:a"), "k"), Some("v"));
}
#[test]
fn emit_single_element_requires_in_scope_declarations() {
let xml = r#"<root xmlns="urn:n"><a><b>x</b></a></root>"#;
let doc = parse(xml);
let a = doc.root().child_element(Some("urn:n"), "a").unwrap();
emit_element(a).unwrap_err();
let out = emit_element(doc.root()).unwrap();
assert!(out.contains("xmlns=\"urn:n\""));
assert!(out.contains("<a>"));
assert!(out.contains("<b>x</b>"));
}
}