classnames_rs/
lib.rs

1/// Conditional selection helper macro for simplifying conditional class name logic.
2///
3/// Accepts three parameters:
4/// - condition: A conditional expression
5/// - true_value: Class name returned when condition is true
6/// - false_value: Class name returned when condition is false
7///
8/// # Examples
9///
10/// ```rust
11/// use classnames_rs::{classnames, choose};
12///
13/// let is_active = true;
14/// let result = classnames!(
15///     "btn",
16///     choose!(is_active, "active", "inactive")
17/// );
18/// assert_eq!(result, "btn active");
19///
20/// // Can be combined with classnames! macro
21/// let is_dark = false;
22/// let size = "large";
23/// let result = classnames!(
24///     "theme",
25///     choose!(is_dark, "dark", "light"),
26///     size
27/// );
28/// assert_eq!(result, "theme light large");
29/// ```
30use proc_macro::TokenStream;
31use quote::quote;
32use syn::{
33    parse::{Parse, ParseStream},
34    parse_macro_input,
35    punctuated::Punctuated,
36    Expr, ExprBlock, ExprIf, ExprTuple, Token,
37};
38
39struct ClassNamesInput {
40    exprs: Vec<Expr>,
41}
42
43impl Parse for ClassNamesInput {
44    fn parse(input: ParseStream) -> syn::Result<Self> {
45        let exprs = Punctuated::<Expr, Token![,]>::parse_terminated(input)?;
46        Ok(ClassNamesInput {
47            exprs: exprs.into_iter().collect(),
48        })
49    }
50}
51
52/// A procedural macro for dynamically building CSS class names.
53///
54/// # Features
55/// - Support for string literals
56/// - Support for conditional class names
57/// - Support for Option types (Use `maybe!` macro)
58/// - Support for ternary expressions
59/// - Support for block expressions
60/// - Automatic whitespace normalization
61///
62/// # Examples
63///
64/// ### Basic usage:
65/// ```rust
66/// use classnames_rs::classnames;
67///
68/// let result = classnames!("btn", "btn-primary");
69/// assert_eq!(result, "btn btn-primary");
70/// ```
71///
72/// ### Conditional class names:
73/// ```rust
74/// use classnames_rs::classnames;
75///
76/// let is_active = true;
77/// let result = classnames!(
78///     "btn",
79///     (is_active, "active")
80/// );
81/// assert_eq!(result, "btn active");
82/// ```
83///
84/// ### Option types:
85/// ```rust
86/// use classnames_rs::{classnames, maybe};
87///
88/// let optional_class: Option<&str> = Some("highlight");
89/// let result = classnames!("base", maybe!(optional_class));
90/// assert_eq!(result, "base highlight");
91/// ```
92///
93/// ### Ternary expressions:
94/// ```rust
95/// use classnames_rs::classnames;
96///
97/// let is_dark = true;
98/// let result = classnames!(
99///     "theme",
100///     if is_dark { "dark" } else { "light" }
101/// );
102/// assert_eq!(result, "theme dark");
103/// ```
104///
105/// ### Triple tuple conditions:
106/// ```rust
107/// use classnames_rs::classnames;
108///
109/// let count = 5;
110/// let result = classnames!(
111///     "list",
112///     (count > 0, "has-items", "empty")
113/// );
114/// assert_eq!(result, "list has-items");
115/// ```
116#[proc_macro]
117pub fn classnames(input: TokenStream) -> TokenStream {
118    let input = parse_macro_input!(input as ClassNamesInput);
119    let mut tokens = Vec::new();
120
121    for expr in input.exprs {
122        tokens.push(parse_expr(expr));
123    }
124
125    quote! {
126        {
127            let mut classes = Vec::new();
128            #(#tokens)*
129            classes.into_iter()
130                .filter(|s| !s.is_empty())
131                .collect::<Vec<_>>()
132                .join(" ")
133        }
134    }
135    .into()
136}
137
138/// Inline function for normalizing class name strings
139#[inline]
140#[allow(dead_code)]
141fn normalize_classname(input: &str) -> String {
142    input
143        .split_whitespace()
144        .filter(|s| !s.is_empty())
145        .collect::<Vec<_>>()
146        .join(" ")
147}
148
149fn parse_expr(expr: Expr) -> proc_macro2::TokenStream {
150    // Detailed debug output for development
151    // eprintln!("DEBUG - Full Expression: {:#?}", expr);
152
153    match expr {
154        // Regular Path (constants or variable references)
155        Expr::Path(path) => {
156            // eprintln!("DEBUG - Matched Regular Path: {:#?}", path);
157            quote! {
158                {
159                    let class_str = #path;
160                    let normalized = class_str.split_whitespace()
161                        .filter(|s| !s.is_empty())
162                        .collect::<Vec<_>>()
163                        .join(" ");
164                    classes.push(normalized);
165                }
166            }
167        }
168        Expr::Reference(expr_ref) => {
169            // eprintln!("DEBUG - Matched Reference: {:#?}", expr_ref);
170            quote! {
171                {
172                    let class_str = #expr_ref;
173                    classes.push(class_str.to_string());
174                }
175            }
176        }
177        // String literals: "text"
178        Expr::Lit(syn::ExprLit {
179            lit: syn::Lit::Str(s),
180            ..
181        }) => {
182            let value = s.value();
183            quote! {
184                classes.push(
185                    (#value).split_whitespace()
186                        .filter(|s| !s.is_empty())
187                        .collect::<Vec<_>>()
188                        .join(" ")
189                );
190            }
191        }
192        // Tuple conditions: (cond, "class")
193        Expr::Tuple(ExprTuple { elems, .. }) if elems.len() == 2 => {
194            let cond = &elems[0];
195            let class = &elems[1];
196            quote! {
197                if #cond {
198                    let class = #class.to_string();
199                    if !class.is_empty() { classes.push(class); }
200                }
201            }
202        }
203        // Ternary expressions: cond ? a : b
204        Expr::If(ExprIf {
205            cond,
206            then_branch,
207            else_branch,
208            ..
209        }) => {
210            if let Some((_, else_expr)) = else_branch {
211                quote! {
212                    {
213                        let value = if #cond {
214                            #then_branch
215                        } else {
216                            #else_expr
217                        };
218                        let class = value.to_string();
219                        if !class.is_empty() {
220                            classes.push(class);
221                        }
222                    }
223                }
224            } else {
225                // Handle cases without else branch
226                quote! {
227                    if #cond {
228                        let class = #then_branch.to_string();
229                        if !class.is_empty() {
230                            classes.push(class);
231                        }
232                    }
233                }
234            }
235        }
236        // Block expressions: if x { ... }
237        Expr::Block(ExprBlock { block, .. }) => {
238            quote! {
239                {
240                    let result = #block;
241                    if let Some(class) = result {
242                        let class = class.to_string();
243                        if !class.is_empty() { classes.push(class); }
244                    }
245                }
246            }
247        }
248        // Triple tuple conditions: (cond, true_value, false_value)
249        Expr::Tuple(ExprTuple { elems, .. }) if elems.len() == 3 => {
250            let cond = &elems[0];
251            let true_val = &elems[1];
252            let false_val = &elems[2];
253            quote! {
254                {
255                    let class = if #cond { #true_val } else { #false_val };
256                    let class = class.to_string();
257                    if !class.is_empty() { classes.push(class); }
258                }
259            }
260        }
261        // Other expressions (variables, function calls, etc.)
262        _ => {
263            // eprintln!("DEBUG - Matched Other: {:#?}", expr);
264            quote! {
265                {
266                    let class = #expr.to_string();
267                    if !class.is_empty() { classes.push(class); }
268                }
269            }
270        }
271    }
272}
273
274/// Conditional class name selection macro for dynamically choosing different class names based on conditions
275///
276/// # Description
277/// - Accepts a conditional expression and two class name values
278/// - Returns the corresponding class name based on whether the condition is true or false
279/// - Automatically handles excess whitespace in class names
280/// - Can be combined with other class name macros
281///
282/// # Parameters
283/// - `condition`: Any expression that evaluates to a boolean value
284/// - `true_value`: Class name returned when condition is true
285/// - `false_value`: Class name returned when condition is false
286///
287/// # Examples
288///
289/// ### Basic usage:
290/// ```rust
291/// use classnames_rs::choose;
292///
293/// let is_active = true;
294/// let class = choose!(is_active, "active", "inactive");
295/// assert_eq!(class, "active");
296/// ```
297///
298/// ### Combined with classnames!:
299/// ```rust
300/// use classnames_rs::{classnames, choose};
301///
302/// let is_primary = true;
303/// let result = classnames!(
304///     "btn",
305///     choose!(is_primary, "btn-primary", "btn-secondary")
306/// );
307/// assert_eq!(result, "btn btn-primary");
308/// ```
309///
310/// ### Complex condition evaluation:
311/// ```rust
312/// use classnames_rs::{classnames, choose};
313///
314/// let score = 85;
315/// let result = classnames!(
316///     "grade",
317///     choose!(score >= 80, "excellent", "normal")
318/// );
319/// assert_eq!(result, "grade excellent");
320/// ```
321///
322/// ### Nested usage:
323/// ```rust
324/// use classnames_rs::{classnames, choose};
325///
326/// let is_dark = true;
327/// let is_active = false;
328/// let result = classnames!(
329///     "theme",
330///     choose!(is_dark, "dark", "light"),
331///     choose!(is_active, "active", "inactive")
332/// );
333/// assert_eq!(result, "theme dark inactive");
334/// ```
335#[proc_macro]
336pub fn choose(input: TokenStream) -> TokenStream {
337    let input = parse_macro_input!(input as ClassNamesInput);
338    let exprs: Vec<_> = input.exprs.into_iter().collect();
339
340    if exprs.len() != 3 {
341        return syn::Error::new(
342            proc_macro2::Span::call_site(),
343            "choose! macro requires exactly three arguments: condition, true_value, false_value",
344        )
345        .to_compile_error()
346        .into();
347    }
348
349    let cond = &exprs[0];
350    let true_val = &exprs[1];
351    let false_val = &exprs[2];
352
353    // Wrap the result in a string expression
354    quote! {
355        ({
356            let result = if #cond {
357                let raw = #true_val.to_string();
358                raw.split_whitespace()
359                    .filter(|s| !s.is_empty())
360                    .collect::<Vec<_>>()
361                    .join(" ")
362            } else {
363                let raw = #false_val.to_string();
364                raw.split_whitespace()
365                    .filter(|s| !s.is_empty())
366                    .collect::<Vec<_>>()
367                    .join(" ")
368            };
369            result
370        })
371    }
372    .into()
373}
374
375/// Helper macro for handling optional types
376///
377/// # Examples
378/// ```rust
379/// use classnames_rs::{classnames, maybe};
380///
381/// let optional_class: Option<&str> = Some("highlight");
382/// let result = classnames!(
383///     "base",
384///     maybe!(optional_class)
385/// );
386/// assert_eq!(result, "base highlight");
387///
388/// let no_class: Option<&str> = None;
389/// let result = classnames!(
390///     "base",
391///     maybe!(no_class)
392/// );
393/// assert_eq!(result, "base");
394/// ```
395#[proc_macro]
396pub fn maybe(input: TokenStream) -> TokenStream {
397    let input = parse_macro_input!(input as ClassNamesInput);
398    let exprs: Vec<_> = input.exprs.into_iter().collect();
399
400    if exprs.len() != 1 {
401        return syn::Error::new(
402            proc_macro2::Span::call_site(),
403            "maybe! macro requires exactly one argument",
404        )
405        .to_compile_error()
406        .into();
407    }
408
409    let value = &exprs[0];
410    quote! {
411        ({
412            match #value {
413                Some(value) => {
414                    let raw = value.to_string();
415                    raw.split_whitespace()
416                        .filter(|s| !s.is_empty())
417                        .collect::<Vec<_>>()
418                        .join(" ")
419                },
420                None => String::new()
421            }
422        })
423    }
424    .into()
425}
426
427/// Conditional helper macro for cleaner syntax
428///
429/// # Examples
430/// ```rust
431/// use classnames_rs::{classnames, when};
432///
433/// let is_active = true;
434/// let result = classnames!(
435///     "btn",
436///     when!(is_active, "active")  // More concise syntax
437/// );
438/// assert_eq!(result, "btn active");
439///
440/// let is_disabled = false;
441/// let result = classnames!(
442///     "btn",
443///     when!(is_disabled, "disabled")
444/// );
445/// assert_eq!(result, "btn");
446/// ```
447#[proc_macro]
448pub fn when(input: TokenStream) -> TokenStream {
449    let input = parse_macro_input!(input as ClassNamesInput);
450    let exprs: Vec<_> = input.exprs.into_iter().collect();
451
452    if exprs.len() != 2 {
453        return syn::Error::new(
454            proc_macro2::Span::call_site(),
455            "when! macro requires exactly two arguments: condition and value",
456        )
457        .to_compile_error()
458        .into();
459    }
460
461    let cond = &exprs[0];
462    let value = &exprs[1];
463
464    quote! {
465        ({
466            if #cond {
467                let raw = #value.to_string();
468                raw.split_whitespace()
469                    .filter(|s| !s.is_empty())
470                    .collect::<Vec<_>>()
471                    .join(" ")
472            } else {
473                String::new()
474            }
475        })
476    }
477    .into()
478}
479
480/// Public macro for formatting class names and normalizing whitespace
481///
482/// # Examples
483/// ```rust
484/// use classnames_rs::pretty_classname;
485///
486/// let messy = "class1   class2\n\t  class3";
487/// assert_eq!(pretty_classname!(messy), "class1 class2 class3");
488///
489/// let with_tabs = "\tprimary\t\tsecondary\t";
490/// assert_eq!(pretty_classname!(with_tabs), "primary secondary");
491/// ```
492#[proc_macro]
493pub fn pretty_classname(input: TokenStream) -> TokenStream {
494    let input = parse_macro_input!(input as ClassNamesInput);
495    let expr = &input.exprs[0];
496
497    quote! {
498        {
499            let raw = #expr.to_string();
500            raw.split_whitespace()
501                .filter(|s| !s.is_empty())
502                .collect::<Vec<_>>()
503                .join(" ")
504        }
505    }
506    .into()
507}