ddex_builder/generator/
xml_writer.rs1use crate::ast::{AST, Element, Node};
4use crate::determinism::{DeterminismConfig, IndentChar};
5use ddex_core::models::CommentPosition; use crate::error::BuildError;
7use 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(&mut buffer, &ast.root, &ast.namespaces, ast.schema_location.as_deref(), 0)?;
30
31 Ok(String::from_utf8(buffer).map_err(|e| BuildError::Serialization(e.to_string()))?)
32 }
33
34 fn write_element(
35 &self,
36 writer: &mut impl Write,
37 element: &Element,
38 namespaces: &IndexMap<String, String>,
39 schema_location: Option<&str>,
40 depth: usize,
41 ) -> Result<(), BuildError> {
42 let indent = self.get_indent(depth);
43
44 write!(writer, "{}<", indent)?;
46
47 let element_name = if let Some(ns) = &element.namespace {
49 format!("{}:{}", ns, element.name)
50 } else if depth == 0 && !namespaces.is_empty() {
51 if let Some((prefix, _)) = namespaces.first() {
53 format!("{}:{}", prefix, element.name)
54 } else {
55 element.name.clone()
56 }
57 } else {
58 element.name.clone()
59 };
60
61 write!(writer, "{}", element_name)?;
62
63 if depth == 0 {
65 for (prefix, uri) in namespaces {
66 write!(writer, " xmlns:{}=\"{}\"", prefix, uri)?;
67 }
68
69 if let Some(location) = schema_location {
70 write!(writer, " xsi:schemaLocation=\"{}\"", location)?;
71 }
72 }
73
74 for (key, value) in &element.attributes {
76 write!(writer, " {}=\"{}\"", key, self.escape_attribute(value))?;
77 }
78
79 if element.children.is_empty() {
81 writeln!(writer, "/>")?;
82 } else {
83 let only_text = element.children.len() == 1 &&
85 matches!(&element.children[0], Node::Text(_));
86
87 if only_text {
88 write!(writer, ">")?;
90 if let Node::Text(text) = &element.children[0] {
91 write!(writer, "{}", self.escape_text(text))?;
92 }
93 writeln!(writer, "</{}>", element_name)?;
94 } else {
95 writeln!(writer, ">")?;
97
98 for child in &element.children {
100 match child {
101 Node::Element(child_elem) => {
102 self.write_element(writer, child_elem, namespaces, None, depth + 1)?;
103 }
104 Node::Text(text) => {
105 let child_indent = self.get_indent(depth + 1);
106 writeln!(writer, "{}{}", child_indent, self.escape_text(text))?;
107 }
108 Node::Comment(comment) => {
109 self.write_comment(writer, comment, depth + 1)?;
110 }
111 Node::SimpleComment(comment) => {
112 let child_indent = self.get_indent(depth + 1);
113 writeln!(writer, "{}<!-- {} -->", child_indent, comment)?;
114 }
115 }
116 }
117
118 writeln!(writer, "{}</{}>", indent, element_name)?;
120 }
121 }
122
123 Ok(())
124 }
125
126 fn get_indent(&self, depth: usize) -> String {
127 let indent_char = match self.config.indent_char {
128 IndentChar::Space => " ", IndentChar::Tab => "\t", };
131 indent_char.repeat(depth * self.config.indent_width)
132 }
133
134 fn escape_text(&self, text: &str) -> String {
135 text.replace('&', "&")
136 .replace('<', "<")
137 .replace('>', ">")
138 }
139
140 fn escape_attribute(&self, text: &str) -> String {
141 text.replace('&', "&")
142 .replace('<', "<")
143 .replace('>', ">")
144 .replace('"', """)
145 .replace('\'', "'")
146 }
147
148 fn write_comment(
150 &self,
151 writer: &mut impl Write,
152 comment: &ddex_core::models::Comment,
153 depth: usize,
154 ) -> Result<(), BuildError> {
155 let indent = match comment.position {
156 CommentPosition::Before | CommentPosition::After => {
157 self.get_indent(depth.saturating_sub(1))
159 }
160 CommentPosition::FirstChild | CommentPosition::LastChild => {
161 self.get_indent(depth)
163 }
164 CommentPosition::Inline => {
165 String::new()
167 }
168 };
169
170 let comment_xml = comment.to_xml();
172 writeln!(writer, "{}{}", indent, comment_xml)?;
173
174 Ok(())
175 }
176}
177
178