Skip to main content

topcoat_view/attribute/
attributes.rs

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/// A runtime collection of HTML attributes with unique keys.
11///
12/// `Attributes` is map-like: each key appears at most once, and inserting the
13/// same key again replaces the previous value. Do not rely on render order.
14/// Prefer constructing `Attributes` with the [`attributes!`](macro.attributes.html)
15/// macro.
16#[derive(Debug, Default, Clone)]
17pub struct Attributes {
18    map: HashMap<String, ViewPart>,
19}
20
21impl Attributes {
22    /// Creates an empty attribute collection.
23    ///
24    /// Prefer the
25    /// [`attributes!`](https://docs.rs/topcoat/latest/topcoat/view/macro.attributes.html)
26    /// macro when writing attributes directly. Use this constructor when the
27    /// collection must be populated incrementally.
28    #[inline]
29    #[must_use]
30    pub fn new() -> Self {
31        Attributes::default()
32    }
33
34    /// Creates an empty attribute collection with space for at least `capacity`
35    /// attributes.
36    ///
37    /// Prefer the
38    /// [`attributes!`](https://docs.rs/topcoat/latest/topcoat/view/macro.attributes.html)
39    /// macro when writing attributes directly. This is mainly useful for
40    /// generated code or manual builders that already know how many attributes
41    /// they will insert.
42    #[inline]
43    #[must_use]
44    pub fn with_capacity(capacity: usize) -> Self {
45        Self {
46            map: HashMap::with_capacity(capacity),
47        }
48    }
49
50    /// Returns `true` if this collection contains an attribute with key `k`.
51    #[inline]
52    pub fn contains_key(&self, k: impl AsRef<str>) -> bool {
53        self.map.contains_key(k.as_ref())
54    }
55
56    /// Returns the view parts stored for attribute key `k`, if present.
57    #[inline]
58    pub fn get(&self, k: impl AsRef<str>) -> Option<&ViewPart> {
59        self.map.get(k.as_ref())
60    }
61
62    /// Inserts or replaces an attribute.
63    ///
64    /// The value is converted with [`AttributeValueViewParts`].
65    /// If the key was already present, the previous rendered value is returned.
66    /// If the implementation of [`AttributeValueViewParts`] for `v` signals that
67    /// the attribute should not be present, [`ViewPart::empty`] will be used as
68    /// the value instead, which call cause the previous value to be removed
69    /// and the attribute will not be rendered in a `view!`.
70    #[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    /// Removes an attribute, returning its rendered value if the key was
90    /// present.
91    #[inline]
92    pub fn remove(&mut self, k: impl AsRef<str>) -> Option<ViewPart> {
93        self.map.remove(k.as_ref())
94    }
95
96    /// Removes all attributes from the collection.
97    #[inline]
98    pub fn clear(&mut self) {
99        self.map.clear();
100    }
101
102    /// Inserts every `(key, value)` entry from `iter`, replacing any keys
103    /// already present.
104    #[inline]
105    pub fn extend(&mut self, iter: impl IntoIterator<Item = (String, ViewPart)>) {
106        self.map.extend(iter);
107    }
108
109    /// Returns an iterator over attribute keys and rendered values.
110    #[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&quot;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}