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