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