1use crate::escape::escape_to;
6use std::io::{self, Write};
7
8pub struct XmlWriter<W: Write> {
10 writer: W,
11 element_stack: Vec<String>,
13 in_tag: bool,
15 indent: Option<IndentConfig>,
17 level: usize,
19 last_was_start: bool,
21}
22
23#[derive(Clone)]
25pub struct IndentConfig {
26 pub indent_str: String,
28 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 #[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 #[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 #[inline]
70 pub fn into_inner(self) -> W {
71 self.writer
72 }
73
74 #[inline]
76 pub fn depth(&self) -> usize {
77 self.element_stack.len()
78 }
79
80 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 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 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 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.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 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 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 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 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 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 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 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 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 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 pub fn flush(&mut self) -> io::Result<()> {
219 self.writer.flush()
220 }
221}
222
223pub struct StringXmlWriter {
225 writer: XmlWriter<Vec<u8>>,
226}
227
228impl StringXmlWriter {
229 pub fn new() -> Self {
231 Self {
232 writer: XmlWriter::new(Vec::new()),
233 }
234 }
235
236 pub fn with_indent(indent: IndentConfig) -> Self {
238 Self {
239 writer: XmlWriter::with_indent(Vec::new(), indent),
240 }
241 }
242
243 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><>&"'</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 "quotes""/>"#);
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}