Repository: tafia/quick-xml
Pull request #104 (2018-01-03 07:51:25)

Basic indentation support in Writer
Thanks for the great library!
Not sure why there is no indentation support in Writer, but I need indentation when producing human-readable XML files, so I added some basic indentation support there.

mxj4: The latest commit addressed most of your concerns, can you take a look again? Thx!
tafia: Thanks for the changes!
I have just merged the previous PR which migrates to Failure and unfortunately it conflicts with your PR.
Can you please rebase?
Sorry for that.
tafia: Rebased
mxj4: Rebased.

Patch:
diff --git a/src/writer.rs b/src/writer.rs
index 1d6c65e..0207b66 100644
--- a/src/writer.rs
+++ b/src/writer.rs
@@ -63,12 +63,21 @@ use events::Event;
 pub struct Writer<W: Write> {
     /// underlying writer
     writer: W,
+    indent: Option<Indentation>,
 }
 
 impl<W: Write> Writer<W> {
     /// Creates a Writer from a generic Write
     pub fn new(inner: W) -> Writer<W> {
-        Writer { writer: inner }
+        Writer { writer: inner, indent: None }
+    }
+
+    /// Creates a Writer with configured whitespace indents from a generic Write
+    pub fn new_with_indent(inner: W, indent_char: u8, indent_size: usize) -> Writer<W> {
+        Writer {
+            writer: inner,
+            indent: Some(Indentation::new(indent_char, indent_size)),
+        }
     }
 
     /// Consumes this `Writer`, returning the underlying writer.
@@ -78,18 +87,37 @@ impl<W: Write> Writer<W> {
 
     /// Writes the given event to the underlying writer.
     pub fn write_event<'a, E: AsRef<Event<'a>>>(&mut self, event: E) -> Result<usize> {
-        match *event.as_ref() {
-            Event::Start(ref e) => self.write_wrapped(b"<", e, b">"),
-            Event::End(ref e) => self.write_wrapped(b"</", e, b">"),
+        let mut next_should_line_break = true;
+        let result = match *event.as_ref() {
+            Event::Start(ref e) => {
+                let result = self.write_wrapped(b"<", e, b">");
+                if let Some(i) = self.indent.as_mut() {
+                    i.grow();
+                }
+                result
+            },
+            Event::End(ref e) => {
+                if let Some(i) = self.indent.as_mut() {
+                    i.shrink();
+                }
+                self.write_wrapped(b"</", e, b">")
+            },
             Event::Empty(ref e) => self.write_wrapped(b"<", e, b"/>"),
-            Event::Text(ref e) => self.write(&e.escaped()),
+            Event::Text(ref e) => {
+                next_should_line_break = false;
+                self.write(&e.escaped())
+            },
             Event::Comment(ref e) => self.write_wrapped(b"<!--", e, b"-->"),
             Event::CData(ref e) => self.write_wrapped(b"<![CDATA[", e, b"]]>"),
             Event::Decl(ref e) => self.write_wrapped(b"<?", e, b"?>"),
             Event::PI(ref e) => self.write_wrapped(b"<?", e, b"?>"),
             Event::DocType(ref e) => self.write_wrapped(b"<!DOCTYPE", e, b">"),
             Event::Eof => Ok(0),
+        };
+        if let Some(i) = self.indent.as_mut() {
+            i.should_line_break = next_should_line_break;
         }
+        result
     }
 
     /// Writes bytes
@@ -100,6 +128,45 @@ impl<W: Write> Writer<W> {
 
     #[inline]
     fn write_wrapped(&mut self, before: &[u8], value: &[u8], after: &[u8]) -> Result<usize> {
-        Ok(self.write(before)? + self.write(value)? + self.write(after)?)
+        let mut wrote = 0;
+        if let Some(ref i) = self.indent {
+            if i.should_line_break {
+                wrote = self.writer.write(b"\n").map_err(Error::Io)? +
+                    self.writer.write(&i.indents[..i.indents_len]).map_err(Error::Io)?;
+            }
+        }
+        Ok(wrote + self.write(before)? + self.write(value)? + self.write(after)?)
+    }
+}
+
+#[derive(Clone)]
+struct Indentation {
+    should_line_break: bool,
+    indent_char: u8,
+    indent_size: usize,
+    indents: Vec<u8>,
+    indents_len: usize,
+}
+
+impl Indentation {
+    fn new(indent_char: u8, indent_size: usize) -> Indentation {
+        Indentation {
+            should_line_break: false,
+            indent_char,
+            indent_size,
+            indents: vec![indent_char; 128],
+            indents_len: 0,
+        }
+    }
+
+    fn grow(&mut self) {
+        self.indents_len = self.indents_len + self.indent_size;
+        if self.indents_len > self.indents.len() {
+            self.indents.resize(self.indents_len, self.indent_char);
+        }
+    }
+
+    fn shrink(&mut self) {
+        self.indents_len = self.indents_len - self.indent_size;
     }
 }


Test patch:
diff --git a/tests/documents/test_writer_indent.xml b/tests/documents/test_writer_indent.xml
new file mode 100644
index 0000000..3980ddd
--- /dev/null
+++ b/tests/documents/test_writer_indent.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dom="http://www.w3.org/2001/xml-events" xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0" xmlns:math="http://www.w3.org/1998/Math/MathML" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:ooo="http://openoffice.org/2004/office" xmlns:oooc="http://openoffice.org/2004/calc" xmlns:ooow="http://openoffice.org/2004/writer" xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:svg="http://www.w3.org/2000/svg" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:xforms="http://www.w3.org/2002/xforms" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" office:version="1.0">
+    <office:scripts />
+    <office:font-face-decls>
+        <style:font-face style:name="Tahoma1" svg:font-family="Tahoma" />
+        <style:font-face style:name="Andale Sans UI" svg:font-family="'Andale Sans UI'" style:font-pitch="variable" />
+        <style:font-face style:name="Tahoma" svg:font-family="Tahoma" style:font-pitch="variable" />
+        <style:font-face style:name="Thorndale" svg:font-family="Thorndale" style:font-family-generic="roman" style:font-pitch="variable" />
+    </office:font-face-decls>
+    <office:automatic-styles>
+        <style:style style:name="P1" style:family="paragraph" style:parent-style-name="Standard">
+            <style:paragraph-properties fo:text-align="center" style:justify-single-word="false" />
+            <style:text-properties fo:font-size="8pt" />
+        </style:style>
+        <style:style style:name="P2" style:family="paragraph" style:parent-style-name="Standard">
+            <style:paragraph-properties fo:text-align="center" style:justify-single-word="false" />
+            <style:text-properties fo:font-size="10pt" />
+        </style:style>
+        <style:style style:name="P3" style:family="paragraph" style:parent-style-name="Standard">
+            <style:paragraph-properties fo:text-align="center" style:justify-single-word="false" />
+            <style:text-properties fo:font-size="12pt" />
+        </style:style>
+        <style:style style:name="P4" style:family="paragraph" style:parent-style-name="Standard">
+            <style:paragraph-properties fo:text-align="center" style:justify-single-word="false" />
+            <style:text-properties fo:font-size="14pt" />
+        </style:style>
+        <style:style style:name="P5" style:family="paragraph" style:parent-style-name="Standard">
+            <style:paragraph-properties fo:text-align="center" style:justify-single-word="false" />
+        </style:style>
+    </office:automatic-styles>
+    <office:body>
+        <office:text>
+            <text:sequence-decls>
+                <!-- This is a comment with indentation! -->
+                <text:sequence-decl text:display-outline-level="0" text:name="Illustration" />
+                <text:sequence-decl text:display-outline-level="0" text:name="Table" />
+                <text:sequence-decl text:display-outline-level="0" text:name="Text" />
+                <text:sequence-decl text:display-outline-level="0" text:name="Drawing" />
+            </text:sequence-decls>
+            <text:p text:style-name="P1">This is a simple test document to demonstrate the DocumentLoader example!</text:p>
+            <text:p text:style-name="P2">This is a simple test document to demonstrate the DocumentLoader example!</text:p>
+            <text:p text:style-name="P3">This is a simple test document to demonstrate the DocumentLoader example!</text:p>
+            <text:p text:style-name="P4">This is a simple test document to demonstrate the DocumentLoader example!</text:p>
+            <text:p text:style-name="P5">This is a simple test document to demonstrate the DocumentLoader example!</text:p>
+        </office:text>
+    </office:body>
+</office:document-content>
\ No newline at end of file
diff --git a/tests/unit_tests.rs b/tests/unit_tests.rs
index 5f42422..92d7acd 100644
--- a/tests/unit_tests.rs
+++ b/tests/unit_tests.rs
@@ -264,6 +264,26 @@ fn test_writer_borrow() {
     assert_eq!(result, txt.as_bytes());
 }
 
+#[test]
+fn test_writer_indent() {
+    let txt = include_str!("../tests/documents/test_writer_indent.xml");
+    let mut reader = Reader::from_str(txt);
+    reader.trim_text(true);
+    let mut writer = Writer::new_with_indent(Cursor::new(Vec::new()), b' ', 4);
+    let mut buf = Vec::new();
+    loop {
+        match reader.read_event(&mut buf) {
+            Ok(Eof) => break,
+            Ok(e) => assert!(writer.write_event(e).is_ok()),
+            Err(e) => panic!(e),
+        }
+    }
+
+    let result = writer.into_inner().into_inner();
+    // println!("{:?}", String::from_utf8_lossy(&result));
+    assert_eq!(result, txt.as_bytes());
+}
+
 #[test]
 fn test_write_empty_element_attrs() {
     let str_from = r#"<source attr="val"/>"#;