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