use crate::ast::{Element, Node, AST};
use crate::determinism::{DeterminismConfig, IndentChar};
use crate::error::BuildError;
use ddex_core::models::CommentPosition; use indexmap::IndexMap;
use std::io::Write;
pub struct XmlWriter {
config: DeterminismConfig,
}
impl XmlWriter {
pub fn new(config: DeterminismConfig) -> Self {
Self { config }
}
pub fn write(&self, ast: &AST) -> Result<String, BuildError> {
let mut buffer = Vec::new();
writeln!(&mut buffer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
self.write_element(
&mut buffer,
&ast.root,
&ast.namespaces,
ast.schema_location.as_deref(),
0,
)?;
Ok(String::from_utf8(buffer).map_err(|e| BuildError::Serialization(e.to_string()))?)
}
fn write_element(
&self,
writer: &mut impl Write,
element: &Element,
namespaces: &IndexMap<String, String>,
schema_location: Option<&str>,
depth: usize,
) -> Result<(), BuildError> {
let indent = self.get_indent(depth);
write!(writer, "{}<", indent)?;
let element_name = if let Some(ns) = &element.namespace {
format!("{}:{}", ns, element.name)
} else if depth == 0 && !namespaces.is_empty() {
if let Some((prefix, _)) = namespaces.first() {
format!("{}:{}", prefix, element.name)
} else {
element.name.clone()
}
} else {
element.name.clone()
};
write!(writer, "{}", element_name)?;
if depth == 0 {
for (prefix, uri) in namespaces {
write!(writer, " xmlns:{}=\"{}\"", prefix, uri)?;
}
if let Some(location) = schema_location {
write!(writer, " xsi:schemaLocation=\"{}\"", location)?;
}
}
for (key, value) in &element.attributes {
write!(writer, " {}=\"{}\"", key, self.escape_attribute(value))?;
}
if element.children.is_empty() {
writeln!(writer, "/>")?;
} else {
let only_text =
element.children.len() == 1 && matches!(&element.children[0], Node::Text(_));
if only_text {
write!(writer, ">")?;
if let Node::Text(text) = &element.children[0] {
write!(writer, "{}", self.escape_text(text))?;
}
writeln!(writer, "</{}>", element_name)?;
} else {
writeln!(writer, ">")?;
for child in &element.children {
match child {
Node::Element(child_elem) => {
self.write_element(writer, child_elem, namespaces, None, depth + 1)?;
}
Node::Text(text) => {
let child_indent = self.get_indent(depth + 1);
writeln!(writer, "{}{}", child_indent, self.escape_text(text))?;
}
Node::Comment(comment) => {
self.write_comment(writer, comment, depth + 1)?;
}
Node::SimpleComment(comment) => {
let child_indent = self.get_indent(depth + 1);
writeln!(writer, "{}<!-- {} -->", child_indent, comment)?;
}
}
}
writeln!(writer, "{}</{}>", indent, element_name)?;
}
}
Ok(())
}
fn get_indent(&self, depth: usize) -> String {
let indent_char = match self.config.indent_char {
IndentChar::Space => " ", IndentChar::Tab => "\t", };
indent_char.repeat(depth * self.config.indent_width)
}
fn escape_text(&self, text: &str) -> String {
text.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}
fn escape_attribute(&self, text: &str) -> String {
text.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn write_comment(
&self,
writer: &mut impl Write,
comment: &ddex_core::models::Comment,
depth: usize,
) -> Result<(), BuildError> {
let indent = match comment.position {
CommentPosition::Before | CommentPosition::After => {
self.get_indent(depth.saturating_sub(1))
}
CommentPosition::FirstChild | CommentPosition::LastChild => {
self.get_indent(depth)
}
CommentPosition::Inline => {
String::new()
}
};
let comment_xml = comment.to_xml();
writeln!(writer, "{}{}", indent, comment_xml)?;
Ok(())
}
}