use std::collections::BTreeMap;
use std::fmt::Write;
#[derive(Debug, Clone)]
pub struct Element {
pub name: String,
pub attrs: BTreeMap<String, String>,
pub children: Vec<Child>,
}
#[derive(Debug, Clone)]
pub enum Child {
Element(Element),
Text(String),
}
impl Element {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
attrs: BTreeMap::new(),
children: Vec::new(),
}
}
pub fn attr(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
self.attrs.insert(k.into(), v.into());
self
}
pub fn text(mut self, t: impl Into<String>) -> Self {
self.children.push(Child::Text(t.into()));
self
}
pub fn child(mut self, c: Element) -> Self {
self.children.push(Child::Element(c));
self
}
pub fn push(&mut self, c: Element) -> &mut Self {
self.children.push(Child::Element(c));
self
}
}
fn escape_text(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
c => out.push(c),
}
}
out
}
fn escape_attr(s: &str) -> String {
let mut out = escape_text(s);
out = out.replace('"', """).replace('\'', "'");
out
}
fn render_at(el: &Element, indent: usize, out: &mut String) {
let pad = " ".repeat(indent);
let _ = write!(out, "{pad}<{}", el.name);
for (k, v) in &el.attrs {
let _ = write!(out, " {k}=\"{}\"", escape_attr(v));
}
if el.children.is_empty() {
out.push_str(" />\n");
return;
}
if el.children.len() == 1 {
if let Some(Child::Text(t)) = el.children.first() {
let _ = write!(out, ">{}</{}>\n", escape_text(t), el.name);
return;
}
}
out.push_str(">\n");
for child in &el.children {
match child {
Child::Element(e) => render_at(e, indent + 1, out),
Child::Text(t) => {
let inner_pad = " ".repeat(indent + 1);
let _ = writeln!(out, "{inner_pad}{}", escape_text(t));
}
}
}
let _ = writeln!(out, "{pad}</{}>", el.name);
}
pub fn render_document(el: &Element) -> String {
let mut out = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
render_at(el, 0, &mut out);
out
}
pub fn render_doc_no_decl(el: &Element, out: &mut String) {
render_at(el, 0, out);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_escapes_lt_gt_amp() {
assert_eq!(escape_text("a < b & c > d"), "a < b & c > d");
}
#[test]
fn attr_escapes_quote() {
assert_eq!(escape_attr(r#"a"b"#), "a"b");
}
#[test]
fn empty_element_self_closes() {
let e = Element::new("dep").attr("name", "x");
let mut out = String::new();
render_at(&e, 0, &mut out);
assert_eq!(out, "<dep name=\"x\" />\n");
}
#[test]
fn single_text_inline() {
let e = Element::new("name").text("widget");
let mut out = String::new();
render_at(&e, 0, &mut out);
assert_eq!(out, "<name>widget</name>\n");
}
}