1pub struct Classes {
2 them: Vec<Class>,
3}
4
5impl Classes {
6 pub fn new() -> Self {
7 Self { them: Vec::new() }
8 }
9
10 pub fn add(&mut self, class: Class) -> &mut Self {
11 self.them.push(class);
12 self
13 }
14
15 pub fn collect(&self) -> String {
16 self.them
17 .iter()
18 .filter_map(|class| class.get())
19 .collect::<Vec<String>>()
20 .join(" ")
21 }
22}
23
24impl Default for Classes {
25 fn default() -> Self {
26 Classes::new()
27 }
28}
29
30pub struct Class(Option<String>);
31
32impl Class {
33 pub fn get(&self) -> Option<String> {
34 self.0.clone()
35 }
36
37 pub fn new(value: Option<String>) -> Self {
38 Self(value.and_then(|it| if it.is_empty() { None } else { Some(it) }))
39 }
40}
41
42impl From<String> for Class {
43 fn from(value: String) -> Self {
44 Class::new(Some(value))
45 }
46}
47
48impl From<&str> for Class {
49 fn from(value: &str) -> Self {
50 Class::new(Some(value.into()))
51 }
52}
53
54impl From<Option<String>> for Class {
55 fn from(value: Option<String>) -> Self {
56 Class::new(value)
57 }
58}
59
60impl From<Option<&str>> for Class {
61 fn from(value: Option<&str>) -> Self {
62 Class::new(value.map(|it| it.into()))
63 }
64}
65
66#[macro_export]
67macro_rules! classes {
68 ($($token:expr$(=> $bool:expr)?),*) => [
69 {
70 let mut classes = $crate::core::Classes::new();
71
72 $(
73 if true $(&& $bool)? {
74 classes.add($token.into());
75 }
76 )*
77
78 classes.collect()
79 }
80 ];
81}
82
83#[cfg(test)]
84mod tests {
85 macro_rules! tests {
86 [$([$test_name:ident, $actual:expr, $expected:literal]),+$(,)?] => {
87 $(
88 #[test]
89 fn $test_name() {
90 assert_eq!($actual, $expected);
91 }
92 )+
93 }
94 }
95
96 const DISABLED: bool = true;
97
98 tests![
99 [should_accept_str_and_strings, classes!["button".to_string(), "button--disabled"], "button button--disabled"],
100 [should_accept_optionals, classes![Some("button--active"), None::<String>, Some("button--disabled".to_string())], "button--active button--disabled"],
101 [should_accept_expressions, classes!["concatenated".to_string() + "-class", Some("batman").map(|_| "bruce-wayne")], "concatenated-class bruce-wayne"],
102 [should_apply_classes_evaluating_to_true, classes!["button" => true, "button--disabled" => DISABLED, "button--active" => false, "all-the-buttons" => 42 > 3 ], "button button--disabled all-the-buttons"],
103 [should_accept_various_types_at_the_same_time, classes!["button" => true, Some("button--disabled"), None::<String>, "button--primary"], "button button--disabled button--primary"],
104 [should_remove_empty_str, classes!["button", "", "button--active"], "button button--active"],
105 [should_remove_empty_string, classes!["button", "".to_string(), "button--active"], "button button--active"],
106 [should_remove_empty_strings_passed_as_options, classes!["button", Some(""), "button--active"], "button button--active"],
107 ];
108}