From ea6a1187fdf1218078dd1694ac6d93bdc3fa9f09 Mon Sep 17 00:00:00 2001 From: "Arellano Ruiz, Eugenio Salvador" Date: Thu, 28 Mar 2024 17:42:15 +0500 Subject: [PATCH] Implement API to add newlines between attributes when writing XML. --- Changelog.md | 2 + src/events/mod.rs | 10 +++ src/writer.rs | 214 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 222 insertions(+), 4 deletions(-) diff --git a/Changelog.md b/Changelog.md index 1ee04a0a..bd1e572d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -30,6 +30,7 @@ to get an offset of the error position. For `SyntaxError`s the range - [#362]: Added `BytesCData::minimal_escape()` which escapes only `&` and `<`. - [#362]: Added `Serializer::set_quote_level()` which allow to set desired level of escaping. - [#705]: Added `NsReader::prefixes()` to list all the prefixes currently declared. +- [#275]: Added `ElementWriter::new_line()` which enables pretty printing elements with multiple attributes. ### Bug Fixes @@ -61,6 +62,7 @@ to get an offset of the error position. For `SyntaxError`s the range - [#689]: `buffer_position()` now always report the position the parser last seen. To get an error position use `error_position()`. +[#275]: https://github.com/tafia/quick-xml/issues/275 [#362]: https://github.com/tafia/quick-xml/issues/362 [#513]: https://github.com/tafia/quick-xml/issues/513 [#622]: https://github.com/tafia/quick-xml/issues/622 diff --git a/src/events/mod.rs b/src/events/mod.rs index 546ad392..d7efe710 100644 --- a/src/events/mod.rs +++ b/src/events/mod.rs @@ -251,6 +251,16 @@ impl<'a> BytesStart<'a> { bytes.push(b'"'); } + /// Adds new line in existing element + pub fn push_newline(&mut self) { + self.buf.to_mut().push(b'\n'); + } + + /// Adds indentation bytes in existing element + pub(crate) fn push_indent(&mut self, indent: &[u8]) { + self.buf.to_mut().extend_from_slice(indent); + } + /// Remove all attributes from the ByteStart pub fn clear_attributes(&mut self) -> &mut BytesStart<'a> { self.buf.to_mut().truncate(self.name_len); diff --git a/src/writer.rs b/src/writer.rs index 122eb488..461319b3 100644 --- a/src/writer.rs +++ b/src/writer.rs @@ -150,6 +150,7 @@ impl Writer { ElementWriter { writer: self, start_tag: BytesStart::new(name.as_ref()), + state: AttributeIndent::NoneAttributesWritten, } } } @@ -335,11 +336,23 @@ impl Writer { } } +/// Track indent inside elements state +#[derive(Debug)] +enum AttributeIndent { + /// Initial state + NoneAttributesWritten, + /// Keep indent that should be used if `new_line()` would be called + SomeAttributesWritten(usize), + /// Write specified indent before writing attribute in `with_attribute()` + Indent(usize), +} + /// A struct to write an element. Contains methods to add attributes and inner /// elements to the element pub struct ElementWriter<'a, W> { writer: &'a mut Writer, start_tag: BytesStart<'a>, + state: AttributeIndent, } impl<'a, W> ElementWriter<'a, W> { @@ -348,6 +361,26 @@ impl<'a, W> ElementWriter<'a, W> { where I: Into>, { + if let Some(i) = self.writer.indent.as_mut() { + let next_indent = match self.state { + // Neither .new_line() or .with_attribute() yet called + // If newline inside attributes will be requested, we should indent them + // by the length of tag name and one space + AttributeIndent::NoneAttributesWritten => self.start_tag.name().as_ref().len() + 1, + // .new_line() was not called, but .with_attribute() was. + // use the previously calculated indent + AttributeIndent::SomeAttributesWritten(indent) => indent, + AttributeIndent::Indent(indent) => { + // Indent was requested by previous call to .new_line(), write it + // New line was already written + self.start_tag.push_indent(i.additional(indent)); + indent + } + }; + // Save the indent that we should use next time when .new_line() be called + self.state = AttributeIndent::SomeAttributesWritten(next_indent); + } + // write attribute self.start_tag.push_attribute(attr); self } @@ -363,6 +396,64 @@ impl<'a, W> ElementWriter<'a, W> { self.start_tag.extend_attributes(attributes); self } + + /// Push a new line inside an element. + /// + /// # Examples + /// + /// The following code + /// + /// ```rust + /// # use quick_xml::writer::Writer; + /// let mut buffer = Vec::new(); + /// let mut writer = Writer::new_with_indent(&mut buffer, b' ', 2); + /// writer + /// .create_element("element") + /// //.new_line() (1) + /// .with_attribute("first", "1") + /// .with_attribute("second", "2") + /// .new_line() + /// .with_attribute("third", "3") + /// .with_attribute("fourth", "4") + /// //.new_line() (2) + /// .write_empty(); + /// ``` + /// will produce the following XMLs: + /// ```xml + /// + /// + /// + /// + /// + /// + /// + /// + /// ``` + pub fn new_line(mut self) -> Self { + if let Some(i) = self.writer.indent.as_mut() { + match self.state { + // .new_line() called just after .create_element(). + // Use element indent to additionally indent attributes + AttributeIndent::NoneAttributesWritten => { + self.state = AttributeIndent::Indent(i.indent_size) + } + // .new_line() called when .with_attribute() was called at least once. + // Plan saved indent + AttributeIndent::SomeAttributesWritten(indent) => { + self.state = AttributeIndent::Indent(indent) + } + AttributeIndent::Indent(_) => {} + } + self.start_tag.push_newline(); + }; + self + } } impl<'a, W: Write> ElementWriter<'a, W> { @@ -458,10 +549,7 @@ impl Indentation { /// Increase indentation by one level pub fn grow(&mut self) { self.current_indent_len += self.indent_size; - if self.current_indent_len > self.indents.len() { - self.indents - .resize(self.current_indent_len, self.indent_char); - } + self.ensure(self.current_indent_len); } /// Decrease indentation by one level. Do nothing, if level already zero @@ -473,6 +561,19 @@ impl Indentation { pub fn current(&self) -> &[u8] { &self.indents[..self.current_indent_len] } + + /// Returns indent with current indent plus additional indent + pub fn additional(&mut self, additional_indent: usize) -> &[u8] { + let new_len = self.current_indent_len + additional_indent; + self.ensure(new_len); + &self.indents[..new_len] + } + + fn ensure(&mut self, new_len: usize) { + if new_len > self.indents.len() { + self.indents.resize(new_len, self.indent_char); + } + } } #[cfg(test)] @@ -781,4 +882,109 @@ mod indentation { "# ); } + + mod in_attributes { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn newline_first() { + let mut buffer = Vec::new(); + let mut writer = Writer::new_with_indent(&mut buffer, b' ', 4); + + writer + .create_element("element") + .new_line() + .with_attribute(("first", "1")) + .with_attribute(("second", "2")) + .new_line() + .with_attribute(("third", "3")) + .with_attribute(("fourth", "4")) + .write_empty() + .expect("write tag failed"); + + assert_eq!( + std::str::from_utf8(&buffer).unwrap(), + r#""# + ); + } + + #[test] + fn newline_inside() { + let mut buffer = Vec::new(); + let mut writer = Writer::new_with_indent(&mut buffer, b' ', 4); + + writer + .create_element("element") + .with_attribute(("first", "1")) + .with_attribute(("second", "2")) + .new_line() + .with_attribute(("third", "3")) + .with_attribute(("fourth", "4")) + .write_empty() + .expect("write tag failed"); + + assert_eq!( + std::str::from_utf8(&buffer).unwrap(), + r#""# + ); + } + + #[test] + fn newline_last() { + let mut buffer = Vec::new(); + let mut writer = Writer::new_with_indent(&mut buffer, b' ', 4); + + writer + .create_element("element") + .new_line() + .with_attribute(("first", "1")) + .with_attribute(("second", "2")) + .new_line() + .with_attribute(("third", "3")) + .with_attribute(("fourth", "4")) + .new_line() + .write_empty() + .expect("write tag failed"); + + writer + .create_element("element") + .with_attribute(("first", "1")) + .with_attribute(("second", "2")) + .new_line() + .with_attribute(("third", "3")) + .with_attribute(("fourth", "4")) + .new_line() + .write_empty() + .expect("write tag failed"); + + assert_eq!( + std::str::from_utf8(&buffer).unwrap(), + r#" +"# + ); + } + + #[test] + fn long_element_name() { + let mut buffer = Vec::new(); + let mut writer = Writer::new_with_indent(&mut buffer, b' ', 4); + + writer + .create_element(String::from("x").repeat(128).as_str()) + .with_attribute(("first", "1")) + .new_line() + .with_attribute(("second", "2")) + .write_empty() + .expect("Problem with indentation reference"); + } + } }