use std::borrow::Cow;
pub fn escape_attr(s: &str) -> Cow<'_, str> {
let needs_escape = s.chars().any(|c| matches!(c, '&' | '<' | '>' | '"' | '\''));
if !needs_escape {
return Cow::Borrowed(s);
}
let mut out = String::with_capacity(s.len() + 8);
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
other => out.push(other),
}
}
Cow::Owned(out)
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct AttrMap(pub Vec<(Cow<'static, str>, Cow<'static, str>)>);
impl AttrMap {
pub fn new() -> Self {
AttrMap(Vec::new())
}
pub fn push(&mut self, key: impl Into<Cow<'static, str>>, val: impl Into<Cow<'static, str>>) {
self.0.push((key.into(), val.into()));
}
pub fn extend_from<I, K, V>(&mut self, iter: I)
where
I: IntoIterator<Item = (K, V)>,
K: Into<Cow<'static, str>>,
V: Into<Cow<'static, str>>,
{
for (k, v) in iter {
self.0.push((k.into(), v.into()));
}
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.0.iter().map(|(k, v)| (k.as_ref(), v.as_ref()))
}
pub fn render(&self) -> String {
if self.0.is_empty() {
return String::new();
}
let mut out = String::new();
for (k, v) in &self.0 {
out.push(' ');
out.push_str(k);
out.push_str("=\"");
out.push_str(&escape_attr(v));
out.push('"');
}
out
}
}
impl<K, V> FromIterator<(K, V)> for AttrMap
where
K: Into<Cow<'static, str>>,
V: Into<Cow<'static, str>>,
{
fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
let mut map = AttrMap::new();
for (k, v) in iter {
map.0.push((k.into(), v.into()));
}
map
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_renders_empty_string() {
let m = AttrMap::new();
assert_eq!(m.render(), "");
}
#[test]
fn single_attr_renders_with_leading_space() {
let mut m = AttrMap::new();
m.push("id", "foo");
assert_eq!(m.render(), r#" id="foo""#);
}
#[test]
fn escapes_dangerous_chars_in_values() {
let mut m = AttrMap::new();
m.push("data-x", r#"a"b&c<d>e'f"#);
assert_eq!(m.render(), r#" data-x="a"b&c<d>e'f""#);
}
#[test]
fn escape_attr_no_alloc_on_safe_string() {
let s = "hello world";
let escaped = escape_attr(s);
assert!(matches!(escaped, Cow::Borrowed(_)));
}
}