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
10pub trait AttributeKeyViewParts {
26 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#[non_exhaustive]
137#[derive(Debug, Clone)]
138pub enum AttributeKey {
139 PromotedStr {
141 value: &'static &'static str,
142 context: HtmlContext,
143 },
144 StaticStr {
146 value: &'static str,
147 context: HtmlContext,
148 },
149 String { value: String, context: HtmlContext },
151}
152
153impl AttributeKey {
154 #[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 #[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 #[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 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 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}