ddex_builder/generator/
xml_writer.rs1use crate::ast::{Element, Node, AST};
4use crate::determinism::{DeterminismConfig, IndentChar};
5use crate::error::BuildError;
6use ddex_core::models::CommentPosition; use indexmap::IndexMap;
8use std::io::Write;
9
10pub struct XmlWriter {
12 config: DeterminismConfig,
13}
14
15impl XmlWriter {
16 pub fn new(config: DeterminismConfig) -> Self {
18 Self { config }
19 }
20
21 pub fn write(&self, ast: &AST) -> Result<String, BuildError> {
23 let mut buffer = Vec::new();
24
25 writeln!(&mut buffer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
27
28 self.write_element(
30 &mut buffer,
31 &ast.root,
32 &ast.namespaces,
33 ast.schema_location.as_deref(),
34 0,
35 )?;
36
37 Ok(String::from_utf8(buffer).map_err(|e| BuildError::Serialization(e.to_string()))?)
38 }
39
40 fn write_element(
41 &self,
42 writer: &mut impl Write,
43 element: &Element,
44 namespaces: &IndexMap<String, String>,
45 schema_location: Option<&str>,
46 depth: usize,
47 ) -> Result<(), BuildError> {
48 let indent = self.get_indent(depth);
49
50 write!(writer, "{}<", indent)?;
52
53 let element_name = if let Some(ns) = &element.namespace {
55 format!("{}:{}", ns, element.name)
56 } else if depth == 0 && !namespaces.is_empty() {
57 if let Some((prefix, _)) = namespaces.first() {
59 format!("{}:{}", prefix, element.name)
60 } else {
61 element.name.clone()
62 }
63 } else {
64 element.name.clone()
65 };
66
67 write!(writer, "{}", element_name)?;
68
69 if depth == 0 {
71 for (prefix, uri) in namespaces {
72 write!(writer, " xmlns:{}=\"{}\"", prefix, uri)?;
73 }
74
75 if let Some(location) = schema_location {
76 write!(writer, " xsi:schemaLocation=\"{}\"", location)?;
77 }
78 }
79
80 for (key, value) in &element.attributes {
82 write!(writer, " {}=\"{}\"", key, self.escape_attribute(value))?;
83 }
84
85 if element.children.is_empty() {
87 writeln!(writer, "/>")?;
88 } else {
89 let only_text =
91 element.children.len() == 1 && matches!(&element.children[0], Node::Text(_));
92
93 if only_text {
94 write!(writer, ">")?;
96 if let Node::Text(text) = &element.children[0] {
97 write!(writer, "{}", self.escape_text(text))?;
98 }
99 writeln!(writer, "</{}>", element_name)?;
100 } else {
101 writeln!(writer, ">")?;
103
104 for child in &element.children {
106 match child {
107 Node::Element(child_elem) => {
108 self.write_element(writer, child_elem, namespaces, None, depth + 1)?;
109 }
110 Node::Text(text) => {
111 let child_indent = self.get_indent(depth + 1);
112 writeln!(writer, "{}{}", child_indent, self.escape_text(text))?;
113 }
114 Node::Comment(comment) => {
115 self.write_comment(writer, comment, depth + 1)?;
116 }
117 Node::SimpleComment(comment) => {
118 let child_indent = self.get_indent(depth + 1);
119 writeln!(writer, "{}<!-- {} -->", child_indent, comment)?;
120 }
121 }
122 }
123
124 writeln!(writer, "{}</{}>", indent, element_name)?;
126 }
127 }
128
129 Ok(())
130 }
131
132 fn get_indent(&self, depth: usize) -> String {
133 let indent_char = match self.config.indent_char {
134 IndentChar::Space => " ", IndentChar::Tab => "\t", };
137 indent_char.repeat(depth * self.config.indent_width)
138 }
139
140 fn escape_text(&self, text: &str) -> String {
141 text.replace('&', "&")
142 .replace('<', "<")
143 .replace('>', ">")
144 }
145
146 fn escape_attribute(&self, text: &str) -> String {
147 text.replace('&', "&")
148 .replace('<', "<")
149 .replace('>', ">")
150 .replace('"', """)
151 .replace('\'', "'")
152 }
153
154 fn write_comment(
156 &self,
157 writer: &mut impl Write,
158 comment: &ddex_core::models::Comment,
159 depth: usize,
160 ) -> Result<(), BuildError> {
161 let indent = match comment.position {
162 CommentPosition::Before | CommentPosition::After => {
163 self.get_indent(depth.saturating_sub(1))
165 }
166 CommentPosition::FirstChild | CommentPosition::LastChild => {
167 self.get_indent(depth)
169 }
170 CommentPosition::Inline => {
171 String::new()
173 }
174 };
175
176 let comment_xml = comment.to_xml();
178 writeln!(writer, "{}{}", indent, comment_xml)?;
179
180 Ok(())
181 }
182}
183
184