Skip to content

Commit

Permalink
Implement API to add newlines between attributes when writing XML.
Browse files Browse the repository at this point in the history
  • Loading branch information
areleu authored and Mingun committed Mar 31, 2024
1 parent 8fc158a commit ea6a118
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 4 deletions.
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
214 changes: 210 additions & 4 deletions src/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ impl<W> Writer<W> {
ElementWriter {
writer: self,
start_tag: BytesStart::new(name.as_ref()),
state: AttributeIndent::NoneAttributesWritten,
}
}
}
Expand Down Expand Up @@ -335,11 +336,23 @@ impl<W: Write> Writer<W> {
}
}

/// 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<W>,
start_tag: BytesStart<'a>,
state: AttributeIndent,
}

impl<'a, W> ElementWriter<'a, W> {
Expand All @@ -348,6 +361,26 @@ impl<'a, W> ElementWriter<'a, W> {
where
I: Into<Attribute<'b>>,
{
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
}
Expand All @@ -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
/// <!-- result of the code above -->
/// <element first="1" second="2"
/// third="3" fourth="4"/>
///
/// <!-- if uncomment only (1) - indent depends on indentation
/// settings - 2 spaces here -->
/// <element
/// first="1" second="2"
/// third="3" fourth="4"/>
///
/// <!-- if uncomment only (2) -->
/// <element first="1" second="2"
/// third="3" fourth="4"
/// />
/// ```
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> {
Expand Down Expand Up @@ -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
Expand All @@ -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)]
Expand Down Expand Up @@ -781,4 +882,109 @@ mod indentation {
</outer>"#
);
}

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#"<element
first="1" second="2"
third="3" fourth="4"/>"#
);
}

#[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#"<element first="1" second="2"
third="3" fourth="4"/>"#
);
}

#[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#"<element
first="1" second="2"
third="3" fourth="4"
/>
<element first="1" second="2"
third="3" fourth="4"
/>"#
);
}

#[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");
}
}
}

0 comments on commit ea6a118

Please sign in to comment.