ddex_builder/generator/
xml_writer.rs

1//! XML serialization from AST
2
3use crate::ast::{AST, Element, Node};
4use crate::determinism::{DeterminismConfig, IndentChar};
5use ddex_core::models::CommentPosition;  // Fixed import
6use crate::error::BuildError;
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(&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        // Start tag
45        write!(writer, "{}<", indent)?;
46        
47        // Add namespace prefix if needed
48        let element_name = if let Some(ns) = &element.namespace {
49            format!("{}:{}", ns, element.name)
50        } else if depth == 0 && !namespaces.is_empty() {
51            // Root element gets default namespace prefix if available
52            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        // Add namespace declarations on root element
64        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        // Add attributes (in deterministic order)
75        for (key, value) in &element.attributes {
76            write!(writer, " {}=\"{}\"", key, self.escape_attribute(value))?;
77        }
78        
79        // Check if we have children
80        if element.children.is_empty() {
81            writeln!(writer, "/>")?;
82        } else {
83            // Check if we only have text content
84            let only_text = element.children.len() == 1 && 
85                matches!(&element.children[0], Node::Text(_));
86            
87            if only_text {
88                // Inline text content
89                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                // Has child elements
96                writeln!(writer, ">")?;
97                
98                // Write children
99                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                // Close tag
119                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 => " ",  // Fixed: removed super::determinism::
129            IndentChar::Tab => "\t",   // Fixed: removed super::determinism::
130        };
131        indent_char.repeat(depth * self.config.indent_width)
132    }
133    
134    fn escape_text(&self, text: &str) -> String {
135        text.replace('&', "&amp;")
136            .replace('<', "&lt;")
137            .replace('>', "&gt;")
138    }
139    
140    fn escape_attribute(&self, text: &str) -> String {
141        text.replace('&', "&amp;")
142            .replace('<', "&lt;")
143            .replace('>', "&gt;")
144            .replace('"', "&quot;")
145            .replace('\'', "&apos;")
146    }
147    
148    /// Write a structured comment with position-aware formatting
149    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                // Comments at element level use element indentation
158                self.get_indent(depth.saturating_sub(1))
159            }
160            CommentPosition::FirstChild | CommentPosition::LastChild => {
161                // Comments inside elements use child indentation
162                self.get_indent(depth)
163            }
164            CommentPosition::Inline => {
165                // Inline comments don't get indentation
166                String::new()
167            }
168        };
169        
170        // Use the comment's XML formatting which handles escaping
171        let comment_xml = comment.to_xml();
172        writeln!(writer, "{}{}", indent, comment_xml)?;
173        
174        Ok(())
175    }
176}
177
178// Removed duplicate From<std::io::Error> implementation
179// (it's already in error.rs)