1use proc_macro::TokenStream;
7use quote::quote;
8use syn::{parse::ParseStream, parse_macro_input, LitStr, Token};
9
10#[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#[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#[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#[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#[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#[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
170struct 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
243struct 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
330struct 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
403struct 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 if let Some(ref name) = self.name {
454 class_set.add_class(name.to_string());
455 }
456
457 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 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 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
492struct 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
577struct 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}