topcoat_view/html/attribute/
attributes.rs1use std::collections::HashMap;
2
3use topcoat_core::context::Cx;
4
5use crate::{
6 Attribute, AttributeCollector, AttributeKey, AttributeKeyViewParts, AttributeValue,
7 AttributeValueViewParts, AttributeViewParts, HtmlContext, PartsWriter,
8};
9
10#[derive(Debug, Default, Clone)]
20pub struct Attributes {
21 map: HashMap<AttributeKey, AttributeValue>,
22}
23
24impl Attributes {
25 #[inline]
32 #[must_use]
33 pub fn new() -> Self {
34 Attributes::default()
35 }
36
37 #[inline]
40 #[must_use]
41 pub fn with_capacity(capacity: usize) -> Self {
42 Self {
43 map: HashMap::with_capacity(capacity),
44 }
45 }
46
47 #[inline]
49 pub fn contains_key(&self, k: impl AsRef<str>) -> bool {
50 self.map.contains_key(k.as_ref())
51 }
52
53 #[inline]
55 pub fn get(&self, k: impl AsRef<str>) -> Option<&AttributeValue> {
56 self.map.get(k.as_ref())
57 }
58
59 #[inline]
66 pub fn insert(
67 &mut self,
68 cx: &Cx,
69 k: impl AttributeKeyViewParts,
70 v: impl AttributeValueViewParts,
71 ) -> Option<AttributeValue> {
72 let mut collector = AttributeCollector::new();
73 k.into_view_parts(
74 cx,
75 &mut PartsWriter::collecting(&mut collector, cx, HtmlContext::AttributeKey),
76 );
77 let key = collector.finish(cx);
78 let value = if v.attribute_present() {
79 let mut collector = AttributeCollector::new();
80 v.into_view_parts(
81 cx,
82 &mut PartsWriter::collecting(&mut collector, cx, HtmlContext::AttributeValue),
83 );
84 collector.finish(cx)
85 } else {
86 AttributeValue::Absent
87 };
88 self.map.insert(key, value)
89 }
90
91 #[inline]
94 pub fn remove(&mut self, k: impl AsRef<str>) -> Option<AttributeValue> {
95 self.map.remove(k.as_ref())
96 }
97
98 #[inline]
100 pub fn clear(&mut self) {
101 self.map.clear();
102 }
103
104 #[inline]
107 pub fn extend(&mut self, iter: impl IntoIterator<Item = (AttributeKey, AttributeValue)>) {
108 self.map.extend(iter);
109 }
110
111 #[inline]
113 pub fn iter(&self) -> <&Self as IntoIterator>::IntoIter {
114 self.into_iter()
115 }
116}
117
118impl AttributeViewParts for Attributes {
119 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
120 for (key, value) in self {
121 Attribute::new(key, value).into_view_parts(cx, parts);
122 }
123 }
124}
125
126impl IntoIterator for Attributes {
127 type Item = (AttributeKey, AttributeValue);
128 type IntoIter = std::collections::hash_map::IntoIter<AttributeKey, AttributeValue>;
129
130 #[inline]
131 fn into_iter(self) -> Self::IntoIter {
132 self.map.into_iter()
133 }
134}
135
136impl<'a> IntoIterator for &'a Attributes {
137 type Item = (&'a AttributeKey, &'a AttributeValue);
138 type IntoIter = std::collections::hash_map::Iter<'a, AttributeKey, AttributeValue>;
139
140 #[inline]
141 fn into_iter(self) -> Self::IntoIter {
142 self.map.iter()
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use std::collections::HashSet;
149
150 use topcoat_core::context::Cx;
151
152 use super::*;
153 use crate::internal::Builder;
154
155 fn render(cx: &Cx, attrs: Attributes) -> String {
156 Builder::build(cx, |b| b.attributes(attrs)).render(cx)
157 }
158
159 #[test]
160 fn new_is_empty() {
161 let attrs = Attributes::new();
162 assert!(!attrs.contains_key("class"));
163 assert_eq!(attrs.iter().count(), 0);
164 }
165
166 #[test]
167 fn with_capacity_is_empty() {
168 let attrs = Attributes::with_capacity(4);
169 assert_eq!(attrs.iter().count(), 0);
170 }
171
172 #[test]
173 fn insert_then_contains_key() {
174 let cx = &Cx::default();
175 let mut attrs = Attributes::new();
176 attrs.insert(cx, "class", "button");
177 assert!(attrs.contains_key("class"));
178 assert!(!attrs.contains_key("id"));
179 }
180
181 #[test]
182 fn insert_returns_none_for_new_key() {
183 let cx = &Cx::default();
184 let mut attrs = Attributes::new();
185 assert!(attrs.insert(cx, "class", "button").is_none());
186 }
187
188 #[test]
189 fn insert_replaces_existing_value() {
190 let cx = &Cx::default();
191 let mut attrs = Attributes::new();
192 attrs.insert(cx, "class", "button");
193 let previous = attrs.insert(cx, "class", "link");
194 assert!(previous.is_some());
195 assert_eq!(render(cx, attrs), " class=\"link\"");
196 }
197
198 #[test]
199 fn get_returns_inserted_value() {
200 let cx = &Cx::default();
201 let mut attrs = Attributes::new();
202 attrs.insert(cx, "class", "button");
203 assert!(attrs.get("class").is_some());
204 assert!(attrs.get("missing").is_none());
205 }
206
207 #[test]
208 fn remove_returns_value_and_deletes_entry() {
209 let cx = &Cx::default();
210 let mut attrs = Attributes::new();
211 attrs.insert(cx, "class", "button");
212 assert!(attrs.remove("class").is_some());
213 assert!(!attrs.contains_key("class"));
214 assert!(attrs.remove("class").is_none());
215 }
216
217 #[test]
218 fn clear_removes_all_entries() {
219 let cx = &Cx::default();
220 let mut attrs = Attributes::new();
221 attrs.insert(cx, "class", "button");
222 attrs.insert(cx, "id", "submit");
223 attrs.clear();
224 assert_eq!(attrs.iter().count(), 0);
225 assert!(!attrs.contains_key("class"));
226 }
227
228 #[test]
229 fn renders_single_attribute() {
230 let cx = &Cx::default();
231 let mut attrs = Attributes::new();
232 attrs.insert(cx, "class", "button");
233 assert_eq!(render(cx, attrs), " class=\"button\"");
234 }
235
236 #[test]
237 fn renders_multiple_attributes() {
238 let cx = &Cx::default();
239 let mut attrs = Attributes::new();
240 attrs.insert(cx, "class", "button");
241 attrs.insert(cx, "id", "submit");
242 let rendered = render(cx, attrs);
243 let parts: HashSet<&str> = rendered
244 .split_terminator(' ')
245 .filter(|s| !s.is_empty())
246 .collect();
247 let expected: HashSet<&str> = ["class=\"button\"", "id=\"submit\""].into_iter().collect();
248 assert_eq!(parts, expected);
249 }
250
251 #[test]
252 fn escapes_attribute_value() {
253 let cx = &Cx::default();
254 let mut attrs = Attributes::new();
255 attrs.insert(cx, "data-x", "a\"b<c");
256 assert_eq!(render(cx, attrs), " data-x=\"a"b<c\"");
257 }
258
259 #[test]
260 fn omits_false_boolean_attribute() {
261 let cx = &Cx::default();
262 let mut attrs = Attributes::new();
263 attrs.insert(cx, "disabled", false);
264 assert_eq!(render(cx, attrs), "");
265 }
266
267 #[test]
268 fn renders_true_boolean_attribute() {
269 let cx = &Cx::default();
270 let mut attrs = Attributes::new();
271 attrs.insert(cx, "disabled", true);
272 assert_eq!(render(cx, attrs), " disabled=\"\"");
273 }
274
275 #[test]
276 fn omits_none_option_attribute() {
277 let cx = &Cx::default();
278 let mut attrs = Attributes::new();
279 attrs.insert(cx, "title", Option::<&str>::None);
280 assert!(!attrs.get("title").unwrap().is_present());
281 assert_eq!(render(cx, attrs), "");
282 }
283
284 #[test]
285 fn renders_some_option_attribute() {
286 let cx = &Cx::default();
287 let mut attrs = Attributes::new();
288 attrs.insert(cx, "title", Some("hello"));
289 assert_eq!(render(cx, attrs), " title=\"hello\"");
290 }
291
292 #[test]
293 fn iter_yields_inserted_entries() {
294 let cx = &Cx::default();
295 let mut attrs = Attributes::new();
296 attrs.insert(cx, "class", "button");
297 attrs.insert(cx, "id", "submit");
298 let keys: HashSet<&str> = attrs.iter().map(|(k, _)| k.as_str()).collect();
299 let expected: HashSet<&str> = ["class", "id"].into_iter().collect();
300 assert_eq!(keys, expected);
301 }
302
303 #[test]
304 fn into_iter_yields_inserted_entries() {
305 let cx = &Cx::default();
306 let mut attrs = Attributes::new();
307 attrs.insert(cx, "class", "button");
308 attrs.insert(cx, "id", "submit");
309 let keys: HashSet<String> = attrs
310 .into_iter()
311 .map(|(k, _)| k.as_str().to_owned())
312 .collect();
313 let expected: HashSet<String> = ["class", "id"].into_iter().map(String::from).collect();
314 assert_eq!(keys, expected);
315 }
316}