use crate::writer::WriterResult;
use quick_xml::events::attributes::Attribute;
use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
use std::borrow::Cow;
use std::io::Write;
pub(crate) struct XmlWriter<'a, W> {
writer: quick_xml::Writer<W>,
start_element: Option<BytesStart<'a>>,
}
impl<'a, W: Write> XmlWriter<'a, W> {
pub(crate) fn new(writer: W) -> Self {
Self {
writer: quick_xml::Writer::new_with_indent(writer, b' ', 2),
start_element: None,
}
}
pub(crate) fn write_utf8_declaration(&mut self) -> WriterResult<&mut Self> {
const XML_VERSION: &str = "1.0";
const XML_ENCODING: &str = "UTF-8";
self.writer.write_event(Event::Decl(BytesDecl::new(
XML_VERSION,
Some(XML_ENCODING),
None,
)))?;
Ok(self)
}
pub(crate) fn start_element(&mut self, tag: &'a str) -> WriterResult<&mut Self> {
self.finish_start_element()?;
self.start_element = Some(BytesStart::new(tag));
Ok(self)
}
pub(crate) fn add_attribute<'b>(
&mut self,
name: &str,
value: impl Into<Option<&'b str>>,
) -> &mut Self {
if let (Some(element), Some(value)) = (&mut self.start_element, value.into()) {
element.push_attribute(new_escaped_attribute(name, value));
}
self
}
pub(crate) fn add_attributes<'b>(
&mut self,
iter: impl IntoIterator<Item = (&'b str, &'b str)>,
) -> &mut Self {
if let Some(element) = &mut self.start_element {
element.extend_attributes(
iter.into_iter()
.map(|(name, value)| new_escaped_attribute(name, value)),
);
}
self
}
pub(crate) fn finish_start_element(&mut self) -> WriterResult<()> {
if let Some(element) = self.start_element.take() {
self.writer.write_event(Event::Start(element))?;
}
Ok(())
}
pub(crate) fn finish_end_element(&mut self, tag: &str) -> WriterResult<()> {
self.finish_start_element()?;
self.writer.write_event(Event::End(BytesEnd::new(tag)))?;
Ok(())
}
pub(crate) fn finish_text_element(&mut self, text: &str) -> WriterResult<()> {
if let Some(element) = self.start_element.take() {
let text = BytesText::from_escaped(escape(text));
self.writer.write_event(Event::Start(element.borrow()))?;
self.writer.write_event(Event::Text(text))?;
self.writer.write_event(Event::End(element.to_end()))?;
}
Ok(())
}
pub(crate) fn finish_empty_element(&mut self) -> WriterResult<()> {
if let Some(element) = self.start_element.take() {
self.writer.write_event(Event::Empty(element))?;
}
Ok(())
}
}
fn new_escaped_attribute<'a>(name: &'a str, value: &'a str) -> Attribute<'a> {
Attribute {
key: quick_xml::name::QName(name.as_bytes()),
value: match escape(value.trim()) {
Cow::Borrowed(borrowed) => Cow::Borrowed(borrowed.as_bytes()),
Cow::Owned(owned) => Cow::Owned(owned.into_bytes()),
},
}
}
fn escape(input: &str) -> Cow<'_, str> {
macro_rules! escape_chars {
{$($char:literal => $entity:literal,)+} => {
const ESCAPE_CHARS: &'static [char] = &[$($char),+];
fn get_entity(c: char) -> &'static str {
match c {
$($char => $entity,)+
_ => unreachable!("only characters in `ESCAPE_CHARS` are matched"),
}
}
};
}
escape_chars! {
'<' => "<",
'>' => ">",
'"' => """,
'&' => "&",
'\'' => "'",
'\t' => "	",
'\n' => " ",
'\r' => " ",
'\u{00A0}' => " ", }
let mut escaped = None;
let mut last_pos = 0;
for (i, matched) in input.match_indices(ESCAPE_CHARS) {
let out = escaped.get_or_insert_with(|| String::with_capacity(input.len() + 16));
let c = matched
.chars()
.next()
.expect("Should not be an empty string");
out.push_str(&input[last_pos..i]);
out.push_str(get_entity(c));
last_pos = i + matched.len();
}
match escaped {
None => Cow::Borrowed(input),
Some(mut s) => {
s.push_str(&input[last_pos..]);
Cow::Owned(s)
}
}
}
macro_rules! write_element {
(writer: $w:expr, tag: $t:expr, $(attributes: $attrs:tt)?) => {
$crate::writer::xml::write_element!(@helper $w, $t, $($attrs)?)
.finish_empty_element()
};
(writer: $w:expr, tag: $t:expr, text: $text:expr, $(attributes: $attrs:tt)?) => {
$crate::writer::xml::write_element!(@helper $w, $t, $($attrs)?)
.finish_text_element($text)
};
(writer: $w:expr, tag: $t:expr, $(attributes: $attrs:tt)? inner_content: $inner:block) => {{
let tag = $t;
$crate::writer::xml::write_element!(@helper $w, tag, $($attrs)?);
$w.finish_start_element()?;
$inner
$w.finish_end_element(tag)
}};
(@helper $w:expr, $t:expr, { $($name:path $(where $cond:expr)? => $val:expr,)* ..$iter:expr, }) => {
write_element!(@helper $w, $t, { $($name $(where $cond)? => $val,)* })
.add_attributes($iter.filter(|(name, _)| match *name {
$($name)|* => false,
_ => true,
}))
};
(@helper $w:expr, $t:expr, { $($name:path $(where $cond:expr)? => $val:expr,)* }) => {{
let mut element = $w.start_element($t)?;
$(
$(if $cond)? {
element = element.add_attribute($name, $val);
}
)*
element
}};
(@helper $w:expr, $t:expr,) => {
$w.start_element($t)?
};
}
pub(crate) use write_element;
#[cfg(test)]
mod tests {
#[test]
fn test_escape() {
#[rustfmt::skip]
let expected = [
("<>'"& 	 ", "<>'\"&\r\n\t\u{00A0}"),
("abc xyz", "abc xyz"),
("1 < 2 & 3", "1 < 2 & 3"),
("3 > 1 & 2", "3 > 1 & 2"),
(""'quoted'"", "\"'quoted'\""),
("line1 line2 line3", "line1\nline2\r\nline3"),
("		With non breaking space", "\t\tWith\u{00A0}non breaking\u{00A0}space"),
("esc<aped&attr>ibute"value'", "esc<aped&attr>ibute\"value'"),
];
for (expected_escaped, original) in expected {
assert_eq!(expected_escaped, super::escape(original));
}
}
}