Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,17 @@

## Unreleased

Added a way to configure `Writer`. Now all configuration is contained in the `writer::Config`
struct and can be applied at once. When `serde-types` feature is enabled, configuration is serializable.

### New Features

- [#846]: Add methods `config()` and `config_mut()` to inspect and change the writer configuration.
- [#846]: Add ability to write space before `/>` in self-closed tags for maximum compatibility with
XHTML.
- [#846]: Add method `empty_element_handling()` as a more powerful alternative to `expand_empty_elements()`
in `Serializer`.

### Bug Fixes

### Misc Changes
Expand All @@ -25,6 +34,7 @@
of `NsReader`. Use `.resolver().bindings()` and `.resolver().resolve()` methods instead.
- [#913]: `Attributes::has_nil` now accepts `NamespaceResolver` instead of `Reader<R>`.

[#846]: https://github.com/tafia/quick-xml/issues/846
[#908]: https://github.com/tafia/quick-xml/pull/908
[#913]: https://github.com/tafia/quick-xml/pull/913

Expand Down
49 changes: 30 additions & 19 deletions src/se/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
use crate::de::TEXT_KEY;
use crate::se::element::{ElementSerializer, Struct, Tuple};
use crate::se::simple_type::{QuoteTarget, SimpleTypeSerializer};
use crate::se::{Indent, QuoteLevel, SeError, TextFormat, WriteResult, XmlName};
use crate::se::{
EmptyElementHandling, Indent, QuoteLevel, SeError, TextFormat, WriteResult, XmlName,
};
use serde::ser::{
Impossible, Serialize, SerializeSeq, SerializeTuple, SerializeTupleStruct, Serializer,
};
Expand Down Expand Up @@ -80,9 +82,8 @@ pub struct ContentSerializer<'w, 'i, W: Write> {
/// as a text that makes it impossible to distinguish between them during
/// deserialization. Instead of ambiguous serialization the error is returned.
pub allow_primitive: bool,
// If `true`, then empty elements will be serialized as `<element></element>`
// instead of `<element/>`.
pub expand_empty_elements: bool,
/// Specifies how empty elements are written.
pub empty_element_handling: EmptyElementHandling,
}

impl<'w, 'i, W: Write> ContentSerializer<'w, 'i, W> {
Expand Down Expand Up @@ -124,25 +125,35 @@ impl<'w, 'i, W: Write> ContentSerializer<'w, 'i, W> {
write_indent: self.write_indent,
text_format: self.text_format,
allow_primitive,
expand_empty_elements: self.expand_empty_elements,
empty_element_handling: self.empty_element_handling,
}
}

/// Writes `name` as self-closed tag
#[inline]
pub(super) fn write_empty(mut self, name: XmlName) -> Result<WriteResult, SeError> {
self.write_indent()?;
if self.expand_empty_elements {
self.writer.write_char('<')?;
self.writer.write_str(name.0)?;
self.writer.write_str("></")?;
self.writer.write_str(name.0)?;
self.writer.write_char('>')?;
} else {
self.writer.write_str("<")?;
self.writer.write_str(name.0)?;
self.writer.write_str("/>")?;

match self.empty_element_handling {
EmptyElementHandling::SelfClosed => {
self.writer.write_char('<')?;
self.writer.write_str(name.0)?;
self.writer.write_str("/>")?;
}
EmptyElementHandling::SelfClosedWithSpace => {
self.writer.write_char('<')?;
self.writer.write_str(name.0)?;
self.writer.write_str(" />")?;
}
EmptyElementHandling::Expanded => {
self.writer.write_char('<')?;
self.writer.write_str(name.0)?;
self.writer.write_str("></")?;
self.writer.write_str(name.0)?;
self.writer.write_char('>')?;
}
}

Ok(WriteResult::Element)
}

Expand Down Expand Up @@ -604,7 +615,7 @@ pub(super) mod tests {
write_indent: false,
text_format: TextFormat::Text,
allow_primitive: true,
expand_empty_elements: false,
empty_element_handling: EmptyElementHandling::SelfClosed,
};

let result = $data.serialize(ser).unwrap();
Expand All @@ -628,7 +639,7 @@ pub(super) mod tests {
write_indent: false,
text_format: TextFormat::Text,
allow_primitive: true,
expand_empty_elements: false,
empty_element_handling: EmptyElementHandling::SelfClosed,
};

match $data.serialize(ser).unwrap_err() {
Expand Down Expand Up @@ -1070,7 +1081,7 @@ pub(super) mod tests {
write_indent: false,
text_format: TextFormat::Text,
allow_primitive: true,
expand_empty_elements: false,
empty_element_handling: EmptyElementHandling::SelfClosed,
};

let result = $data.serialize(ser).unwrap();
Expand All @@ -1094,7 +1105,7 @@ pub(super) mod tests {
write_indent: false,
text_format: TextFormat::Text,
allow_primitive: true,
expand_empty_elements: false,
empty_element_handling: EmptyElementHandling::SelfClosed,
};

match $data.serialize(ser).unwrap_err() {
Expand Down
32 changes: 19 additions & 13 deletions src/se/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::se::content::ContentSerializer;
use crate::se::key::QNameSerializer;
use crate::se::simple_type::{QuoteTarget, SimpleSeq, SimpleTypeSerializer};
use crate::se::text::TextSerializer;
use crate::se::{SeError, WriteResult, XmlName};
use crate::se::{EmptyElementHandling, SeError, WriteResult, XmlName};
use serde::ser::{
Impossible, Serialize, SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant,
SerializeTuple, SerializeTupleStruct, SerializeTupleVariant, Serializer,
Expand Down Expand Up @@ -442,7 +442,7 @@ impl<'w, 'k, W: Write> Struct<'w, 'k, W> {
write_indent: self.write_indent,
text_format: self.ser.ser.text_format,
allow_primitive: true,
expand_empty_elements: self.ser.ser.expand_empty_elements,
empty_element_handling: self.ser.ser.empty_element_handling,
};

if key == TEXT_KEY {
Expand Down Expand Up @@ -479,12 +479,18 @@ impl<'w, 'k, W: Write> SerializeStruct for Struct<'w, 'k, W> {
self.ser.ser.indent.decrease();

if self.children.is_empty() {
if self.ser.ser.expand_empty_elements {
self.ser.ser.writer.write_str("></")?;
self.ser.ser.writer.write_str(self.ser.key.0)?;
self.ser.ser.writer.write_char('>')?;
} else {
self.ser.ser.writer.write_str("/>")?;
match self.ser.ser.empty_element_handling {
EmptyElementHandling::SelfClosed => {
self.ser.ser.writer.write_str("/>")?;
}
EmptyElementHandling::SelfClosedWithSpace => {
self.ser.ser.writer.write_str(" />")?;
}
EmptyElementHandling::Expanded => {
self.ser.ser.writer.write_str("></")?;
self.ser.ser.writer.write_str(self.ser.key.0)?;
self.ser.ser.writer.write_char('>')?;
}
}
} else {
self.ser.ser.writer.write_char('>')?;
Expand Down Expand Up @@ -635,7 +641,7 @@ mod tests {
write_indent: false,
text_format: TextFormat::Text,
allow_primitive: true,
expand_empty_elements: false,
empty_element_handling: EmptyElementHandling::SelfClosed,
},
key: XmlName("root"),
};
Expand All @@ -662,7 +668,7 @@ mod tests {
write_indent: false,
text_format: TextFormat::Text,
allow_primitive: true,
expand_empty_elements: false,
empty_element_handling: EmptyElementHandling::SelfClosed,
},
key: XmlName("root"),
};
Expand Down Expand Up @@ -1348,7 +1354,7 @@ mod tests {
write_indent: false,
text_format: TextFormat::Text,
allow_primitive: true,
expand_empty_elements: false,
empty_element_handling: EmptyElementHandling::SelfClosed,
},
key: XmlName("root"),
};
Expand All @@ -1375,7 +1381,7 @@ mod tests {
write_indent: false,
text_format: TextFormat::Text,
allow_primitive: true,
expand_empty_elements: false,
empty_element_handling: EmptyElementHandling::SelfClosed,
},
key: XmlName("root"),
};
Expand Down Expand Up @@ -2083,7 +2089,7 @@ mod tests {
write_indent: false,
text_format: TextFormat::Text,
allow_primitive: true,
expand_empty_elements: true,
empty_element_handling: EmptyElementHandling::Expanded,
},
key: XmlName("root"),
};
Expand Down
96 changes: 89 additions & 7 deletions src/se/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,26 @@ pub enum QuoteLevel {
Minimal,
}

/// Specifies how empty elements are serialized.
/// The default is self-closed with no space before the slash.
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
pub enum EmptyElementHandling {
/// Empty elements will be written as `<element/>`. This is the default behavior.
#[default]
SelfClosed,

/// The same, as [`SelfClosed`], but an extra space will be written before
/// the slash, so empty elements will be written as `<element />`.
/// This is recommended by the [W3C guidelines] for XHTML.
///
/// [`SelfClosed`]: Self::SelfClosed
/// [W3C guidelines]: https://www.w3.org/TR/xhtml1/#guidelines
SelfClosedWithSpace,

/// Empty elements will be expanded, as in: `<element></element>`.
Expanded,
}

/// Classification of the type written by the serializer.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteResult {
Expand Down Expand Up @@ -568,7 +588,7 @@ impl<'w, 'r, W: Write> Serializer<'w, 'r, W> {
write_indent: false,
text_format: TextFormat::Text,
allow_primitive: true,
expand_empty_elements: false,
empty_element_handling: EmptyElementHandling::SelfClosed,
},
root_tag: None,
}
Expand Down Expand Up @@ -635,21 +655,80 @@ impl<'w, 'r, W: Write> Serializer<'w, 'r, W> {
write_indent: false,
text_format: TextFormat::Text,
allow_primitive: true,
expand_empty_elements: false,
empty_element_handling: EmptyElementHandling::SelfClosed,
},
root_tag: root_tag.map(XmlName::try_from).transpose()?,
})
}

/// Enable or disable expansion of empty elements. Defaults to `false`.
/// Enable or disable expansion of empty elements. Defaults to [`EmptyElementHandling::SelfClosed`].
///
/// # Examples
///
/// ```
/// # use pretty_assertions::assert_eq;
/// # use serde::Serialize;
/// # use quick_xml::se::Serializer;
/// # use quick_xml::se::{Serializer, EmptyElementHandling};
/// #
/// #[derive(Debug, PartialEq, Serialize)]
/// struct Struct {
/// question: Option<String>,
/// }
///
/// let data = Struct {
/// question: None,
/// };
///
/// {
/// let mut buffer = String::new();
/// let mut ser = Serializer::new(&mut buffer);
/// ser.empty_element_handling(EmptyElementHandling::SelfClosed);
/// data.serialize(ser).unwrap();
/// assert_eq!(
/// buffer,
/// "<Struct><question/></Struct>"
/// );
/// }
///
/// {
/// let mut buffer = String::new();
/// let mut ser = Serializer::new(&mut buffer);
/// ser.empty_element_handling(EmptyElementHandling::SelfClosedWithSpace);
/// data.serialize(ser).unwrap();
/// assert_eq!(
/// buffer,
/// "<Struct><question /></Struct>"
/// );
/// }
///
/// {
/// let mut buffer = String::new();
/// let mut ser = Serializer::new(&mut buffer);
/// ser.empty_element_handling(EmptyElementHandling::Expanded);
/// data.serialize(ser).unwrap();
/// assert_eq!(
/// buffer,
/// "<Struct><question></question></Struct>"
/// );
/// }
/// ```
pub fn empty_element_handling(&mut self, handling: EmptyElementHandling) -> &mut Self {
self.ser.empty_element_handling = handling;
self
}

/// Enable or disable expansion of empty elements (without adding space before `/>`).
///
/// This is the historycally first way to configure empty element handling. You can use
/// [`empty_element_handling`](Self::empty_element_handling) for more control.
///
/// # Examples
///
/// ```
/// # use pretty_assertions::assert_eq;
/// # use serde::Serialize;
/// # use quick_xml::se::Serializer;
/// #
/// #[derive(Debug, PartialEq, Serialize)]
/// struct Struct {
/// question: Option<String>,
Expand All @@ -660,7 +739,7 @@ impl<'w, 'r, W: Write> Serializer<'w, 'r, W> {
/// ser.expand_empty_elements(true);
///
/// let data = Struct {
/// question: None,
/// question: None,
/// };
///
/// data.serialize(ser).unwrap();
Expand All @@ -670,8 +749,11 @@ impl<'w, 'r, W: Write> Serializer<'w, 'r, W> {
/// );
/// ```
pub fn expand_empty_elements(&mut self, expand: bool) -> &mut Self {
self.ser.expand_empty_elements = expand;
self
self.empty_element_handling(if expand {
EmptyElementHandling::Expanded
} else {
EmptyElementHandling::SelfClosed
})
}

/// Set the text format used for serializing text content.
Expand Down
Loading