ddex_builder/generator/
xml_writer.rs

1//! XML serialization from AST
2
3use crate::ast::{Element, Node, AST};
4use crate::determinism::{DeterminismConfig, IndentChar};
5use crate::error::BuildError;
6use ddex_core::models::CommentPosition; // Fixed import
7use indexmap::IndexMap;
8use std::io::Write;
9
10/// XML Writer for converting AST to XML string
11pub struct XmlWriter {
12    config: DeterminismConfig,
13}
14
15impl XmlWriter {
16    /// Create a new XML writer
17    pub fn new(config: DeterminismConfig) -> Self {
18        Self { config }
19    }
20
21    /// Write AST to XML string
22    pub fn write(&self, ast: &AST) -> Result<String, BuildError> {
23        let mut buffer = Vec::new();
24
25        // Write XML declaration
26        writeln!(&mut buffer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
27
28        // Write root element with namespaces
29        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        // Start tag
51        write!(writer, "{}<", indent)?;
52
53        // Add namespace prefix if needed
54        let element_name = if let Some(ns) = &element.namespace {
55            format!("{}:{}", ns, element.name)
56        } else if depth == 0 && !namespaces.is_empty() {
57            // Root element gets default namespace prefix if available
58            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        // Add namespace declarations on root element
70        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        // Add attributes (in deterministic order)
81        for (key, value) in &element.attributes {
82            write!(writer, " {}=\"{}\"", key, self.escape_attribute(value))?;
83        }
84
85        // Check if we have children
86        if element.children.is_empty() {
87            writeln!(writer, "/>")?;
88        } else {
89            // Check if we only have text content
90            let only_text =
91                element.children.len() == 1 && matches!(&element.children[0], Node::Text(_));
92
93            if only_text {
94                // Inline text content
95                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                // Has child elements
102                writeln!(writer, ">")?;
103
104                // Write children
105                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                // Close tag
125                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 => " ", // Fixed: removed super::determinism::
135            IndentChar::Tab => "\t",  // Fixed: removed super::determinism::
136        };
137        indent_char.repeat(depth * self.config.indent_width)
138    }
139
140    fn escape_text(&self, text: &str) -> String {
141        text.replace('&', "&amp;")
142            .replace('<', "&lt;")
143            .replace('>', "&gt;")
144    }
145
146    fn escape_attribute(&self, text: &str) -> String {
147        text.replace('&', "&amp;")
148            .replace('<', "&lt;")
149            .replace('>', "&gt;")
150            .replace('"', "&quot;")
151            .replace('\'', "&apos;")
152    }
153
154    /// Write a structured comment with position-aware formatting
155    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                // Comments at element level use element indentation
164                self.get_indent(depth.saturating_sub(1))
165            }
166            CommentPosition::FirstChild | CommentPosition::LastChild => {
167                // Comments inside elements use child indentation
168                self.get_indent(depth)
169            }
170            CommentPosition::Inline => {
171                // Inline comments don't get indentation
172                String::new()
173            }
174        };
175
176        // Use the comment's XML formatting which handles escaping
177        let comment_xml = comment.to_xml();
178        writeln!(writer, "{}{}", indent, comment_xml)?;
179
180        Ok(())
181    }
182}
183
184// Removed duplicate From<std::io::Error> implementation
185// (it's already in error.rs)