1mod attributes;
2mod collector;
3mod key;
4mod value;
5
6pub use attributes::*;
7pub(crate) use collector::*;
8pub use key::*;
9use topcoat_core::context::Cx;
10pub use value::*;
11
12use crate::{HtmlContext, PartsWriter};
13
14#[derive(Debug, Clone)]
19pub struct Attribute<K, V> {
20 key: K,
21 value: V,
22}
23
24impl<K, V> Attribute<K, V> {
25 #[inline]
27 pub fn new(key: K, value: V) -> Self {
28 Self { key, value }
29 }
30}
31
32pub trait AttributeViewParts {
51 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>);
53}
54
55impl<K, V> AttributeViewParts for Attribute<K, V>
56where
57 K: AttributeKeyViewParts,
58 V: AttributeValueViewParts,
59{
60 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
61 if self.value.attribute_present() {
62 parts.push_promoted_str_unescaped(&" ");
63 parts.in_context(HtmlContext::AttributeKey, |parts| {
64 self.key.into_view_parts(cx, parts);
65 });
66 parts.push_promoted_str_unescaped(&"=\"");
67 parts.in_context(HtmlContext::AttributeValue, |parts| {
68 self.value.into_view_parts(cx, parts);
69 });
70 parts.push_promoted_str_unescaped(&"\"");
71 }
72 }
73}
74
75impl<T> AttributeViewParts for Option<T>
76where
77 T: AttributeViewParts,
78{
79 #[inline]
80 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
81 if let Some(value) = self {
82 value.into_view_parts(cx, parts);
83 }
84 }
85}
86
87impl<T> AttributeViewParts for Vec<T>
88where
89 T: AttributeViewParts,
90{
91 #[inline]
92 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
93 for value in self {
94 value.into_view_parts(cx, parts);
95 }
96 }
97}
98
99impl<'b, T: ?Sized> AttributeViewParts for &&'b T
100where
101 &'b T: AttributeViewParts,
102{
103 #[inline]
104 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
105 (*self).into_view_parts(cx, parts);
106 }
107}
108
109macro_rules! impl_tuple {
110 ($($ty:ident),+) => {
111 impl<$($ty),+> AttributeViewParts for ($($ty,)+)
112 where
113 $($ty: AttributeViewParts,)+
114 {
115 #[inline]
116 #[allow(non_snake_case)]
117 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
118 let ($($ty,)+) = self;
119 $($ty.into_view_parts(cx, parts);)+
120 }
121 }
122 };
123}
124
125impl_tuple!(T1);
126impl_tuple!(T1, T2);
127impl_tuple!(T1, T2, T3);
128impl_tuple!(T1, T2, T3, T4);
129impl_tuple!(T1, T2, T3, T4, T5);
130impl_tuple!(T1, T2, T3, T4, T5, T6);
131impl_tuple!(T1, T2, T3, T4, T5, T6, T7);
132impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8);
133impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9);
134impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
135impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
136impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::internal::Builder;
142
143 fn render(attribute: impl AttributeViewParts) -> String {
144 let cx = Cx::default();
145 Builder::build(&cx, |b| b.attributes(attribute)).render(&cx)
146 }
147
148 #[test]
149 fn renders_key_and_escaped_value() {
150 let rendered = render(Attribute::new("data-x", "a\"b<c"));
151 assert_eq!(rendered, " data-x=\"a"b<c\"");
152 }
153
154 #[test]
155 fn omits_absent_value() {
156 assert_eq!(render(Attribute::new("disabled", false)), "");
157 }
158
159 #[test]
160 fn dynamic_key_is_validated() {
161 let rendered = render(Attribute::new(String::from("data-x"), "y"));
162 assert_eq!(rendered, " data-x=\"y\"");
163 }
164
165 #[test]
166 #[should_panic(expected = "invalid attribute key")]
167 fn dynamic_key_rejects_breakout() {
168 render(Attribute::new(String::from("x onmouseover"), "y"));
169 }
170}