use std::io::Write;
use crate::document::XmlDocument;
use crate::error::Result;
use crate::node::{NodeType, XmlNode, XmlRoNode};
#[derive(Debug, Clone)]
pub struct SerializeOptions {
pub indent: bool,
pub indent_str: String,
pub xml_declaration: bool,
pub encoding: String,
}
impl Default for SerializeOptions {
fn default() -> Self {
Self {
indent: false,
indent_str: " ".to_string(),
xml_declaration: false,
encoding: "UTF-8".to_string(),
}
}
}
impl SerializeOptions {
pub fn pretty() -> Self {
Self {
indent: true,
..Default::default()
}
}
}
pub fn node_to_xml_string(doc: &XmlDocument, node: &XmlNode) -> Result<String> {
node_to_xml_string_with_options(doc, node, &SerializeOptions::default())
}
pub fn readonly_node_to_xml_string(doc: &XmlDocument, node: &XmlRoNode) -> Result<String> {
node_to_xml_string_with_options(doc, &node.clone().into_node(), &SerializeOptions::default())
}
pub fn node_to_xml_string_with_options(
_doc: &XmlDocument,
node: &XmlNode,
options: &SerializeOptions,
) -> Result<String> {
let mut output = Vec::new();
let mut serializer = XmlSerializer::new(&mut output, options.clone());
if options.xml_declaration {
serializer.write_declaration()?;
}
serializer.write_node(node, 0)?;
Ok(String::from_utf8(output)?)
}
pub fn document_to_xml_string(doc: &XmlDocument) -> Result<String> {
document_to_xml_string_with_options(doc, &SerializeOptions::default())
}
pub fn document_to_xml_string_with_options(
doc: &XmlDocument,
options: &SerializeOptions,
) -> Result<String> {
let root = doc.get_root_element()?;
let mut opts = options.clone();
opts.xml_declaration = true;
node_to_xml_string_with_options(doc, &root, &opts)
}
struct XmlSerializer<W: Write> {
writer: W,
options: SerializeOptions,
}
impl<W: Write> XmlSerializer<W> {
fn new(writer: W, options: SerializeOptions) -> Self {
Self { writer, options }
}
fn write_declaration(&mut self) -> Result<()> {
write!(
self.writer,
"<?xml version=\"1.0\" encoding=\"{}\"?>",
self.options.encoding
)?;
if self.options.indent {
writeln!(self.writer)?;
}
Ok(())
}
fn write_node(&mut self, node: &XmlNode, depth: usize) -> Result<()> {
match node.get_type() {
NodeType::Document => {
for child in node.get_child_nodes() {
self.write_node(&child, depth)?;
}
}
NodeType::Element => {
self.write_element(node, depth)?;
}
NodeType::Text => {
if let Some(content) = node.get_content() {
self.write_escaped_text(&content)?;
}
}
NodeType::CData => {
if let Some(content) = node.get_content() {
write!(self.writer, "<![CDATA[{}]]>", content)?;
}
}
NodeType::Comment => {
if let Some(content) = node.get_content() {
write!(self.writer, "<!--{}-->", content)?;
}
}
NodeType::ProcessingInstruction => {
let name = node.get_name();
if let Some(content) = node.get_content() {
write!(self.writer, "<?{} {}?>", name, content)?;
} else {
write!(self.writer, "<?{}?>", name)?;
}
}
NodeType::Attribute => {
}
NodeType::Namespace => {
}
}
Ok(())
}
fn write_element(&mut self, node: &XmlNode, depth: usize) -> Result<()> {
let qname = node.qname();
let attributes = node.get_attributes();
let namespace_decls = node.get_namespace_declarations();
let children = node.get_child_nodes();
if self.options.indent && depth > 0 {
for _ in 0..depth {
write!(self.writer, "{}", self.options.indent_str)?;
}
}
write!(self.writer, "<{}", qname)?;
for ns in &namespace_decls {
if ns.prefix().is_empty() {
write!(self.writer, " xmlns=\"{}\"", escape_attribute(ns.uri()))?;
} else {
write!(
self.writer,
" xmlns:{}=\"{}\"",
ns.prefix(),
escape_attribute(ns.uri())
)?;
}
}
for (name, value) in &attributes {
let attr_qname = if let Some((prefix, _uri)) = node.get_attribute_ns_info(name) {
if prefix.is_empty() {
name.clone()
} else {
format!("{}:{}", prefix, name)
}
} else {
name.clone()
};
write!(
self.writer,
" {}=\"{}\"",
attr_qname,
escape_attribute(value)
)?;
}
if children.is_empty() {
write!(self.writer, "/>")?;
} else {
write!(self.writer, ">")?;
let has_element_children = children.iter().any(|c| c.is_element());
if self.options.indent && has_element_children {
writeln!(self.writer)?;
}
for child in &children {
self.write_node(child, depth + 1)?;
if self.options.indent && child.is_element() {
writeln!(self.writer)?;
}
}
if self.options.indent && has_element_children {
for _ in 0..depth {
write!(self.writer, "{}", self.options.indent_str)?;
}
}
write!(self.writer, "</{}>", qname)?;
}
Ok(())
}
fn write_escaped_text(&mut self, text: &str) -> Result<()> {
for ch in text.chars() {
match ch {
'&' => write!(self.writer, "&")?,
'<' => write!(self.writer, "<")?,
'>' => write!(self.writer, ">")?,
_ => write!(self.writer, "{}", ch)?,
}
}
Ok(())
}
}
fn escape_attribute(value: &str) -> String {
let mut result = String::with_capacity(value.len());
for ch in value.chars() {
match ch {
'&' => result.push_str("&"),
'<' => result.push_str("<"),
'>' => result.push_str(">"),
'"' => result.push_str("""),
'\'' => result.push_str("'"),
_ => result.push(ch),
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse;
#[test]
fn test_serialize_simple() {
let doc = parse(r#"<root attr="value"><child>text</child></root>"#).unwrap();
let root = doc.get_root_element().unwrap();
let xml = node_to_xml_string(&doc, &root).unwrap();
assert!(xml.contains("<root"));
assert!(xml.contains("attr=\"value\""));
assert!(xml.contains("<child>text</child>"));
assert!(xml.contains("</root>"));
}
#[test]
fn test_serialize_namespaced() {
let doc = parse(
r#"<gml:root xmlns:gml="http://www.opengis.net/gml">
<gml:child>text</gml:child>
</gml:root>"#,
)
.unwrap();
let root = doc.get_root_element().unwrap();
let xml = node_to_xml_string(&doc, &root).unwrap();
assert!(xml.contains("xmlns:gml="));
assert!(xml.contains("<gml:root"));
}
#[test]
fn test_serialize_pretty() {
let doc = parse(r#"<root><a/><b/></root>"#).unwrap();
let root = doc.get_root_element().unwrap();
let xml =
node_to_xml_string_with_options(&doc, &root, &SerializeOptions::pretty()).unwrap();
assert!(xml.contains('\n'));
}
#[test]
fn test_escape_special_chars() {
let doc = parse(r#"<root attr="&test"><text></root>"#).unwrap();
let root = doc.get_root_element().unwrap();
let xml = node_to_xml_string(&doc, &root).unwrap();
assert!(xml.contains("&") || xml.contains("&test"));
assert!(xml.contains("<") || xml.contains("<text>"));
}
}