use std::fmt::Write as _;
#[derive(Debug, Clone, PartialEq)]
pub struct XmlNode {
pub name: String,
pub attrs: Vec<(String, String)>,
pub children: Vec<XmlNode>,
pub text: Option<String>,
}
impl XmlNode {
#[must_use]
pub fn element(name: impl Into<String>) -> Self {
Self {
name: name.into(),
attrs: Vec::new(),
children: Vec::new(),
text: None,
}
}
#[must_use]
pub fn text_node(text: impl Into<String>) -> Self {
Self {
name: String::new(),
attrs: Vec::new(),
children: Vec::new(),
text: Some(text.into()),
}
}
#[must_use]
pub fn attr(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.attrs.push((key.into(), value.into()));
self
}
pub fn push_child(&mut self, child: XmlNode) {
self.children.push(child);
}
#[must_use]
pub fn get_attr(&self, key: &str) -> Option<&str> {
self.attrs
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
}
#[must_use]
pub fn child(&self, name: &str) -> Option<&XmlNode> {
self.children.iter().find(|c| c.name == name)
}
pub fn children_named<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a XmlNode> {
self.children.iter().filter(move |c| c.name == name)
}
#[must_use]
pub fn to_xml_string(&self) -> String {
let mut out = String::new();
self.write_self_xml(&mut out);
out
}
}
pub trait XmlElement {
fn element_name(&self) -> &'static str;
fn attributes(&self) -> Vec<(String, String)>;
fn child_nodes(&self) -> Vec<XmlNode> {
Vec::new()
}
fn text_content(&self) -> Option<&str> {
None
}
fn to_xml(&self) -> String {
let mut out = String::new();
self.write_xml(&mut out);
out
}
fn write_xml(&self, out: &mut String) {
let name = self.element_name();
out.push('<');
out.push_str(name);
for (k, v) in self.attributes() {
out.push(' ');
out.push_str(&k);
out.push_str("=\"");
out.push_str(&xml_escape(&v));
out.push('"');
}
let children = self.child_nodes();
let text = self.text_content();
if children.is_empty() && text.is_none_or(|s| s.is_empty()) {
out.push_str("/>");
return;
}
out.push('>');
if let Some(t) = text {
out.push_str(&xml_escape(t));
}
for child in children {
child.write_self_xml(out);
}
out.push_str("</");
out.push_str(name);
out.push('>');
}
fn from_xml(node: &XmlNode) -> Result<Self, XmlElementError>
where
Self: Sized;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XmlElementError(pub String);
impl std::fmt::Display for XmlElementError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for XmlElementError {}
impl XmlNode {
fn write_self_xml(&self, out: &mut String) {
if let Some(text) = &self.text {
out.push_str(&xml_escape(text));
return;
}
let _ = write!(out, "<{}", self.name);
for (k, v) in &self.attrs {
let _ = write!(out, " {k}=\"{}\"", xml_escape(v));
}
let is_empty = self.children.is_empty()
|| (self.children.len() == 1
&& self.children[0].name.is_empty()
&& self.children[0].text.as_deref().is_some_and(str::is_empty));
if is_empty {
out.push_str("/>");
return;
}
out.push('>');
for child in &self.children {
child.write_self_xml(out);
}
let _ = write!(out, "</{}>", self.name);
}
}
#[must_use]
pub fn xml_escape(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(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
struct SampleElement {
id: String,
name: String,
}
impl XmlElement for SampleElement {
fn element_name(&self) -> &'static str {
"Sample"
}
fn attributes(&self) -> Vec<(String, String)> {
vec![("ID".to_string(), self.id.clone())]
}
fn text_content(&self) -> Option<&str> {
Some(&self.name)
}
fn from_xml(node: &XmlNode) -> Result<Self, XmlElementError> {
Ok(Self {
id: node.get_attr("ID").unwrap_or_default().to_string(),
name: node.text.clone().unwrap_or_default(),
})
}
}
#[test]
fn test_to_xml_simple() {
let el = SampleElement {
id: "1".to_string(),
name: "测试 & 数据".to_string(),
};
let xml = el.to_xml();
assert_eq!(xml, r#"<Sample ID="1">测试 & 数据</Sample>"#);
}
#[test]
fn test_node_roundtrip() {
let mut node = XmlNode::element("Page")
.attr("ID", "5")
.attr("BaseLoc", "Pages/Page_0/Content.xml");
node.push_child(XmlNode::element("Layer"));
assert_eq!(node.get_attr("ID"), Some("5"));
assert_eq!(node.child("Layer").unwrap().name, "Layer");
let mut out = String::new();
node.write_self_xml(&mut out);
let xml = out;
assert!(xml.starts_with(r#"<Page ID="5" BaseLoc="Pages/Page_0/Content.xml">"#));
assert!(xml.contains("<Layer/>"));
}
#[test]
fn test_xml_escape() {
assert_eq!(xml_escape("<a&b\"c'>"), "<a&b"c'>");
}
}