1use std::borrow::Cow;
2
3use topcoat_core::context::Cx;
4
5use crate::{AttributeValueViewParts, PartsWriter, PromotedStr, StaticStr, Unescaped};
6
7pub trait ClassViewParts {
16 fn is_present(&self) -> bool;
21
22 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>);
24}
25
26impl ClassViewParts for &str {
27 #[inline]
28 fn is_present(&self) -> bool {
29 !self.is_empty()
30 }
31
32 #[inline]
33 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
34 parts.push_str(self);
35 }
36}
37
38impl ClassViewParts for String {
39 #[inline]
40 fn is_present(&self) -> bool {
41 !self.is_empty()
42 }
43
44 #[inline]
45 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
46 parts.push_string(self);
47 }
48}
49
50impl ClassViewParts for &String {
51 #[inline]
52 fn is_present(&self) -> bool {
53 !self.is_empty()
54 }
55
56 #[inline]
57 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
58 ClassViewParts::into_view_parts(self.as_str(), cx, parts);
59 }
60}
61
62impl ClassViewParts for PromotedStr {
63 #[inline]
64 fn is_present(&self) -> bool {
65 !self.0.is_empty()
66 }
67
68 #[inline]
69 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
70 parts.push_promoted_str(self.0);
71 }
72}
73
74impl ClassViewParts for StaticStr {
75 #[inline]
76 fn is_present(&self) -> bool {
77 !self.0.is_empty()
78 }
79
80 #[inline]
81 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
82 parts.push_static_str(self.0);
83 }
84}
85
86impl ClassViewParts for Unescaped<PromotedStr> {
87 #[inline]
88 fn is_present(&self) -> bool {
89 !self.0.0.is_empty()
90 }
91
92 #[inline]
93 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
94 parts.push_promoted_str_unescaped(self.0.0);
95 }
96}
97
98impl ClassViewParts for Unescaped<StaticStr> {
99 #[inline]
100 fn is_present(&self) -> bool {
101 !self.0.0.is_empty()
102 }
103
104 #[inline]
105 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
106 parts.push_static_str_unescaped(self.0.0);
107 }
108}
109
110impl ClassViewParts for Cow<'static, str> {
111 #[inline]
112 fn is_present(&self) -> bool {
113 !self.is_empty()
114 }
115
116 #[inline]
117 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
118 match self {
119 Cow::Borrowed(value) => parts.push_static_str(value),
120 Cow::Owned(value) => parts.push_string(value),
121 };
122 }
123}
124
125impl<T> ClassViewParts for Option<T>
126where
127 T: ClassViewParts,
128{
129 #[inline]
130 fn is_present(&self) -> bool {
131 self.as_ref().is_some_and(T::is_present)
132 }
133
134 #[inline]
135 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
136 if let Some(value) = self {
137 value.into_view_parts(cx, parts);
138 }
139 }
140}
141
142#[doc(hidden)]
147#[derive(Debug, Clone, Copy)]
148pub enum ClassBranch<A, B> {
149 Then(A),
150 Else(B),
151}
152
153impl<A, B> ClassViewParts for ClassBranch<A, B>
154where
155 A: ClassViewParts,
156 B: ClassViewParts,
157{
158 #[inline]
159 fn is_present(&self) -> bool {
160 match self {
161 Self::Then(inner) => inner.is_present(),
162 Self::Else(inner) => inner.is_present(),
163 }
164 }
165
166 #[inline]
167 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
168 match self {
169 Self::Then(inner) => inner.into_view_parts(cx, parts),
170 Self::Else(inner) => inner.into_view_parts(cx, parts),
171 }
172 }
173}
174
175pub struct ClassWriter<'a, 'b> {
179 parts: &'b mut PartsWriter<'a>,
180 first: bool,
181}
182
183impl<'a, 'b> ClassWriter<'a, 'b> {
184 #[inline]
186 pub(crate) fn new(parts: &'b mut PartsWriter<'a>) -> Self {
187 Self { parts, first: true }
188 }
189
190 #[inline]
196 pub fn entry(&mut self, cx: &Cx, value: impl ClassViewParts) -> &mut Self {
197 if value.is_present() {
198 if !self.first {
199 self.parts.push_promoted_str(&" ");
200 }
201 value.into_view_parts(cx, self.parts);
202 self.first = false;
203 }
204 self
205 }
206}
207
208pub trait ClassEntries {
212 fn any_present(&self) -> bool;
214
215 fn write_entries(self, cx: &Cx, writer: &mut ClassWriter<'_, '_>);
217}
218
219impl<T> ClassEntries for T
220where
221 T: ClassViewParts,
222{
223 #[inline]
224 fn any_present(&self) -> bool {
225 self.is_present()
226 }
227
228 #[inline]
229 fn write_entries(self, cx: &Cx, writer: &mut ClassWriter<'_, '_>) {
230 writer.entry(cx, self);
231 }
232}
233
234impl ClassEntries for () {
235 #[inline]
236 fn any_present(&self) -> bool {
237 false
238 }
239
240 #[inline]
241 fn write_entries(self, _cx: &Cx, _writer: &mut ClassWriter<'_, '_>) {}
242}
243
244impl<T> ClassEntries for Vec<T>
245where
246 T: ClassEntries,
247{
248 #[inline]
249 fn any_present(&self) -> bool {
250 self.iter().any(ClassEntries::any_present)
251 }
252
253 #[inline]
254 fn write_entries(self, cx: &Cx, writer: &mut ClassWriter<'_, '_>) {
255 for entry in self {
256 entry.write_entries(cx, writer);
257 }
258 }
259}
260
261impl<T, const N: usize> ClassEntries for [T; N]
262where
263 T: ClassEntries,
264{
265 #[inline]
266 fn any_present(&self) -> bool {
267 self.iter().any(ClassEntries::any_present)
268 }
269
270 #[inline]
271 fn write_entries(self, cx: &Cx, writer: &mut ClassWriter<'_, '_>) {
272 for entry in self {
273 entry.write_entries(cx, writer);
274 }
275 }
276}
277
278macro_rules! impl_tuple {
279 ($($ty:ident),+) => {
280 impl<$($ty),+> ClassEntries for ($($ty,)+)
281 where
282 $($ty: ClassEntries,)+
283 {
284 #[inline]
285 #[allow(non_snake_case)]
286 fn any_present(&self) -> bool {
287 let ($($ty,)+) = self;
288 $($ty.any_present())||+
289 }
290
291 #[inline]
292 #[allow(non_snake_case)]
293 fn write_entries(self, cx: &Cx, writer: &mut ClassWriter<'_, '_>) {
294 let ($($ty,)+) = self;
295 $($ty.write_entries(cx, writer);)+
296 }
297 }
298 };
299}
300
301impl_tuple!(T1);
302impl_tuple!(T1, T2);
303impl_tuple!(T1, T2, T3);
304impl_tuple!(T1, T2, T3, T4);
305impl_tuple!(T1, T2, T3, T4, T5);
306impl_tuple!(T1, T2, T3, T4, T5, T6);
307impl_tuple!(T1, T2, T3, T4, T5, T6, T7);
308impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8);
309impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9);
310impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
311impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
312impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
313
314#[derive(Debug, Default, Clone, Copy)]
333pub struct Class<T>(pub T);
334
335impl<T> AttributeValueViewParts for Class<T>
336where
337 T: ClassEntries,
338{
339 #[inline]
340 fn attribute_present(&self) -> bool {
341 self.0.any_present()
342 }
343
344 #[inline]
345 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
346 self.0.write_entries(cx, &mut ClassWriter::new(parts));
347 }
348}
349
350impl<T> ClassViewParts for Class<T>
351where
352 T: ClassEntries,
353{
354 #[inline]
355 fn is_present(&self) -> bool {
356 self.0.any_present()
357 }
358
359 #[inline]
360 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
361 self.0.write_entries(cx, &mut ClassWriter::new(parts));
365 }
366}
367
368pub type StaticClass = Class<Unescaped<PromotedStr>>;
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use crate::{AttributeValue, Attributes, internal::Builder};
384
385 fn render(class: Class<impl ClassEntries>) -> String {
386 let cx = Cx::default();
387 Builder::build(&cx, |b| b.attribute_value(class)).render(&cx)
388 }
389
390 #[test]
391 fn no_entries_is_absent() {
392 let class = Class(());
393 assert!(!class.attribute_present());
394 assert_eq!(render(class), "");
395 }
396
397 #[test]
398 fn single_entry_renders_without_separator() {
399 let class = Class("btn");
400 assert!(class.attribute_present());
401 assert_eq!(render(class), "btn");
402 }
403
404 #[test]
405 fn entries_are_separated_by_single_spaces() {
406 let class = Class(("btn", "btn-lg".to_owned(), Cow::Borrowed("active")));
407 assert_eq!(render(class), "btn btn-lg active");
408 }
409
410 #[test]
411 fn none_option_is_skipped_without_a_separator() {
412 let class = Class(("a", Option::<&str>::None, "b"));
413 assert_eq!(render(class), "a b");
414 }
415
416 #[test]
417 fn some_option_is_rendered() {
418 assert_eq!(render(Class(Some("active"))), "active");
419 }
420
421 #[test]
422 fn empty_strings_are_skipped_without_a_separator() {
423 let class = Class(("a", "", String::new(), Some(""), "b"));
424 assert_eq!(render(class), "a b");
425 }
426
427 #[test]
428 fn all_entries_absent_omits_the_attribute() {
429 let class = Class(("", Option::<&str>::None));
430 assert!(!class.attribute_present());
431 assert_eq!(render(class), "");
432 }
433
434 #[test]
435 fn skipped_leading_entry_produces_no_leading_space() {
436 let class = Class((Option::<&str>::None, "a"));
437 assert_eq!(render(class), "a");
438 }
439
440 #[test]
441 fn entries_are_escaped_for_the_attribute_value_position() {
442 assert_eq!(render(Class("a\"b")), "a"b");
443 }
444
445 #[test]
446 fn branch_entries_render_the_taken_branch() {
447 let class = Class((
448 ClassBranch::<&str, String>::Then("on"),
449 ClassBranch::<&str, String>::Else("off".to_owned()),
450 ));
451 assert_eq!(render(class), "on off");
452 }
453
454 #[test]
455 fn nested_class_is_spliced_with_separators() {
456 let class = Class(("card", Class(("btn", "btn-lg"))));
457 assert_eq!(render(class), "card btn btn-lg");
458 }
459
460 #[test]
461 fn empty_nested_class_is_skipped_without_a_separator() {
462 let class = Class(("a", Class(()), "b"));
463 assert_eq!(render(class), "a b");
464 }
465
466 #[test]
467 fn nested_tuples_flatten_with_separators() {
468 let class = Class((("a", "b"), ("c", "d")));
469 assert_eq!(render(class), "a b c d");
470 }
471
472 #[test]
473 fn vec_entries_render_with_separators() {
474 let class = Class(vec!["a".to_owned(), String::new(), "b".to_owned()]);
475 assert_eq!(render(class), "a b");
476 }
477
478 #[test]
479 fn array_entries_render_with_separators() {
480 let class = Class([Some("a"), None, Some("b")]);
481 assert_eq!(render(class), "a b");
482 }
483
484 #[test]
485 fn attribute_value_entries_are_spliced_as_captured() {
486 let cx = Cx::default();
487 let mut attrs = Attributes::new();
488 attrs.insert(&cx, "class", "[&>*]:mt-2");
489 let value = attrs.remove("class").unwrap();
490 assert_eq!(render(Class(("btn", &value))), "btn [&>*]:mt-2");
491 }
492
493 #[test]
494 fn absent_attribute_value_entry_is_skipped_without_a_separator() {
495 let value = AttributeValue::absent();
496 assert_eq!(render(Class(("a", &value, "b"))), "a b");
497 }
498
499 #[test]
500 fn promoted_and_static_entries_render_with_separators() {
501 let class = Class((PromotedStr(&"btn"), StaticStr("btn-lg")));
502 assert_eq!(render(class), "btn btn-lg");
503 }
504
505 #[test]
506 fn empty_promoted_and_static_entries_are_skipped_without_a_separator() {
507 let class = Class((StaticStr("a"), PromotedStr(&""), StaticStr(""), "b"));
508 assert!(class.attribute_present());
509 assert_eq!(render(class), "a b");
510 }
511
512 #[test]
513 fn promoted_and_static_entries_are_escaped_for_the_attribute_value_position() {
514 let class = Class((PromotedStr(&"a\"b"), StaticStr("c\"d")));
515 assert_eq!(render(class), "a"b c"d");
516 }
517
518 #[test]
519 fn unescaped_entries_render_verbatim() {
520 let class = Class((
521 Unescaped::new_unchecked(PromotedStr(&"a"b")),
522 Unescaped::new_unchecked(StaticStr("c&d")),
523 ));
524 assert_eq!(render(class), "a"b c&d");
525 }
526
527 #[test]
528 fn empty_unescaped_entries_are_skipped_without_a_separator() {
529 let class = Class((
530 "a",
531 Unescaped::new_unchecked(PromotedStr(&"")),
532 Unescaped::new_unchecked(StaticStr("")),
533 "b",
534 ));
535 assert_eq!(render(class), "a b");
536 }
537
538 #[test]
539 fn borrowed_cow_entries_render_without_reallocating() {
540 let class = Class((
541 Cow::Borrowed("static"),
542 Cow::<'static, str>::Owned("owned".to_owned()),
543 ));
544 assert_eq!(render(class), "static owned");
545 }
546}