Skip to main content

topcoat_view/html/attribute/
attributes.rs

1use 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/// A runtime collection of HTML attributes with unique keys.
11///
12/// Each key appears at most once. Inserting the same key again replaces its
13/// value. Attribute order is unspecified.
14/// Prefer constructing `Attributes` with the [`attributes!`](macro.attributes.html)
15/// macro.
16///
17/// Keys and values are captured when inserted. The collection can be built
18/// separately from the element that renders it.
19#[derive(Debug, Default, Clone)]
20pub struct Attributes {
21    map: HashMap<AttributeKey, AttributeValue>,
22}
23
24impl Attributes {
25    /// Creates an empty attribute collection.
26    ///
27    /// Prefer the
28    /// [`attributes!`](https://docs.rs/topcoat/latest/topcoat/view/macro.attributes.html)
29    /// macro when writing attributes directly. Use this constructor when the
30    /// collection must be populated incrementally.
31    #[inline]
32    #[must_use]
33    pub fn new() -> Self {
34        Attributes::default()
35    }
36
37    /// Creates an empty attribute collection with space for at least `capacity`
38    /// attributes.
39    #[inline]
40    #[must_use]
41    pub fn with_capacity(capacity: usize) -> Self {
42        Self {
43            map: HashMap::with_capacity(capacity),
44        }
45    }
46
47    /// Returns `true` if this collection contains an attribute with key `k`.
48    #[inline]
49    pub fn contains_key(&self, k: impl AsRef<str>) -> bool {
50        self.map.contains_key(k.as_ref())
51    }
52
53    /// Returns the captured value stored for attribute key `k`, if present.
54    #[inline]
55    pub fn get(&self, k: impl AsRef<str>) -> Option<&AttributeValue> {
56        self.map.get(k.as_ref())
57    }
58
59    /// Inserts or replaces an attribute.
60    ///
61    /// Returns the previous value if the key was present. If `v` reports
62    /// that its attribute should be omitted, stores
63    /// [`AttributeValue::Absent`]. This still replaces the previous value
64    /// and retains the key, but renders no attribute.
65    #[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    /// Removes an attribute, returning its captured value if the key was
92    /// present.
93    #[inline]
94    pub fn remove(&mut self, k: impl AsRef<str>) -> Option<AttributeValue> {
95        self.map.remove(k.as_ref())
96    }
97
98    /// Removes all attributes from the collection.
99    #[inline]
100    pub fn clear(&mut self) {
101        self.map.clear();
102    }
103
104    /// Inserts every `(key, value)` entry from `iter`, replacing any keys
105    /// already present.
106    #[inline]
107    pub fn extend(&mut self, iter: impl IntoIterator<Item = (AttributeKey, AttributeValue)>) {
108        self.map.extend(iter);
109    }
110
111    /// Returns an iterator over attribute keys and captured values.
112    #[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&quot;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}