1use std::borrow::Cow;
2
3use topcoat_core::context::Cx;
4
5use crate::{AttributeValueViewParts, PartsWriter, ViewPart};
6
7pub trait ClassViewParts {
19 fn is_present(&self) -> bool;
24
25 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>);
27}
28
29impl ClassViewParts for &str {
30 #[inline]
31 fn is_present(&self) -> bool {
32 !self.is_empty()
33 }
34
35 #[inline]
36 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
37 parts.push_str(self.to_owned());
38 }
39}
40
41impl ClassViewParts for String {
42 #[inline]
43 fn is_present(&self) -> bool {
44 !self.is_empty()
45 }
46
47 #[inline]
48 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
49 parts.push_str(self);
50 }
51}
52
53impl ClassViewParts for &String {
54 #[inline]
55 fn is_present(&self) -> bool {
56 !self.is_empty()
57 }
58
59 #[inline]
60 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
61 ClassViewParts::into_view_parts(self.as_str(), cx, parts);
62 }
63}
64
65impl ClassViewParts for Cow<'static, str> {
66 #[inline]
67 fn is_present(&self) -> bool {
68 !self.is_empty()
69 }
70
71 #[inline]
72 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
73 parts.push_str(self);
74 }
75}
76
77impl ClassViewParts for ViewPart {
81 #[inline]
82 fn is_present(&self) -> bool {
83 self.attribute_present()
84 }
85
86 #[inline]
87 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
88 parts.push_part(self);
91 }
92}
93
94impl ClassViewParts for &ViewPart {
95 #[inline]
96 fn is_present(&self) -> bool {
97 self.attribute_present()
98 }
99
100 #[inline]
101 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
102 ClassViewParts::into_view_parts(ViewPart::clone(self), cx, parts);
103 }
104}
105
106impl<T> ClassViewParts for Option<T>
107where
108 T: ClassViewParts,
109{
110 #[inline]
111 fn is_present(&self) -> bool {
112 self.as_ref().is_some_and(T::is_present)
113 }
114
115 #[inline]
116 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
117 if let Some(value) = self {
118 value.into_view_parts(cx, parts);
119 }
120 }
121}
122
123#[doc(hidden)]
128#[derive(Debug, Clone, Copy)]
129pub enum ClassBranch<A, B> {
130 Then(A),
131 Else(B),
132}
133
134impl<A, B> ClassViewParts for ClassBranch<A, B>
135where
136 A: ClassViewParts,
137 B: ClassViewParts,
138{
139 #[inline]
140 fn is_present(&self) -> bool {
141 match self {
142 Self::Then(inner) => inner.is_present(),
143 Self::Else(inner) => inner.is_present(),
144 }
145 }
146
147 #[inline]
148 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
149 match self {
150 Self::Then(inner) => inner.into_view_parts(cx, parts),
151 Self::Else(inner) => inner.into_view_parts(cx, parts),
152 }
153 }
154}
155
156pub struct ClassWriter<'a, 'b> {
162 parts: &'b mut PartsWriter<'a>,
163 first: bool,
164}
165
166impl<'a, 'b> ClassWriter<'a, 'b> {
167 #[inline]
169 pub(crate) fn new(parts: &'b mut PartsWriter<'a>) -> Self {
170 Self { parts, first: true }
171 }
172
173 #[inline]
179 pub fn entry(&mut self, cx: &Cx, value: impl ClassViewParts) -> &mut Self {
180 if value.is_present() {
181 if !self.first {
182 self.parts.push_str(" ");
183 }
184 value.into_view_parts(cx, self.parts);
185 self.first = false;
186 }
187 self
188 }
189}
190
191pub trait ClassEntries {
198 fn any_present(&self) -> bool;
200
201 fn write_entries(self, cx: &Cx, writer: &mut ClassWriter<'_, '_>);
203}
204
205impl<T> ClassEntries for T
206where
207 T: ClassViewParts,
208{
209 #[inline]
210 fn any_present(&self) -> bool {
211 self.is_present()
212 }
213
214 #[inline]
215 fn write_entries(self, cx: &Cx, writer: &mut ClassWriter<'_, '_>) {
216 writer.entry(cx, self);
217 }
218}
219
220impl ClassEntries for () {
221 #[inline]
222 fn any_present(&self) -> bool {
223 false
224 }
225
226 #[inline]
227 fn write_entries(self, _cx: &Cx, _writer: &mut ClassWriter<'_, '_>) {}
228}
229
230impl<T> ClassEntries for Vec<T>
231where
232 T: ClassEntries,
233{
234 #[inline]
235 fn any_present(&self) -> bool {
236 self.iter().any(ClassEntries::any_present)
237 }
238
239 #[inline]
240 fn write_entries(self, cx: &Cx, writer: &mut ClassWriter<'_, '_>) {
241 for entry in self {
242 entry.write_entries(cx, writer);
243 }
244 }
245}
246
247impl<T, const N: usize> ClassEntries for [T; N]
248where
249 T: ClassEntries,
250{
251 #[inline]
252 fn any_present(&self) -> bool {
253 self.iter().any(ClassEntries::any_present)
254 }
255
256 #[inline]
257 fn write_entries(self, cx: &Cx, writer: &mut ClassWriter<'_, '_>) {
258 for entry in self {
259 entry.write_entries(cx, writer);
260 }
261 }
262}
263
264macro_rules! impl_tuple {
265 ($($ty:ident),+) => {
266 impl<$($ty),+> ClassEntries for ($($ty,)+)
267 where
268 $($ty: ClassEntries,)+
269 {
270 #[inline]
271 #[allow(non_snake_case)]
272 fn any_present(&self) -> bool {
273 let ($($ty,)+) = self;
274 $($ty.any_present())||+
275 }
276
277 #[inline]
278 #[allow(non_snake_case)]
279 fn write_entries(self, cx: &Cx, writer: &mut ClassWriter<'_, '_>) {
280 let ($($ty,)+) = self;
281 $($ty.write_entries(cx, writer);)+
282 }
283 }
284 };
285}
286
287impl_tuple!(T1);
288impl_tuple!(T1, T2);
289impl_tuple!(T1, T2, T3);
290impl_tuple!(T1, T2, T3, T4);
291impl_tuple!(T1, T2, T3, T4, T5);
292impl_tuple!(T1, T2, T3, T4, T5, T6);
293impl_tuple!(T1, T2, T3, T4, T5, T6, T7);
294impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8);
295impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9);
296impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
297impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
298impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
299
300#[derive(Debug, Default, Clone, Copy)]
322pub struct Class<T>(pub T);
323
324impl<T> AttributeValueViewParts for Class<T>
325where
326 T: ClassEntries,
327{
328 #[inline]
329 fn attribute_present(&self) -> bool {
330 self.0.any_present()
331 }
332
333 #[inline]
334 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
335 self.0.write_entries(cx, &mut ClassWriter::new(parts));
336 }
337}
338
339impl<T> ClassViewParts for Class<T>
340where
341 T: ClassEntries,
342{
343 #[inline]
344 fn is_present(&self) -> bool {
345 self.0.any_present()
346 }
347
348 #[inline]
349 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
350 self.0.write_entries(cx, &mut ClassWriter::new(parts));
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360 use crate::{HtmlContext, View, ViewParts};
361
362 fn render(class: Class<impl ClassEntries>) -> String {
363 let cx = Cx::default();
364 let mut parts = ViewParts::new();
365 AttributeValueViewParts::into_view_parts(
366 class,
367 &cx,
368 &mut PartsWriter::new(&mut parts, HtmlContext::AttributeValue),
369 );
370 View::new(parts).render(&cx)
371 }
372
373 #[test]
374 fn no_entries_is_absent() {
375 let class = Class(());
376 assert!(!class.attribute_present());
377 assert_eq!(render(class), "");
378 }
379
380 #[test]
381 fn single_entry_renders_without_separator() {
382 let class = Class("btn");
383 assert!(class.attribute_present());
384 assert_eq!(render(class), "btn");
385 }
386
387 #[test]
388 fn entries_are_separated_by_single_spaces() {
389 let class = Class(("btn", "btn-lg".to_owned(), Cow::Borrowed("active")));
390 assert_eq!(render(class), "btn btn-lg active");
391 }
392
393 #[test]
394 fn none_option_is_skipped_without_a_separator() {
395 let class = Class(("a", Option::<&str>::None, "b"));
396 assert_eq!(render(class), "a b");
397 }
398
399 #[test]
400 fn some_option_is_rendered() {
401 assert_eq!(render(Class(Some("active"))), "active");
402 }
403
404 #[test]
405 fn empty_strings_are_skipped_without_a_separator() {
406 let class = Class(("a", "", String::new(), Some(""), "b"));
407 assert_eq!(render(class), "a b");
408 }
409
410 #[test]
411 fn all_entries_absent_omits_the_attribute() {
412 let class = Class(("", Option::<&str>::None));
413 assert!(!class.attribute_present());
414 assert_eq!(render(class), "");
415 }
416
417 #[test]
418 fn skipped_leading_entry_produces_no_leading_space() {
419 let class = Class((Option::<&str>::None, "a"));
420 assert_eq!(render(class), "a");
421 }
422
423 #[test]
424 fn entries_are_escaped_for_the_attribute_value_position() {
425 assert_eq!(render(Class("a\"b")), "a"b");
426 }
427
428 #[test]
429 fn branch_entries_render_the_taken_branch() {
430 let class = Class((
431 ClassBranch::<&str, String>::Then("on"),
432 ClassBranch::<&str, String>::Else("off".to_owned()),
433 ));
434 assert_eq!(render(class), "on off");
435 }
436
437 #[test]
438 fn nested_class_is_spliced_with_separators() {
439 let class = Class(("card", Class(("btn", "btn-lg"))));
440 assert_eq!(render(class), "card btn btn-lg");
441 }
442
443 #[test]
444 fn empty_nested_class_is_skipped_without_a_separator() {
445 let class = Class(("a", Class(()), "b"));
446 assert_eq!(render(class), "a b");
447 }
448
449 #[test]
450 fn nested_tuples_flatten_with_separators() {
451 let class = Class((("a", "b"), ("c", "d")));
452 assert_eq!(render(class), "a b c d");
453 }
454
455 #[test]
456 fn vec_entries_render_with_separators() {
457 let class = Class(vec!["a".to_owned(), String::new(), "b".to_owned()]);
458 assert_eq!(render(class), "a b");
459 }
460
461 #[test]
462 fn array_entries_render_with_separators() {
463 let class = Class([Some("a"), None, Some("b")]);
464 assert_eq!(render(class), "a b");
465 }
466
467 #[test]
468 fn attribute_value_entries_are_spliced_verbatim() {
469 let mut parts = ViewParts::new();
470 PartsWriter::new(&mut parts, HtmlContext::AttributeValue).push_str("[&>*]:mt-2");
471 let part = ViewPart::from(parts);
472 assert_eq!(render(Class(("btn", &part))), "btn [&>*]:mt-2");
473 }
474
475 #[test]
476 fn empty_attribute_value_entry_is_skipped_without_a_separator() {
477 let part = ViewPart::empty();
478 assert_eq!(render(Class(("a", &part, "b"))), "a b");
479 }
480
481 #[test]
482 fn borrowed_cow_entries_render_without_reallocating() {
483 let class = Class((
484 Cow::Borrowed("static"),
485 Cow::<'static, str>::Owned("owned".to_owned()),
486 ));
487 assert_eq!(render(class), "static owned");
488 }
489}