Skip to main content

topcoat_view/html/attribute/
key.rs

1use std::{
2    borrow::Borrow,
3    hash::{Hash, Hasher},
4};
5
6use topcoat_core::context::Cx;
7
8use crate::{Captured, HtmlContext, PartsWriter, PromotedStr, StaticStr, Unescaped};
9
10/// Converts a value used as an attribute key into view parts.
11///
12/// Implement this trait to use a type as a dynamic attribute name in
13/// [`view!`](https://docs.rs/topcoat/latest/topcoat/view/macro.view.html):
14///
15/// ```rust
16/// # use topcoat::view::{View, component, view};
17/// # #[component]
18/// # async fn example() -> topcoat::Result<impl View> {
19/// # let my_key = "data-state";
20/// Ok(view! {
21///     <div (my_key)="value"></div>
22/// })
23/// # }
24/// ```
25pub trait AttributeKeyViewParts {
26    /// Appends this attribute key to the view being built.
27    fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>);
28}
29
30impl AttributeKeyViewParts for String {
31    #[inline]
32    fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
33        parts.push_string(self);
34    }
35}
36
37impl AttributeKeyViewParts for &str {
38    #[inline]
39    fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
40        parts.push_str(self);
41    }
42}
43
44impl AttributeKeyViewParts for PromotedStr {
45    #[inline]
46    fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
47        parts.push_promoted_str(self.0);
48    }
49}
50
51impl AttributeKeyViewParts for StaticStr {
52    #[inline]
53    fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
54        parts.push_static_str(self.0);
55    }
56}
57
58impl AttributeKeyViewParts for Unescaped<String> {
59    #[inline]
60    fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
61        parts.push_string_unescaped(self.0);
62    }
63}
64
65impl AttributeKeyViewParts for Unescaped<&'static str> {
66    #[inline]
67    fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
68        parts.push_static_str_unescaped(self.0);
69    }
70}
71
72impl AttributeKeyViewParts for Unescaped<PromotedStr> {
73    #[inline]
74    fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
75        parts.push_promoted_str_unescaped(self.0.0);
76    }
77}
78
79impl AttributeKeyViewParts for Unescaped<StaticStr> {
80    #[inline]
81    fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
82        parts.push_static_str_unescaped(self.0.0);
83    }
84}
85
86impl AttributeKeyViewParts for &String {
87    #[inline]
88    fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
89        self.as_str().into_view_parts(cx, parts);
90    }
91}
92
93impl<'b, T: ?Sized> AttributeKeyViewParts for &&'b T
94where
95    &'b T: AttributeKeyViewParts,
96{
97    #[inline]
98    fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
99        (*self).into_view_parts(cx, parts);
100    }
101}
102
103macro_rules! impl_tuple {
104    ($($ty:ident),+) => {
105        impl<$($ty),+> AttributeKeyViewParts for ($($ty,)+)
106        where
107            $($ty: AttributeKeyViewParts,)+
108        {
109            #[inline]
110            #[allow(non_snake_case)]
111            fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
112                let ($($ty,)+) = self;
113                $($ty.into_view_parts(cx, parts);)+
114            }
115        }
116    };
117}
118
119impl_tuple!(T1);
120impl_tuple!(T1, T2);
121impl_tuple!(T1, T2, T3);
122impl_tuple!(T1, T2, T3, T4);
123impl_tuple!(T1, T2, T3, T4, T5);
124impl_tuple!(T1, T2, T3, T4, T5, T6);
125impl_tuple!(T1, T2, T3, T4, T5, T6, T7);
126impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8);
127impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9);
128impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
129impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
130impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
131
132/// An attribute key captured from any [`AttributeKeyViewParts`] value.
133///
134/// Keys retain their HTML context for validation when rendered. Equality
135/// and hashing use only the key's text.
136#[non_exhaustive]
137#[derive(Debug, Clone)]
138pub enum AttributeKey {
139    /// A static string held by reference.
140    PromotedStr {
141        value: &'static &'static str,
142        context: HtmlContext,
143    },
144    /// A static string.
145    StaticStr {
146        value: &'static str,
147        context: HtmlContext,
148    },
149    /// An owned string.
150    String { value: String, context: HtmlContext },
151}
152
153impl AttributeKey {
154    /// Returns the key's text.
155    #[inline]
156    #[must_use]
157    pub fn as_str(&self) -> &str {
158        match self {
159            Self::PromotedStr { value, .. } => value,
160            Self::StaticStr { value, .. } => value,
161            Self::String { value, .. } => value,
162        }
163    }
164
165    /// Writes the key into `parts` under the context it was captured with.
166    #[inline]
167    fn push_into(self, parts: &mut PartsWriter<'_>) {
168        match self {
169            Self::PromotedStr { value, context } => {
170                parts.in_context(context, |parts| {
171                    parts.push_promoted_str(value);
172                });
173            }
174            Self::StaticStr { value, context } => {
175                parts.in_context(context, |parts| {
176                    parts.push_static_str(value);
177                });
178            }
179            Self::String { value, context } => {
180                parts.in_context(context, |parts| {
181                    parts.push_string(value);
182                });
183            }
184        }
185    }
186
187    /// Writes the key into `parts` under the context it was captured with,
188    /// copying an owned string instead of moving it.
189    #[inline]
190    fn push_ref_into(&self, parts: &mut PartsWriter<'_>) {
191        match self {
192            Self::PromotedStr { value, context } => {
193                parts.in_context(*context, |parts| {
194                    parts.push_promoted_str(value);
195                });
196            }
197            Self::StaticStr { value, context } => {
198                parts.in_context(*context, |parts| {
199                    parts.push_static_str(value);
200                });
201            }
202            Self::String { value, context } => {
203                parts.in_context(*context, |parts| {
204                    parts.push_str(value);
205                });
206            }
207        }
208    }
209}
210
211impl PartialEq for AttributeKey {
212    #[inline]
213    fn eq(&self, other: &Self) -> bool {
214        self.as_str() == other.as_str()
215    }
216}
217
218impl Eq for AttributeKey {}
219
220impl Hash for AttributeKey {
221    #[inline]
222    fn hash<H: Hasher>(&self, state: &mut H) {
223        self.as_str().hash(state);
224    }
225}
226
227impl Borrow<str> for AttributeKey {
228    #[inline]
229    fn borrow(&self) -> &str {
230        self.as_str()
231    }
232}
233
234impl Captured for AttributeKey {
235    #[inline]
236    fn empty() -> Self {
237        Self::StaticStr {
238            value: "",
239            context: HtmlContext::AttributeKey,
240        }
241    }
242
243    #[inline]
244    fn promoted_str(value: &'static &'static str, context: HtmlContext) -> Self {
245        Self::PromotedStr { value, context }
246    }
247
248    #[inline]
249    fn static_str(value: &'static str, context: HtmlContext) -> Self {
250        Self::StaticStr { value, context }
251    }
252
253    #[inline]
254    fn string(value: String, context: HtmlContext) -> Self {
255        Self::String { value, context }
256    }
257}
258
259impl AttributeKeyViewParts for AttributeKey {
260    #[inline]
261    fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
262        self.push_into(parts);
263    }
264}
265
266impl AttributeKeyViewParts for &AttributeKey {
267    #[inline]
268    fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
269        self.push_ref_into(parts);
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use std::collections::HashSet;
276
277    use super::*;
278    use crate::{Attributes, internal::Builder};
279
280    /// Captures `key` the way [`Attributes::insert`] does.
281    fn capture(key: impl AttributeKeyViewParts) -> AttributeKey {
282        let cx = Cx::default();
283        let mut attrs = Attributes::new();
284        attrs.insert(&cx, key, true);
285        attrs.into_iter().next().unwrap().0
286    }
287
288    /// Writes a captured key in attribute key position and renders it.
289    fn render(key: &AttributeKey) -> String {
290        let cx = Cx::default();
291        Builder::build(&cx, |b| b.attribute_key(key)).render(&cx)
292    }
293
294    #[test]
295    fn a_single_promoted_string_is_kept_as_is() {
296        let key = capture(PromotedStr(&"data-x"));
297        assert!(matches!(
298            key,
299            AttributeKey::PromotedStr {
300                context: HtmlContext::AttributeKey,
301                ..
302            }
303        ));
304        assert_eq!(key.as_str(), "data-x");
305        assert_eq!(render(&key), "data-x");
306    }
307
308    #[test]
309    fn a_single_borrowed_string_is_owned() {
310        let key = capture("data-x");
311        assert!(matches!(key, AttributeKey::String { .. }));
312        assert_eq!(key.as_str(), "data-x");
313    }
314
315    #[test]
316    fn multiple_parts_are_rendered() {
317        let key = capture(("data-", "x"));
318        assert!(matches!(
319            key,
320            AttributeKey::String {
321                context: HtmlContext::Unescaped,
322                ..
323            }
324        ));
325        assert_eq!(key.as_str(), "data-x");
326        assert_eq!(render(&key), "data-x");
327    }
328
329    #[test]
330    #[should_panic(expected = "invalid attribute key")]
331    fn an_invalid_key_panics_when_rendered() {
332        render(&capture("on click"));
333    }
334
335    #[test]
336    fn keys_compare_and_hash_by_text() {
337        let promoted = capture(PromotedStr(&"class"));
338        let owned = capture(String::from("class"));
339        assert_eq!(promoted, owned);
340        let keys: HashSet<AttributeKey> = [promoted, owned].into_iter().collect();
341        assert_eq!(keys.len(), 1);
342        assert!(keys.contains("class"));
343    }
344}