use std::fmt::{Display as _, Write as _};
#[derive(Default, Debug, PartialEq, Eq, Hash)]
pub struct Attribute {
prefix: Option<String>,
name: String,
value: String,
}
impl Attribute {
pub fn new(prefix: Option<&str>, name: &str, value: &str) -> Attribute {
Self {
prefix: prefix.map(String::from),
name: name.to_owned(),
value: value.to_owned(),
}
}
#[inline(always)]
pub fn name(&self) -> &str {
&self.name
}
#[inline(always)]
pub fn value(&self) -> &str {
&self.value
}
}
impl std::fmt::Display for Attribute {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(prefix) = &self.prefix {
f.write_str(prefix)?;
f.write_char(':')?;
}
f.write_str(&self.name)?;
f.write_str("=\"")?;
write_attr_value(f, &self.value)?; f.write_str("\"")
}
}
#[inline]
fn is_valid_xml_char(ch: char) -> bool {
matches!(
ch,
'\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}'
)
}
#[inline]
fn write_attr_value(f: &mut std::fmt::Formatter<'_>, value: &str) -> std::fmt::Result {
let bytes = value.as_bytes();
let mut run_start = 0;
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
let ascii_safe =
(0x20..0x80).contains(&b) && !matches!(b, b'&' | b'<' | b'>' | b'"' | b'\'');
if ascii_safe {
i += 1;
continue;
}
if run_start < i {
f.write_str(&value[run_start..i])?;
}
let ch = value[i..].chars().next().unwrap();
match ch {
'\t' | '\n' | '\r' => f.write_char(' ')?,
'&' => f.write_str("&")?,
'<' => f.write_str("<")?,
'>' => f.write_str(">")?,
'"' => f.write_str(""")?,
'\'' => f.write_str("'")?,
c if is_valid_xml_char(c) => f.write_char(c)?, _ => {} }
i += ch.len_utf8();
run_start = i;
}
if run_start < i {
f.write_str(&value[run_start..i])?;
}
Ok(())
}
#[inline]
fn write_text(f: &mut std::fmt::Formatter<'_>, value: &str) -> std::fmt::Result {
for ch in value.chars() {
match ch {
'&' => f.write_str("&")?,
'<' => f.write_str("<")?,
'>' => f.write_str(">")?,
'\t' | '\n' | '\r' => f.write_char(ch)?, c if is_valid_xml_char(c) => f.write_char(c)?,
_ => {} }
}
Ok(())
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Element {
name: String,
attributes: Vec<Attribute>,
text: Option<String>,
childrens: Vec<Element>,
}
impl Element {
pub fn new(name: &str) -> Element {
Element {
name: name.to_owned(),
..Default::default()
}
}
pub fn with_capacity(name: &str, capacity: usize) -> Element {
Element {
name: name.to_owned(),
attributes: Vec::with_capacity(capacity),
..Default::default()
}
}
pub fn set_attribute(&mut self, name: &str, value: &str) {
if self.attributes.iter().any(|a| a.name() == name) {
return;
}
self.attributes.push(Attribute::new(None, name, value));
}
pub fn set_attribute_with_prefix(&mut self, prefix: Option<&str>, name: &str, value: &str) {
if self
.attributes
.iter()
.any(|a| a.name() == name && a.prefix.as_deref() == prefix)
{
return;
}
self.attributes.push(Attribute::new(prefix, name, value));
}
pub fn set_text(&mut self, text: &str) {
if self.childrens.is_empty() {
self.text = Some(text.to_owned());
}
}
#[inline]
pub fn append_child(&mut self, child: Element) {
self.text = None;
self.childrens.push(child);
}
#[inline]
pub fn childrens(&self) -> impl Iterator<Item = &Element> {
self.childrens.iter()
}
#[inline]
pub fn descendants(&self) -> Descendants<'_> {
Descendants::new(self)
}
#[inline]
pub fn attributes(&self) -> impl Iterator<Item = &Attribute> {
self.attributes.iter()
}
#[inline]
pub fn name(&self) -> &str {
&self.name
}
#[inline]
pub fn attr(&self, name: &str) -> Option<&str> {
self.attributes
.iter()
.find(|x| x.name() == name)
.map(|x| x.value())
}
pub(crate) fn fmt_with_indent(
&self,
f: &mut std::fmt::Formatter<'_>,
indent: usize,
) -> std::fmt::Result {
write_indent(f, indent)?;
f.write_char('<')?;
f.write_str(&self.name)?;
match self.attributes.len() {
0 => {}
1 => {
f.write_char(' ')?;
self.attributes[0].fmt(f)?;
}
n => {
let child_indent = indent + 1;
f.write_char('\n')?;
write_indent(f, child_indent)?;
for (idx, attr) in self.attributes.iter().enumerate() {
attr.fmt(f)?;
if idx + 1 != n {
f.write_char('\n')?;
write_indent(f, child_indent)?;
}
}
}
}
if self.childrens.is_empty() {
match self.text.as_deref() {
Some(text) => {
f.write_char('>')?;
write_text(f, text)?;
f.write_str("</")?;
f.write_str(&self.name)?;
f.write_str(">")?;
}
None => {
f.write_str("/>")?;
}
}
f.write_char('\n')?;
} else {
f.write_char('>')?;
f.write_char('\n')?;
for child in &self.childrens {
child.fmt_with_indent(f, indent + 1)?;
}
write_indent(f, indent)?;
f.write_str("</")?;
f.write_str(&self.name)?;
f.write_str(">\n")?;
}
Ok(())
}
}
#[inline]
fn write_indent(f: &mut std::fmt::Formatter<'_>, indent: usize) -> std::fmt::Result {
for _ in 0..indent {
f.write_str(" ")?;
}
Ok(())
}
impl std::fmt::Display for Element {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "<?xml version=\"1.0\" encoding=\"utf-8\"?>")?;
self.fmt_with_indent(f, 0)
}
}
pub struct Descendants<'a> {
stack: Vec<std::slice::Iter<'a, Element>>,
}
impl<'a> Descendants<'a> {
fn new(root: &'a Element) -> Descendants<'a> {
Descendants {
stack: vec![root.childrens.iter()],
}
}
}
impl<'a> Iterator for Descendants<'a> {
type Item = &'a Element;
fn next(&mut self) -> Option<Self::Item> {
while let Some(top_iter) = self.stack.last_mut() {
if let Some(next_elem) = top_iter.next() {
if !next_elem.childrens.is_empty() {
self.stack.push(next_elem.childrens.iter());
}
return Some(next_elem);
} else {
self.stack.pop();
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn attribute_plain_renders_quoted() {
assert_eq!(Attribute::new(None, "id", "123").to_string(), "id=\"123\"");
}
#[test]
fn attribute_prefixed_renders() {
assert_eq!(
Attribute::new(Some("android"), "name", "x").to_string(),
"android:name=\"x\""
);
}
#[test]
fn attribute_escapes_specials() {
assert_eq!(
Attribute::new(None, "a", "&\n<>\"'").to_string(),
"a=\"& <>"'\""
);
}
#[test]
fn attribute_tabs_cr_to_space() {
assert_eq!(
Attribute::new(None, "a", "x\ty\r").to_string(),
"a=\"x y \""
);
}
#[test]
fn attribute_all_invalid_renders_empty() {
assert_eq!(
Attribute::new(None, "a", "\u{1}\u{FFFF}\u{FFFE}").to_string(),
"a=\"\""
);
}
#[test]
fn attribute_non_ascii_preserved() {
assert_eq!(
Attribute::new(None, "a", "café 😀").to_string(),
"a=\"café 😀\""
);
}
#[test]
fn element_render_golden() {
let mut root = Element::new("root");
root.set_attribute("a", "1");
root.set_attribute("b", "2");
let mut child = Element::new("child");
child.set_attribute("x", "y");
root.append_child(child);
let mut ns_kid = Element::new("ns:kid");
ns_kid.set_attribute("prefix", "p");
root.append_child(ns_kid);
let expected = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\
<root\n a=\"1\"\n b=\"2\">\n <child x=\"y\"/>\n <ns:kid prefix=\"p\"/>\n</root>\n";
assert_eq!(root.to_string(), expected);
}
}