tailwind_rs_macros/
lib.rs

1//! # tailwind-rs-macros
2//!
3//! Procedural macros for the tailwind-rs library.
4//! This crate provides macros for generating type-safe Tailwind CSS classes.
5
6use proc_macro::TokenStream;
7use quote::quote;
8use syn::{parse::ParseStream, parse_macro_input, LitStr, Token};
9
10/// The main `classes!` macro for generating Tailwind CSS classes
11///
12/// # Examples
13///
14/// ```rust
15/// use tailwind_rs_macros::classes;
16///
17/// let classes = classes! {
18///     base: "bg-blue-500 text-white",
19///     variant: "px-4 py-2",
20///     responsive: "sm:text-sm md:text-base",
21///     state: "hover:bg-blue-600 focus:ring-2"
22/// };
23/// ```
24#[proc_macro]
25pub fn classes(input: TokenStream) -> TokenStream {
26    let input = parse_macro_input!(input as ClassesMacro);
27
28    let class_set = input.to_class_set();
29    let class_string = class_set.to_css_classes();
30
31    quote! {
32        #class_string
33    }
34    .into()
35}
36
37/// The `responsive!` macro for generating responsive classes
38///
39/// # Examples
40///
41/// ```rust
42/// use tailwind_rs_macros::responsive;
43///
44/// let responsive_classes = responsive! {
45///     base: "text-sm",
46///     sm: "text-base",
47///     md: "text-lg",
48///     lg: "text-xl"
49/// };
50/// ```
51#[proc_macro]
52pub fn responsive(input: TokenStream) -> TokenStream {
53    let input = parse_macro_input!(input as ResponsiveMacro);
54
55    let class_set = input.to_class_set();
56    let class_string = class_set.to_css_classes();
57
58    quote! {
59        #class_string
60    }
61    .into()
62}
63
64/// The `theme!` macro for generating theme-based classes
65///
66/// # Examples
67///
68/// ```rust
69/// use tailwind_rs_macros::theme;
70///
71/// let theme_classes = theme! {
72///     color: "primary",
73///     spacing: "md",
74///     border_radius: "lg"
75/// };
76/// ```
77#[proc_macro]
78pub fn theme(input: TokenStream) -> TokenStream {
79    let input = parse_macro_input!(input as ThemeMacro);
80
81    let class_set = input.to_class_set();
82    let class_string = class_set.to_css_classes();
83
84    quote! {
85        #class_string
86    }
87    .into()
88}
89
90/// The `component!` macro for generating component classes
91///
92/// # Examples
93///
94/// ```rust
95/// use tailwind_rs_macros::component;
96///
97/// let component_classes = component! {
98///     name: "button",
99///     variant: "primary",
100///     size: "md"
101/// };
102/// ```
103#[proc_macro]
104pub fn component(input: TokenStream) -> TokenStream {
105    let input = parse_macro_input!(input as ComponentMacro);
106
107    let class_set = input.to_class_set();
108    let class_string = class_set.to_css_classes();
109
110    quote! {
111        #class_string
112    }
113    .into()
114}
115
116/// The `state!` macro for generating state-based classes
117///
118/// # Examples
119///
120/// ```rust
121/// use tailwind_rs_macros::state;
122///
123/// let state_classes = state! {
124///     base: "px-4 py-2 rounded-md",
125///     hover: "bg-blue-700",
126///     focus: "ring-2 ring-blue-500",
127///     active: "bg-blue-800"
128/// };
129/// ```
130#[proc_macro]
131pub fn state(input: TokenStream) -> TokenStream {
132    let input = parse_macro_input!(input as StateMacro);
133
134    let class_set = input.to_class_set();
135    let class_string = class_set.to_css_classes();
136
137    quote! {
138        #class_string
139    }
140    .into()
141}
142
143/// The `variant!` macro for generating component variants
144///
145/// # Examples
146///
147/// ```rust
148/// use tailwind_rs_macros::variant;
149///
150/// let variant_classes = variant! {
151///     base: "px-4 py-2 rounded-md font-medium",
152///     primary: "bg-blue-600 text-white hover:bg-blue-700",
153///     secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",
154///     danger: "bg-red-600 text-white hover:bg-red-700"
155/// };
156/// ```
157#[proc_macro]
158pub fn variant(input: TokenStream) -> TokenStream {
159    let input = parse_macro_input!(input as VariantMacro);
160
161    let class_set = input.to_class_set();
162    let class_string = class_set.to_css_classes();
163
164    quote! {
165        #class_string
166    }
167    .into()
168}
169
170/// Parser for the `classes!` macro
171struct ClassesMacro {
172    base: Option<String>,
173    variant: Option<String>,
174    responsive: Option<String>,
175    state: Option<String>,
176    custom: Vec<(String, String)>,
177}
178
179impl syn::parse::Parse for ClassesMacro {
180    fn parse(input: ParseStream) -> syn::Result<Self> {
181        let mut base = None;
182        let mut variant = None;
183        let mut responsive = None;
184        let mut state = None;
185        let mut custom = Vec::new();
186
187        while !input.is_empty() {
188            let key: syn::Ident = input.parse()?;
189            input.parse::<Token![:]>()?;
190            let value: LitStr = input.parse()?;
191
192            match key.to_string().as_str() {
193                "base" => base = Some(value.value()),
194                "variant" => variant = Some(value.value()),
195                "responsive" => responsive = Some(value.value()),
196                "state" => state = Some(value.value()),
197                _ => custom.push((key.to_string(), value.value())),
198            }
199
200            if input.peek(Token![,]) {
201                input.parse::<Token![,]>()?;
202            }
203        }
204
205        Ok(ClassesMacro {
206            base,
207            variant,
208            responsive,
209            state,
210            custom,
211        })
212    }
213}
214
215impl ClassesMacro {
216    fn to_class_set(&self) -> tailwind_rs_core::ClassSet {
217        let mut class_set = tailwind_rs_core::ClassSet::new();
218
219        if let Some(ref base) = self.base {
220            class_set.add_classes(base.split_whitespace().map(|s| s.to_string()));
221        }
222
223        if let Some(ref variant) = self.variant {
224            class_set.add_classes(variant.split_whitespace().map(|s| s.to_string()));
225        }
226
227        if let Some(ref responsive) = self.responsive {
228            class_set.add_classes(responsive.split_whitespace().map(|s| s.to_string()));
229        }
230
231        if let Some(ref state) = self.state {
232            class_set.add_classes(state.split_whitespace().map(|s| s.to_string()));
233        }
234
235        for (key, value) in &self.custom {
236            class_set.add_custom(key.clone(), value.clone());
237        }
238
239        class_set
240    }
241}
242
243/// Parser for the `responsive!` macro
244struct ResponsiveMacro {
245    base: Option<String>,
246    sm: Option<String>,
247    md: Option<String>,
248    lg: Option<String>,
249    xl: Option<String>,
250    xl2: Option<String>,
251}
252
253impl syn::parse::Parse for ResponsiveMacro {
254    fn parse(input: ParseStream) -> syn::Result<Self> {
255        let mut base = None;
256        let mut sm = None;
257        let mut md = None;
258        let mut lg = None;
259        let mut xl = None;
260        let mut xl2 = None;
261
262        while !input.is_empty() {
263            let key: syn::Ident = input.parse()?;
264            input.parse::<Token![:]>()?;
265            let value: LitStr = input.parse()?;
266
267            match key.to_string().as_str() {
268                "base" => base = Some(value.value()),
269                "sm" => sm = Some(value.value()),
270                "md" => md = Some(value.value()),
271                "lg" => lg = Some(value.value()),
272                "xl" => xl = Some(value.value()),
273                "2xl" => xl2 = Some(value.value()),
274                _ => {
275                    return Err(syn::Error::new_spanned(
276                        key,
277                        "Invalid responsive breakpoint",
278                    ));
279                }
280            }
281
282            if input.peek(Token![,]) {
283                input.parse::<Token![,]>()?;
284            }
285        }
286
287        Ok(ResponsiveMacro {
288            base,
289            sm,
290            md,
291            lg,
292            xl,
293            xl2,
294        })
295    }
296}
297
298impl ResponsiveMacro {
299    fn to_class_set(&self) -> tailwind_rs_core::ClassSet {
300        let mut class_set = tailwind_rs_core::ClassSet::new();
301
302        if let Some(ref base) = self.base {
303            class_set.add_classes(base.split_whitespace().map(|s| s.to_string()));
304        }
305
306        if let Some(ref sm) = self.sm {
307            class_set.add_responsive_class(tailwind_rs_core::Breakpoint::Sm, sm.clone());
308        }
309
310        if let Some(ref md) = self.md {
311            class_set.add_responsive_class(tailwind_rs_core::Breakpoint::Md, md.clone());
312        }
313
314        if let Some(ref lg) = self.lg {
315            class_set.add_responsive_class(tailwind_rs_core::Breakpoint::Lg, lg.clone());
316        }
317
318        if let Some(ref xl) = self.xl {
319            class_set.add_responsive_class(tailwind_rs_core::Breakpoint::Xl, xl.clone());
320        }
321
322        if let Some(ref xl2) = self.xl2 {
323            class_set.add_responsive_class(tailwind_rs_core::Breakpoint::Xl2, xl2.clone());
324        }
325
326        class_set
327    }
328}
329
330/// Parser for the `theme!` macro
331struct ThemeMacro {
332    color: Option<String>,
333    spacing: Option<String>,
334    border_radius: Option<String>,
335    box_shadow: Option<String>,
336    custom: Vec<(String, String)>,
337}
338
339impl syn::parse::Parse for ThemeMacro {
340    fn parse(input: ParseStream) -> syn::Result<Self> {
341        let mut color = None;
342        let mut spacing = None;
343        let mut border_radius = None;
344        let mut box_shadow = None;
345        let mut custom = Vec::new();
346
347        while !input.is_empty() {
348            let key: syn::Ident = input.parse()?;
349            input.parse::<Token![:]>()?;
350            let value: LitStr = input.parse()?;
351
352            match key.to_string().as_str() {
353                "color" => color = Some(value.value()),
354                "spacing" => spacing = Some(value.value()),
355                "border_radius" => border_radius = Some(value.value()),
356                "box_shadow" => box_shadow = Some(value.value()),
357                _ => custom.push((key.to_string(), value.value())),
358            }
359
360            if input.peek(Token![,]) {
361                input.parse::<Token![,]>()?;
362            }
363        }
364
365        Ok(ThemeMacro {
366            color,
367            spacing,
368            border_radius,
369            box_shadow,
370            custom,
371        })
372    }
373}
374
375impl ThemeMacro {
376    fn to_class_set(&self) -> tailwind_rs_core::ClassSet {
377        let mut class_set = tailwind_rs_core::ClassSet::new();
378
379        if let Some(ref color) = self.color {
380            class_set.add_class(format!("bg-{}-500", color));
381        }
382
383        if let Some(ref spacing) = self.spacing {
384            class_set.add_class(format!("p-{}", spacing));
385        }
386
387        if let Some(ref border_radius) = self.border_radius {
388            class_set.add_class(format!("rounded-{}", border_radius));
389        }
390
391        if let Some(ref box_shadow) = self.box_shadow {
392            class_set.add_class(format!("shadow-{}", box_shadow));
393        }
394
395        for (key, value) in &self.custom {
396            class_set.add_custom(key.clone(), value.clone());
397        }
398
399        class_set
400    }
401}
402
403/// Parser for the `component!` macro
404struct ComponentMacro {
405    name: Option<String>,
406    variant: Option<String>,
407    size: Option<String>,
408    state: Option<String>,
409    custom: Vec<(String, String)>,
410}
411
412impl syn::parse::Parse for ComponentMacro {
413    fn parse(input: ParseStream) -> syn::Result<Self> {
414        let mut name = None;
415        let mut variant = None;
416        let mut size = None;
417        let mut state = None;
418        let mut custom = Vec::new();
419
420        while !input.is_empty() {
421            let key: syn::Ident = input.parse()?;
422            input.parse::<Token![:]>()?;
423            let value: LitStr = input.parse()?;
424
425            match key.to_string().as_str() {
426                "name" => name = Some(value.value()),
427                "variant" => variant = Some(value.value()),
428                "size" => size = Some(value.value()),
429                "state" => state = Some(value.value()),
430                _ => custom.push((key.to_string(), value.value())),
431            }
432
433            if input.peek(Token![,]) {
434                input.parse::<Token![,]>()?;
435            }
436        }
437
438        Ok(ComponentMacro {
439            name,
440            variant,
441            size,
442            state,
443            custom,
444        })
445    }
446}
447
448impl ComponentMacro {
449    fn to_class_set(&self) -> tailwind_rs_core::ClassSet {
450        let mut class_set = tailwind_rs_core::ClassSet::new();
451
452        // Add base component classes
453        if let Some(ref name) = self.name {
454            class_set.add_class(name.to_string());
455        }
456
457        // Add variant classes
458        if let Some(ref variant) = self.variant {
459            class_set.add_class(format!(
460                "{}-{}",
461                self.name.as_deref().unwrap_or("component"),
462                variant
463            ));
464        }
465
466        // Add size classes
467        if let Some(ref size) = self.size {
468            class_set.add_class(format!(
469                "{}-{}",
470                self.name.as_deref().unwrap_or("component"),
471                size
472            ));
473        }
474
475        // Add state classes
476        if let Some(ref state) = self.state {
477            class_set.add_class(format!(
478                "{}-{}",
479                self.name.as_deref().unwrap_or("component"),
480                state
481            ));
482        }
483
484        for (key, value) in &self.custom {
485            class_set.add_custom(key.clone(), value.clone());
486        }
487
488        class_set
489    }
490}
491
492/// Parser for the `state!` macro
493struct StateMacro {
494    base: Option<String>,
495    hover: Option<String>,
496    focus: Option<String>,
497    active: Option<String>,
498    disabled: Option<String>,
499    custom: Vec<(String, String)>,
500}
501
502impl syn::parse::Parse for StateMacro {
503    fn parse(input: ParseStream) -> syn::Result<Self> {
504        let mut base = None;
505        let mut hover = None;
506        let mut focus = None;
507        let mut active = None;
508        let mut disabled = None;
509        let mut custom = Vec::new();
510
511        while !input.is_empty() {
512            let key: syn::Ident = input.parse()?;
513            input.parse::<Token![:]>()?;
514            let value: LitStr = input.parse()?;
515
516            match key.to_string().as_str() {
517                "base" => base = Some(value.value()),
518                "hover" => hover = Some(value.value()),
519                "focus" => focus = Some(value.value()),
520                "active" => active = Some(value.value()),
521                "disabled" => disabled = Some(value.value()),
522                _ => custom.push((key.to_string(), value.value())),
523            }
524
525            if input.peek(Token![,]) {
526                input.parse::<Token![,]>()?;
527            }
528        }
529
530        Ok(StateMacro {
531            base,
532            hover,
533            focus,
534            active,
535            disabled,
536            custom,
537        })
538    }
539}
540
541impl StateMacro {
542    fn to_class_set(&self) -> tailwind_rs_core::ClassSet {
543        let mut class_set = tailwind_rs_core::ClassSet::new();
544
545        if let Some(ref base) = self.base {
546            class_set.add_classes(base.split_whitespace().map(|s| s.to_string()));
547        }
548
549        if let Some(ref hover) = self.hover {
550            class_set.add_classes(hover.split_whitespace().map(|s| format!("hover:{}", s)));
551        }
552
553        if let Some(ref focus) = self.focus {
554            class_set.add_classes(focus.split_whitespace().map(|s| format!("focus:{}", s)));
555        }
556
557        if let Some(ref active) = self.active {
558            class_set.add_classes(active.split_whitespace().map(|s| format!("active:{}", s)));
559        }
560
561        if let Some(ref disabled) = self.disabled {
562            class_set.add_classes(
563                disabled
564                    .split_whitespace()
565                    .map(|s| format!("disabled:{}", s)),
566            );
567        }
568
569        for (key, value) in &self.custom {
570            class_set.add_custom(key.clone(), value.clone());
571        }
572
573        class_set
574    }
575}
576
577/// Parser for the `variant!` macro
578struct VariantMacro {
579    base: Option<String>,
580    primary: Option<String>,
581    secondary: Option<String>,
582    danger: Option<String>,
583    success: Option<String>,
584    warning: Option<String>,
585    info: Option<String>,
586    custom: Vec<(String, String)>,
587}
588
589impl syn::parse::Parse for VariantMacro {
590    fn parse(input: ParseStream) -> syn::Result<Self> {
591        let mut base = None;
592        let mut primary = None;
593        let mut secondary = None;
594        let mut danger = None;
595        let mut success = None;
596        let mut warning = None;
597        let mut info = None;
598        let mut custom = Vec::new();
599
600        while !input.is_empty() {
601            let key: syn::Ident = input.parse()?;
602            input.parse::<Token![:]>()?;
603            let value: LitStr = input.parse()?;
604
605            match key.to_string().as_str() {
606                "base" => base = Some(value.value()),
607                "primary" => primary = Some(value.value()),
608                "secondary" => secondary = Some(value.value()),
609                "danger" => danger = Some(value.value()),
610                "success" => success = Some(value.value()),
611                "warning" => warning = Some(value.value()),
612                "info" => info = Some(value.value()),
613                _ => custom.push((key.to_string(), value.value())),
614            }
615
616            if input.peek(Token![,]) {
617                input.parse::<Token![,]>()?;
618            }
619        }
620
621        Ok(VariantMacro {
622            base,
623            primary,
624            secondary,
625            danger,
626            success,
627            warning,
628            info,
629            custom,
630        })
631    }
632}
633
634impl VariantMacro {
635    fn to_class_set(&self) -> tailwind_rs_core::ClassSet {
636        let mut class_set = tailwind_rs_core::ClassSet::new();
637
638        if let Some(ref base) = self.base {
639            class_set.add_classes(base.split_whitespace().map(|s| s.to_string()));
640        }
641
642        if let Some(ref primary) = self.primary {
643            class_set.add_classes(primary.split_whitespace().map(|s| s.to_string()));
644        }
645
646        if let Some(ref secondary) = self.secondary {
647            class_set.add_classes(secondary.split_whitespace().map(|s| s.to_string()));
648        }
649
650        if let Some(ref danger) = self.danger {
651            class_set.add_classes(danger.split_whitespace().map(|s| s.to_string()));
652        }
653
654        if let Some(ref success) = self.success {
655            class_set.add_classes(success.split_whitespace().map(|s| s.to_string()));
656        }
657
658        if let Some(ref warning) = self.warning {
659            class_set.add_classes(warning.split_whitespace().map(|s| s.to_string()));
660        }
661
662        if let Some(ref info) = self.info {
663            class_set.add_classes(info.split_whitespace().map(|s| s.to_string()));
664        }
665
666        for (key, value) in &self.custom {
667            class_set.add_custom(key.clone(), value.clone());
668        }
669
670        class_set
671    }
672}
673
674#[cfg(test)]
675mod tests {
676    use super::*;
677
678    #[test]
679    fn test_classes_macro_parser() {
680        let input = quote! {
681            base: "bg-blue-500 text-white",
682            variant: "px-4 py-2",
683            responsive: "sm:text-sm md:text-base",
684            state: "hover:bg-blue-600 focus:ring-2"
685        };
686
687        let parsed = syn::parse2::<ClassesMacro>(input).unwrap();
688        assert_eq!(parsed.base, Some("bg-blue-500 text-white".to_string()));
689        assert_eq!(parsed.variant, Some("px-4 py-2".to_string()));
690        assert_eq!(
691            parsed.responsive,
692            Some("sm:text-sm md:text-base".to_string())
693        );
694        assert_eq!(
695            parsed.state,
696            Some("hover:bg-blue-600 focus:ring-2".to_string())
697        );
698    }
699
700    #[test]
701    fn test_responsive_macro_parser() {
702        let input = quote! {
703            base: "text-sm",
704            sm: "text-base",
705            md: "text-lg",
706            lg: "text-xl"
707        };
708
709        let parsed = syn::parse2::<ResponsiveMacro>(input).unwrap();
710        assert_eq!(parsed.base, Some("text-sm".to_string()));
711        assert_eq!(parsed.sm, Some("text-base".to_string()));
712        assert_eq!(parsed.md, Some("text-lg".to_string()));
713        assert_eq!(parsed.lg, Some("text-xl".to_string()));
714    }
715
716    #[test]
717    fn test_theme_macro_parser() {
718        let input = quote! {
719            color: "primary",
720            spacing: "md",
721            border_radius: "lg"
722        };
723
724        let parsed = syn::parse2::<ThemeMacro>(input).unwrap();
725        assert_eq!(parsed.color, Some("primary".to_string()));
726        assert_eq!(parsed.spacing, Some("md".to_string()));
727        assert_eq!(parsed.border_radius, Some("lg".to_string()));
728    }
729
730    #[test]
731    fn test_component_macro_parser() {
732        let input = quote! {
733            name: "button",
734            variant: "primary",
735            size: "md"
736        };
737
738        let parsed = syn::parse2::<ComponentMacro>(input).unwrap();
739        assert_eq!(parsed.name, Some("button".to_string()));
740        assert_eq!(parsed.variant, Some("primary".to_string()));
741        assert_eq!(parsed.size, Some("md".to_string()));
742    }
743
744    #[test]
745    fn test_state_macro_parser() {
746        let input = quote! {
747            base: "px-4 py-2 rounded-md",
748            hover: "bg-blue-700",
749            focus: "ring-2 ring-blue-500",
750            active: "bg-blue-800"
751        };
752
753        let parsed = syn::parse2::<StateMacro>(input).unwrap();
754        assert_eq!(parsed.base, Some("px-4 py-2 rounded-md".to_string()));
755        assert_eq!(parsed.hover, Some("bg-blue-700".to_string()));
756        assert_eq!(parsed.focus, Some("ring-2 ring-blue-500".to_string()));
757        assert_eq!(parsed.active, Some("bg-blue-800".to_string()));
758    }
759
760    #[test]
761    fn test_variant_macro_parser() {
762        let input = quote! {
763            base: "px-4 py-2 rounded-md font-medium",
764            primary: "bg-blue-600 text-white hover:bg-blue-700",
765            secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",
766            danger: "bg-red-600 text-white hover:bg-red-700"
767        };
768
769        let parsed = syn::parse2::<VariantMacro>(input).unwrap();
770        assert_eq!(
771            parsed.base,
772            Some("px-4 py-2 rounded-md font-medium".to_string())
773        );
774        assert_eq!(
775            parsed.primary,
776            Some("bg-blue-600 text-white hover:bg-blue-700".to_string())
777        );
778        assert_eq!(
779            parsed.secondary,
780            Some("bg-gray-200 text-gray-900 hover:bg-gray-300".to_string())
781        );
782        assert_eq!(
783            parsed.danger,
784            Some("bg-red-600 text-white hover:bg-red-700".to_string())
785        );
786    }
787}