clsx_r/
lib.rs

1//! A Rust macro utility for conditionally constructing strings, primarily used for CSS class names.
2//! Inspired by the JavaScript [clsx](https://github.com/lukeed/clsx) package.
3//!
4//! # Examples
5//!
6//! ```rust
7//! use clsx_r::clsx;
8//!
9//! // Basic usage
10//! assert_eq!(clsx!("foo"), "foo");
11//! assert_eq!(clsx!("foo", "bar"), "foo bar");
12//!
13//! // With conditions
14//! let is_active = true;
15//! assert_eq!(clsx!("foo" => is_active), "foo");
16//!
17//! // Mixed usage
18//! let dynamic_class = "dynamic";
19//! assert_eq!(
20//!     clsx!("base", dynamic_class, "active" => true, "disabled" => false),
21//!     "base dynamic active"
22//! );
23//! ```
24#[macro_export]
25macro_rules! clsx {
26    // Case: No args
27    () => {
28        String::new()
29    };
30
31    // Case: trailing comma after args
32    (@internal $classes:ident ,) => {};
33
34    // Case: empty argument after a trailing comma
35    (@internal $classes:ident) => {};
36
37    // Case: single argument without condition
38    ($class:expr) => {
39        $class.to_string()
40    };
41
42    // Case: conditional argument with remaining token
43    (@internal $classes:ident $class:expr => $cond:expr, $($rest:tt)*) => {{
44        if $cond {
45            $classes.push($class.to_string());
46        }
47        $crate::clsx!(@internal $classes $($rest)*);
48    }};
49
50    // Case: last conditional argument
51    (@internal $classes:ident $class:expr => $cond:expr) => {{
52        if $cond {
53            $classes.push($class.to_string());
54        }
55    }};
56
57    // Case: non-conditional argument with remaining tokens
58    (@internal $classes:ident $class:expr, $($rest:tt)*) => {{
59        $classes.push($class.to_string());
60        $crate::clsx!(@internal $classes $($rest)*);
61    }};
62
63    // Case: last non-conditional argument
64    (@internal $classes:ident $class:expr) => {{
65        $classes.push($class.to_string());
66    }};
67
68    // Entry point for handling arguments
69    ($($args:tt)*) => {{
70        let mut classes = Vec::new();
71        $crate::clsx!(@internal classes $($args)*);
72        classes.into_iter()
73            .filter(|s| !s.is_empty())
74            .collect::<Vec<_>>()
75            .join(" ")
76    }};
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn test_basic_usage() {
85        assert_eq!(clsx!("class1"), "class1");
86        assert_eq!(clsx!("class1", "class2"), "class1 class2");
87        assert_eq!(clsx!("class1", "", "class2"), "class1 class2");
88    }
89
90    #[test]
91    fn test_conditional_usage() {
92        assert_eq!(clsx!("class1" => true, "class2" => false), "class1");
93        assert_eq!(clsx!("class1" => false, "class2" => true), "class2");
94    }
95
96    #[test]
97    fn test_mixed_usage() {
98        let dynamic_class = "dynamic";
99        assert_eq!(
100            clsx!("static", dynamic_class, "conditional" => true),
101            "static dynamic conditional"
102        );
103        assert_eq!(
104            clsx!("base", "always", "active" => true, "disabled" => false),
105            "base always active"
106        );
107    }
108
109    #[test]
110    fn test_empty_input() {
111        assert_eq!(clsx!(), "");
112    }
113
114    #[test]
115    fn test_complex_usage() {
116        let is_active = true;
117        let is_disabled = false;
118        assert_eq!(
119            clsx!("header", "button" => is_active, "hidden" => is_disabled, "footer"),
120            "header button footer"
121        );
122    }
123
124    #[test]
125    fn test_trailing_comma() {
126        assert_eq!(clsx!("class1", "class2",), "class1 class2");
127        assert_eq!(clsx!("class1" => true, "class2" => false,), "class1");
128    }
129}