serde_xml/
writer.rs

1//! Low-level XML writer.
2//!
3//! This module provides a fast XML writer that produces well-formed XML output.
4
5use crate::escape::escape_to;
6use std::io::{self, Write};
7
8/// An XML writer that produces well-formed XML output.
9pub struct XmlWriter<W: Write> {
10    writer: W,
11    /// Stack of open element names.
12    element_stack: Vec<String>,
13    /// Whether we're currently in an element tag (before the closing >).
14    in_tag: bool,
15    /// Indentation settings.
16    indent: Option<IndentConfig>,
17    /// Current indentation level.
18    level: usize,
19    /// Whether the last write was a start element (for formatting).
20    last_was_start: bool,
21}
22
23/// Indentation configuration.
24#[derive(Clone)]
25pub struct IndentConfig {
26    /// Characters to use for each level of indentation.
27    pub indent_str: String,
28    /// Whether to add a newline before each element.
29    pub newlines: bool,
30}
31
32impl Default for IndentConfig {
33    fn default() -> Self {
34        Self {
35            indent_str: "  ".to_string(),
36            newlines: true,
37        }
38    }
39}
40
41impl<W: Write> XmlWriter<W> {
42    /// Creates a new XML writer.
43    #[inline]
44    pub fn new(writer: W) -> Self {
45        Self {
46            writer,
47            element_stack: Vec::new(),
48            in_tag: false,
49            indent: None,
50            level: 0,
51            last_was_start: false,
52        }
53    }
54
55    /// Creates a new XML writer with indentation.
56    #[inline]
57    pub fn with_indent(writer: W, indent: IndentConfig) -> Self {
58        Self {
59            writer,
60            element_stack: Vec::new(),
61            in_tag: false,
62            indent: Some(indent),
63            level: 0,
64            last_was_start: false,
65        }
66    }
67
68    /// Returns the inner writer.
69    #[inline]
70    pub fn into_inner(self) -> W {
71        self.writer
72    }
73
74    /// Returns the current nesting depth.
75    #[inline]
76    pub fn depth(&self) -> usize {
77        self.element_stack.len()
78    }
79
80    /// Writes the XML declaration.
81    pub fn write_declaration(&mut self, version: &str, encoding: Option<&str>) -> io::Result<()> {
82        self.close_tag_if_open()?;
83        write!(self.writer, "<?xml version=\"{}\"", version)?;
84        if let Some(enc) = encoding {
85            write!(self.writer, " encoding=\"{}\"", enc)?;
86        }
87        self.writer.write_all(b"?>")
88    }
89
90    /// Starts an element.
91    pub fn start_element(&mut self, name: &str) -> io::Result<()> {
92        self.close_tag_if_open()?;
93        self.write_indent()?;
94        write!(self.writer, "<{}", name)?;
95        self.element_stack.push(name.to_string());
96        self.in_tag = true;
97        self.last_was_start = true;
98        self.level += 1;
99        Ok(())
100    }
101
102    /// Writes an attribute for the current element.
103    pub fn write_attribute(&mut self, name: &str, value: &str) -> io::Result<()> {
104        if !self.in_tag {
105            return Err(io::Error::new(
106                io::ErrorKind::InvalidInput,
107                "cannot write attribute outside of element tag",
108            ));
109        }
110        write!(self.writer, " {}=\"", name)?;
111        self.write_escaped(value)?;
112        self.writer.write_all(b"\"")
113    }
114
115    /// Ends the current element.
116    pub fn end_element(&mut self) -> io::Result<()> {
117        self.level = self.level.saturating_sub(1);
118
119        if let Some(name) = self.element_stack.pop() {
120            if self.in_tag {
121                // Self-closing tag
122                self.writer.write_all(b"/>")?;
123                self.in_tag = false;
124            } else {
125                if !self.last_was_start {
126                    self.write_indent()?;
127                }
128                write!(self.writer, "</{}>", name)?;
129            }
130            self.last_was_start = false;
131            Ok(())
132        } else {
133            Err(io::Error::new(
134                io::ErrorKind::InvalidInput,
135                "no element to close",
136            ))
137        }
138    }
139
140    /// Writes text content.
141    pub fn write_text(&mut self, text: &str) -> io::Result<()> {
142        self.close_tag_if_open()?;
143        self.write_escaped(text)?;
144        self.last_was_start = false;
145        Ok(())
146    }
147
148    /// Writes a CDATA section.
149    pub fn write_cdata(&mut self, data: &str) -> io::Result<()> {
150        self.close_tag_if_open()?;
151        write!(self.writer, "<![CDATA[{}]]>", data)
152    }
153
154    /// Writes a comment.
155    pub fn write_comment(&mut self, comment: &str) -> io::Result<()> {
156        self.close_tag_if_open()?;
157        self.write_indent()?;
158        write!(self.writer, "<!-- {} -->", comment)
159    }
160
161    /// Writes a processing instruction.
162    pub fn write_pi(&mut self, target: &str, data: Option<&str>) -> io::Result<()> {
163        self.close_tag_if_open()?;
164        self.write_indent()?;
165        write!(self.writer, "<?{}", target)?;
166        if let Some(d) = data {
167            write!(self.writer, " {}", d)?;
168        }
169        self.writer.write_all(b"?>")
170    }
171
172    /// Writes a complete element with text content.
173    pub fn write_element(&mut self, name: &str, content: &str) -> io::Result<()> {
174        self.start_element(name)?;
175        self.write_text(content)?;
176        self.end_element()
177    }
178
179    /// Writes an empty element.
180    pub fn write_empty_element(&mut self, name: &str) -> io::Result<()> {
181        self.close_tag_if_open()?;
182        self.write_indent()?;
183        write!(self.writer, "<{}/>", name)?;
184        self.last_was_start = false;
185        Ok(())
186    }
187
188    /// Closes the opening tag if one is open.
189    fn close_tag_if_open(&mut self) -> io::Result<()> {
190        if self.in_tag {
191            self.writer.write_all(b">")?;
192            self.in_tag = false;
193        }
194        Ok(())
195    }
196
197    /// Writes indentation if configured.
198    fn write_indent(&mut self) -> io::Result<()> {
199        if let Some(ref indent) = self.indent {
200            if indent.newlines && self.level > 0 {
201                self.writer.write_all(b"\n")?;
202            }
203            for _ in 0..self.level.saturating_sub(1) {
204                self.writer.write_all(indent.indent_str.as_bytes())?;
205            }
206        }
207        Ok(())
208    }
209
210    /// Writes escaped text.
211    fn write_escaped(&mut self, s: &str) -> io::Result<()> {
212        let mut escaped = String::with_capacity(s.len());
213        escape_to(s, &mut escaped);
214        self.writer.write_all(escaped.as_bytes())
215    }
216
217    /// Flushes the writer.
218    pub fn flush(&mut self) -> io::Result<()> {
219        self.writer.flush()
220    }
221}
222
223/// A string-based XML writer for convenience.
224pub struct StringXmlWriter {
225    writer: XmlWriter<Vec<u8>>,
226}
227
228impl StringXmlWriter {
229    /// Creates a new string-based XML writer.
230    pub fn new() -> Self {
231        Self {
232            writer: XmlWriter::new(Vec::new()),
233        }
234    }
235
236    /// Creates a new string-based XML writer with indentation.
237    pub fn with_indent(indent: IndentConfig) -> Self {
238        Self {
239            writer: XmlWriter::with_indent(Vec::new(), indent),
240        }
241    }
242
243    /// Consumes the writer and returns the XML string.
244    pub fn into_string(self) -> String {
245        String::from_utf8(self.writer.into_inner()).unwrap_or_default()
246    }
247}
248
249impl Default for StringXmlWriter {
250    fn default() -> Self {
251        Self::new()
252    }
253}
254
255impl std::ops::Deref for StringXmlWriter {
256    type Target = XmlWriter<Vec<u8>>;
257
258    fn deref(&self) -> &Self::Target {
259        &self.writer
260    }
261}
262
263impl std::ops::DerefMut for StringXmlWriter {
264    fn deref_mut(&mut self) -> &mut Self::Target {
265        &mut self.writer
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    fn write_to_string<F>(f: F) -> String
274    where
275        F: FnOnce(&mut XmlWriter<Vec<u8>>) -> io::Result<()>,
276    {
277        let mut writer = XmlWriter::new(Vec::new());
278        f(&mut writer).unwrap();
279        String::from_utf8(writer.into_inner()).unwrap()
280    }
281
282    #[test]
283    fn test_simple_element() {
284        let result = write_to_string(|w| {
285            w.start_element("root")?;
286            w.end_element()
287        });
288        assert_eq!(result, "<root/>");
289    }
290
291    #[test]
292    fn test_element_with_text() {
293        let result = write_to_string(|w| {
294            w.start_element("root")?;
295            w.write_text("Hello")?;
296            w.end_element()
297        });
298        assert_eq!(result, "<root>Hello</root>");
299    }
300
301    #[test]
302    fn test_element_with_attributes() {
303        let result = write_to_string(|w| {
304            w.start_element("root")?;
305            w.write_attribute("id", "1")?;
306            w.write_attribute("name", "test")?;
307            w.end_element()
308        });
309        assert_eq!(result, r#"<root id="1" name="test"/>"#);
310    }
311
312    #[test]
313    fn test_nested_elements() {
314        let result = write_to_string(|w| {
315            w.start_element("root")?;
316            w.start_element("child")?;
317            w.write_text("content")?;
318            w.end_element()?;
319            w.end_element()
320        });
321        assert_eq!(result, "<root><child>content</child></root>");
322    }
323
324    #[test]
325    fn test_escaped_content() {
326        let result = write_to_string(|w| {
327            w.start_element("root")?;
328            w.write_text("<>&\"\'")?;
329            w.end_element()
330        });
331        assert_eq!(result, "<root>&lt;&gt;&amp;&quot;&apos;</root>");
332    }
333
334    #[test]
335    fn test_escaped_attribute() {
336        let result = write_to_string(|w| {
337            w.start_element("root")?;
338            w.write_attribute("attr", "value with \"quotes\"")?;
339            w.end_element()
340        });
341        assert_eq!(result, r#"<root attr="value with &quot;quotes&quot;"/>"#);
342    }
343
344    #[test]
345    fn test_xml_declaration() {
346        let result = write_to_string(|w| {
347            w.write_declaration("1.0", Some("UTF-8"))?;
348            w.start_element("root")?;
349            w.end_element()
350        });
351        assert_eq!(result, r#"<?xml version="1.0" encoding="UTF-8"?><root/>"#);
352    }
353
354    #[test]
355    fn test_comment() {
356        let result = write_to_string(|w| {
357            w.start_element("root")?;
358            w.write_comment("This is a comment")?;
359            w.end_element()
360        });
361        assert!(result.contains("<!-- This is a comment -->"));
362    }
363
364    #[test]
365    fn test_cdata() {
366        let result = write_to_string(|w| {
367            w.start_element("root")?;
368            w.write_cdata("<special>content</special>")?;
369            w.end_element()
370        });
371        assert_eq!(result, "<root><![CDATA[<special>content</special>]]></root>");
372    }
373
374    #[test]
375    fn test_empty_element() {
376        let result = write_to_string(|w| {
377            w.write_empty_element("br")
378        });
379        assert_eq!(result, "<br/>");
380    }
381
382    #[test]
383    fn test_write_element_shorthand() {
384        let result = write_to_string(|w| {
385            w.write_element("name", "John")
386        });
387        assert_eq!(result, "<name>John</name>");
388    }
389
390    #[test]
391    fn test_depth() {
392        let mut writer = XmlWriter::new(Vec::new());
393        assert_eq!(writer.depth(), 0);
394
395        writer.start_element("a").unwrap();
396        assert_eq!(writer.depth(), 1);
397
398        writer.start_element("b").unwrap();
399        assert_eq!(writer.depth(), 2);
400
401        writer.end_element().unwrap();
402        assert_eq!(writer.depth(), 1);
403
404        writer.end_element().unwrap();
405        assert_eq!(writer.depth(), 0);
406    }
407
408    #[test]
409    fn test_processing_instruction() {
410        let result = write_to_string(|w| {
411            w.write_pi("xml-stylesheet", Some("type=\"text/xsl\" href=\"style.xsl\""))
412        });
413        assert_eq!(result, r#"<?xml-stylesheet type="text/xsl" href="style.xsl"?>"#);
414    }
415
416    #[test]
417    fn test_indented_output() {
418        let mut writer = XmlWriter::with_indent(Vec::new(), IndentConfig::default());
419        writer.start_element("root").unwrap();
420        writer.start_element("child").unwrap();
421        writer.write_text("text").unwrap();
422        writer.end_element().unwrap();
423        writer.end_element().unwrap();
424
425        let result = String::from_utf8(writer.into_inner()).unwrap();
426        assert!(result.contains("\n"));
427    }
428}