1use std::borrow::Cow;
2
3pub fn escape_attr(s: &str) -> Cow<'_, str> {
6 let needs_escape = s.chars().any(|c| matches!(c, '&' | '<' | '>' | '"' | '\''));
8 if !needs_escape {
9 return Cow::Borrowed(s);
10 }
11 let mut out = String::with_capacity(s.len() + 8);
12 for c in s.chars() {
13 match c {
14 '&' => out.push_str("&"),
15 '<' => out.push_str("<"),
16 '>' => out.push_str(">"),
17 '"' => out.push_str("""),
18 '\'' => out.push_str("'"),
19 other => out.push(other),
20 }
21 }
22 Cow::Owned(out)
23}
24
25#[derive(Clone, Debug, PartialEq, Eq, Default)]
33pub struct AttrMap(pub Vec<(Cow<'static, str>, Cow<'static, str>)>);
34
35impl AttrMap {
36 pub fn new() -> Self {
38 AttrMap(Vec::new())
39 }
40
41 pub fn push(&mut self, key: impl Into<Cow<'static, str>>, val: impl Into<Cow<'static, str>>) {
43 self.0.push((key.into(), val.into()));
44 }
45
46 pub fn extend_from<I, K, V>(&mut self, iter: I)
48 where
49 I: IntoIterator<Item = (K, V)>,
50 K: Into<Cow<'static, str>>,
51 V: Into<Cow<'static, str>>,
52 {
53 for (k, v) in iter {
54 self.0.push((k.into(), v.into()));
55 }
56 }
57
58 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
60 self.0.iter().map(|(k, v)| (k.as_ref(), v.as_ref()))
61 }
62
63 pub fn render(&self) -> String {
68 if self.0.is_empty() {
69 return String::new();
70 }
71 let mut out = String::new();
72 for (k, v) in &self.0 {
73 out.push(' ');
74 out.push_str(k);
75 out.push_str("=\"");
76 out.push_str(&escape_attr(v));
77 out.push('"');
78 }
79 out
80 }
81}
82
83impl<K, V> FromIterator<(K, V)> for AttrMap
84where
85 K: Into<Cow<'static, str>>,
86 V: Into<Cow<'static, str>>,
87{
88 fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
89 let mut map = AttrMap::new();
90 for (k, v) in iter {
91 map.0.push((k.into(), v.into()));
92 }
93 map
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn empty_renders_empty_string() {
103 let m = AttrMap::new();
104 assert_eq!(m.render(), "");
105 }
106
107 #[test]
108 fn single_attr_renders_with_leading_space() {
109 let mut m = AttrMap::new();
110 m.push("id", "foo");
111 assert_eq!(m.render(), r#" id="foo""#);
112 }
113
114 #[test]
115 fn escapes_dangerous_chars_in_values() {
116 let mut m = AttrMap::new();
117 m.push("data-x", r#"a"b&c<d>e'f"#);
118 assert_eq!(m.render(), r#" data-x="a"b&c<d>e'f""#);
119 }
120
121 #[test]
122 fn escape_attr_no_alloc_on_safe_string() {
123 let s = "hello world";
124 let escaped = escape_attr(s);
125 assert!(matches!(escaped, Cow::Borrowed(_)));
126 }
127}