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}