classes/
core.rs

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}