#[derive(Debug, Clone, PartialEq, Eq)]
pub enum XmlNode {
Element(XmlElement),
Text(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XmlElement {
pub name: String,
pub attributes: Vec<(String, String)>,
pub children: Vec<XmlNode>,
}
impl XmlElement {
pub fn group(name: impl Into<String>, children: Vec<XmlNode>) -> Self {
XmlElement { name: name.into(), attributes: Vec::new(), children }
}
pub fn leaf(name: impl Into<String>, text: impl Into<String>) -> Self {
XmlElement { name: name.into(), attributes: Vec::new(), children: vec![XmlNode::Text(text.into())] }
}
pub fn empty(name: impl Into<String>) -> Self {
XmlElement { name: name.into(), attributes: Vec::new(), children: Vec::new() }
}
pub fn with_attr(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.attributes.push((name.into(), value.into()));
self
}
}
#[derive(Debug, Clone)]
pub struct GenerateOptions {
pub xml_declaration: bool,
pub indent: Option<usize>,
}
impl Default for GenerateOptions {
fn default() -> Self {
GenerateOptions { xml_declaration: false, indent: None }
}
}
pub fn escape_text(s: &str, out: &mut String) {
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
_ => out.push(c),
}
}
}
pub fn escape_attr(s: &str, out: &mut String) {
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\t' => out.push_str("	"),
'\n' => out.push_str(" "),
'\r' => out.push_str(" "),
_ => out.push(c),
}
}
}
fn write_element(el: &XmlElement, opts: &GenerateOptions, depth: usize, out: &mut String) {
let pretty = opts.indent.is_some();
let pad = |n: usize, out: &mut String| {
if let Some(w) = opts.indent {
out.push_str(&" ".repeat(w * n));
}
};
pad(depth, out);
out.push('<');
out.push_str(&el.name);
for (k, v) in &el.attributes {
out.push(' ');
out.push_str(k);
out.push_str("=\"");
escape_attr(v, out);
out.push('"');
}
if el.children.is_empty() {
out.push_str("/>");
if pretty {
out.push('\n');
}
return;
}
if el.children.len() == 1 {
if let XmlNode::Text(t) = &el.children[0] {
out.push('>');
escape_text(t, out);
out.push_str("</");
out.push_str(&el.name);
out.push('>');
if pretty {
out.push('\n');
}
return;
}
}
out.push('>');
if pretty {
out.push('\n');
}
for child in &el.children {
match child {
XmlNode::Element(c) => write_element(c, opts, depth + 1, out),
XmlNode::Text(t) => {
pad(depth + 1, out);
escape_text(t, out);
if pretty {
out.push('\n');
}
}
}
}
pad(depth, out);
out.push_str("</");
out.push_str(&el.name);
out.push('>');
if pretty {
out.push('\n');
}
}
pub fn generate(root: &XmlElement, opts: &GenerateOptions) -> String {
let mut out = String::new();
if opts.xml_declaration {
out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
out.push('\n');
}
write_element(root, opts, 0, &mut out);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escape_text_and_attr() {
let mut t = String::new();
escape_text("a<b&c>d", &mut t);
assert_eq!(t, "a<b&c>d");
let mut a = String::new();
escape_attr("x\"y&z\tw", &mut a);
assert_eq!(a, "x"y&z	w");
}
#[test]
fn generate_compact_deterministic() {
let tree = XmlElement::group(
"G",
vec![
XmlNode::Element(XmlElement::leaf("NEG", "-42")),
XmlNode::Element(XmlElement::leaf("SPC", "a<b&c")),
XmlNode::Element(
XmlElement::group("GRP", vec![XmlNode::Element(XmlElement::leaf("X", "hi"))])
.with_attr("id", "1\""),
),
XmlNode::Element(XmlElement::empty("EMPTY")),
],
);
let out = generate(&tree, &GenerateOptions::default());
assert_eq!(
out,
"<G><NEG>-42</NEG><SPC>a<b&c</SPC><GRP id=\"1"\"><X>hi</X></GRP><EMPTY/></G>"
);
assert_eq!(out, generate(&tree, &GenerateOptions::default())); }
#[test]
fn generate_pretty_and_declaration() {
let tree = XmlElement::group("R", vec![XmlNode::Element(XmlElement::leaf("A", "1"))]);
let out = generate(&tree, &GenerateOptions { xml_declaration: true, indent: Some(2) });
assert_eq!(out, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<R>\n <A>1</A>\n</R>\n");
}
}