use std::fmt;
use std::io::{self, Write};
pub type Result = io::Result<()>;
pub struct XmlWriter<'a, W: Write> {
stack: Vec<(&'a str, bool)>,
ns_stack: Vec<Option<&'a str>>,
writer: Box<W>,
namespace: Option<&'a str>,
very_pretty: bool,
opened: bool,
newline: bool,
children: bool,
text_content: bool,
}
impl<'a, W: Write> fmt::Debug for XmlWriter<'a, W> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Ok(write!(
f,
"XmlWriter {{ stack: {:?}, opened: {} }}",
self.stack, self.opened
)?)
}
}
impl<'a, W: Write> XmlWriter<'a, W> {
pub fn compact_mode(writer: W) -> XmlWriter<'a, W> {
XmlWriter {
stack: Vec::new(),
ns_stack: Vec::new(),
writer: Box::new(writer),
namespace: None,
very_pretty: false,
opened: false,
newline: false,
children: false,
text_content: false,
}
}
pub fn pretty_mode(writer: W) -> XmlWriter<'a, W> {
XmlWriter {
stack: Vec::new(),
ns_stack: Vec::new(),
writer: Box::new(writer),
namespace: None,
very_pretty: true,
opened: false,
newline: false,
children: false,
text_content: false,
}
}
pub fn set_compact_mode(&mut self) {
self.very_pretty = false;
}
pub fn set_pretty_mode(&mut self) {
self.very_pretty = true;
}
pub fn namespace(&self) -> Option<&'a str> {
self.namespace
}
pub fn set_namespace(&mut self, namespace: &'a str) {
self.namespace = Some(namespace);
}
pub fn unset_namespace(&mut self) {
self.namespace = None;
}
pub fn dtd(&mut self, encoding: &str) -> Result {
self.write("<?xml version=\"1.0\" encoding=\"")?;
self.write(encoding)?;
self.write("\" ?>\n")
}
fn indent(&mut self) -> Result {
let indent = self.stack.len();
if self.very_pretty {
if self.newline {
self.write("\n")?;
} else {
self.newline = true;
}
for _ in 0..indent {
self.write(" ")?;
}
}
Ok(())
}
fn ns_prefix(&mut self, namespace: Option<&'a str>) -> Result {
if let Some(ns) = namespace {
self.write(ns)?;
self.write(":")?;
}
Ok(())
}
pub fn ns_decl(&mut self, ns_map: &Vec<(Option<&'a str>, &'a str)>) -> Result {
if !self.opened {
panic!(
"Attempted to write namespace decl to elem, when no elem was opened, stack {:?}",
self.stack
);
}
for item in ns_map {
let name = match item.0 {
Some(pre) => "xmlns:".to_string() + pre,
None => "xmlns".to_string(),
};
self.attr(&name, item.1)?;
}
Ok(())
}
pub fn elem(&mut self, name: &str) -> Result {
self.close_elem()?;
self.indent()?;
self.write("<")?;
let ns = self.namespace;
self.ns_prefix(ns)?;
self.write(name)?;
self.write("/>")
}
pub fn elem_text(&mut self, name: &str, text: &str) -> Result {
self.close_elem()?;
self.indent()?;
self.write("<")?;
let ns = self.namespace;
self.ns_prefix(ns)?;
self.write(name)?;
self.write(">")?;
self.escape(text, false)?;
self.write("</")?;
self.write(name)?;
self.write(">")
}
pub fn begin_elem(&mut self, name: &'a str) -> Result {
self.children = true;
self.close_elem()?;
if let Some(mut previous) = self.stack.pop() {
previous.1 = true;
self.stack.push(previous);
}
self.indent()?;
self.stack.push((name, false));
self.ns_stack.push(self.namespace);
self.write("<")?;
self.opened = true;
self.children = false;
let ns = self.namespace;
self.ns_prefix(ns)?;
self.write(name)
}
fn close_elem(&mut self) -> Result {
if self.opened {
if self.very_pretty && !self.children {
self.write("/>")?;
} else {
self.write(">")?;
}
self.opened = false;
}
Ok(())
}
pub fn end_elem(&mut self) -> Result {
self.close_elem()?;
let ns = self.ns_stack.pop().unwrap_or_else(
|| panic!("Attempted to close namespaced element without corresponding open namespace, stack {:?}", self.ns_stack)
);
match self.stack.pop() {
Some((name, children)) => {
if self.very_pretty {
if !children {
return Ok(());
}
if !self.text_content {
self.indent()?;
}
self.text_content = false;
}
self.write("</")?;
self.ns_prefix(ns)?;
self.write(name)?;
self.write(">")?;
Ok(())
}
None => panic!(
"Attempted to close an elem, when none was open, stack {:?}",
self.stack
),
}
}
pub fn empty_elem(&mut self, name: &'a str) -> Result {
self.children = true;
self.close_elem()?;
if let Some(mut previous) = self.stack.pop() {
previous.1 = true;
self.stack.push(previous);
}
self.children = false;
self.indent()?;
self.write("<")?;
let ns = self.namespace;
self.ns_prefix(ns)?;
self.write(name)?;
self.write("/>")
}
pub fn attr(&mut self, name: &str, value: &str) -> Result {
if !self.opened {
panic!(
"Attempted to write attr to elem, when no elem was opened, stack {:?}",
self.stack
);
}
self.write(" ")?;
self.write(name)?;
self.write("=\"")?;
self.write(value)?;
self.write("\"")
}
pub fn attr_esc(&mut self, name: &str, value: &str) -> Result {
if !self.opened {
panic!(
"Attempted to write attr to elem, when no elem was opened, stack {:?}",
self.stack
);
}
self.write(" ")?;
self.escape(name, true)?;
self.write("=\"")?;
self.escape(value, false)?;
self.write("\"")
}
fn escape(&mut self, text: &str, ident: bool) -> Result {
for c in text.chars() {
match c {
'"' => self.write(""")?,
'\'' => self.write("'")?,
'&' => self.write("&")?,
'<' => self.write("<")?,
'>' => self.write(">")?,
'\\' if ident => self.write("\\\\")?,
_ => self.write_slice(c.encode_utf8(&mut [0; 4]).as_bytes())?,
};
}
Ok(())
}
pub fn text(&mut self, text: &str) -> Result {
self.children = true;
self.close_elem()?;
if let Some(mut previous) = self.stack.pop() {
previous.1 = true;
self.stack.push(previous);
}
self.children = false;
self.text_content = true;
self.escape(text, false)
}
pub fn write(&mut self, text: &str) -> Result {
self.writer.write_all(text.as_bytes())?;
Ok(())
}
fn write_slice(&mut self, slice: &[u8]) -> Result {
self.writer.write_all(slice)?;
Ok(())
}
pub fn cdata(&mut self, cdata: &str) -> Result {
self.children = true;
self.close_elem()?;
if let Some(mut previous) = self.stack.pop() {
previous.1 = true;
self.stack.push(previous);
}
if self.very_pretty {
self.indent()?;
}
self.children = false;
self.write("<![CDATA[")?;
self.write(cdata)?;
self.write("]]>")
}
pub fn comment(&mut self, comment: &str) -> Result {
self.children = true;
self.close_elem()?;
if let Some(mut previous) = self.stack.pop() {
previous.1 = true;
self.stack.push(previous);
}
self.indent()?;
self.children = false;
self.write("<!-- ")?;
self.escape(comment, false)?;
self.write(" -->")
}
pub fn close(&mut self) -> Result {
for _ in 0..self.stack.len() {
self.end_elem()?;
}
Ok(())
}
pub fn flush(&mut self) -> Result {
self.writer.flush()
}
pub fn into_inner(self) -> W {
*self.writer
}
}
#[allow(unused_must_use)]
#[cfg(test)]
mod tests {
use super::XmlWriter;
use std::str;
fn create_xml(
writer: &mut XmlWriter<'_, Vec<u8>>,
nsmap: &Vec<(Option<&'static str>, &'static str)>,
) {
writer.begin_elem("OTDS");
writer.ns_decl(nsmap);
writer.comment("nice to see you");
writer.namespace = Some("st");
writer.empty_elem("success");
writer.begin_elem("node");
writer.attr_esc("name", "\"123\"");
writer.attr("id", "abc");
writer.attr("'unescaped'", "\"123\""); writer.text("'text'");
writer.end_elem();
writer.namespace = None;
writer.begin_elem("stuff");
writer.cdata("blablab");
writer.close();
writer.flush();
}
#[test]
fn compact() {
let nsmap = vec![
(None, "http://localhost/"),
(Some("st"), "http://127.0.0.1/"),
];
let mut writer = XmlWriter::compact_mode(Vec::new());
create_xml(&mut writer, &nsmap);
let actual = writer.into_inner();
println!("{}", str::from_utf8(&actual).unwrap());
assert_eq!(
str::from_utf8(&actual).unwrap(),
"<OTDS xmlns=\"http://localhost/\" xmlns:st=\"http://127.0.0.1/\"><!-- nice to see you --><st:success/><st:node name=\""123"\" id=\"abc\" \'unescaped\'=\"\"123\"\">'text'</st:node><stuff><![CDATA[blablab]]></stuff></OTDS>"
);
}
#[test]
fn pretty() {
let nsmap = vec![
(None, "http://localhost/"),
(Some("st"), "http://127.0.0.1/"),
];
let mut writer = XmlWriter::pretty_mode(Vec::new());
create_xml(&mut writer, &nsmap);
let actual = writer.into_inner();
println!("{}", str::from_utf8(&actual).unwrap());
assert_eq!(
str::from_utf8(&actual).unwrap(),
"<OTDS xmlns=\"http://localhost/\" xmlns:st=\"http://127.0.0.1/\">\n <!-- nice to see you -->\n <st:success/>\n <st:node name=\""123"\" id=\"abc\" \'unescaped\'=\"\"123\"\">'text'</st:node>\n <stuff>\n <![CDATA[blablab]]>\n </stuff>\n</OTDS>"
);
}
#[test]
fn comment() {
let mut xml = XmlWriter::pretty_mode(Vec::new());
xml.comment("comment");
let actual = xml.into_inner();
assert_eq!(str::from_utf8(&actual).unwrap(), "<!-- comment -->");
let mut xml = XmlWriter::compact_mode(Vec::new());
xml.comment("comment");
let actual = xml.into_inner();
assert_eq!(str::from_utf8(&actual).unwrap(), "<!-- comment -->");
}
}