1use lightningcss::{
2 stylesheet::{ParserOptions, PrinterOptions, StyleSheet},
3 targets::Browsers,
4};
5use proc_macro::TokenStream;
6use quote::quote;
7use sha2::{Digest, Sha256};
8use std::path::PathBuf;
9use syn::{parse_macro_input, LitStr};
10
11fn generate_class_name(css: &str) -> String {
12 let mut hasher = Sha256::new();
13 hasher.update(css.as_bytes());
14 let hash = hasher.finalize();
15 let hash_str = format!("{:x}", hash);
16 format!("rustyle-{}", &hash_str[..8])
17}
18
19fn parse_css(css: &str) -> Result<StyleSheet<'_, '_>, String> {
20 StyleSheet::parse(
21 css,
22 ParserOptions {
23 filename: "style.css".to_string(),
24 ..Default::default()
25 },
26 )
27 .map_err(|e| format!("CSS parsing error: {:?}", e))
28}
29
30fn scope_css_advanced(css: &str, scope_class: &str) -> String {
33 use regex::Regex;
34
35 let class_re = Regex::new(r"\.([a-zA-Z_-][a-zA-Z0-9_-]*)").unwrap();
37 let scoped = class_re.replace_all(css, |caps: ®ex::Captures| {
38 let class_name = &caps[1];
39 if class_name.starts_with("rustyle-") {
41 format!(".{}", class_name)
42 } else {
43 format!(".{}.{}", scope_class, class_name)
44 }
45 });
46
47 let id_re = Regex::new(r"#([a-zA-Z_-][a-zA-Z0-9_-]*)").unwrap();
49 let scoped = id_re.replace_all(&scoped, |caps: ®ex::Captures| {
50 format!(".{} #{}", scope_class, &caps[1])
51 });
52
53 let element_re = Regex::new(r"^(\s*)([a-zA-Z][a-zA-Z0-9]*)(\s*\{)").unwrap();
56 let scoped = element_re.replace_all(&scoped, |caps: ®ex::Captures| {
57 format!("{}.{} {}{}", &caps[1], scope_class, &caps[2], &caps[3])
58 });
59
60 scoped.to_string()
61}
62
63fn process_css(css: &str, scope_class: &str, should_scope: bool) -> Result<String, String> {
64 let stylesheet = parse_css(css)?;
66
67 let minify = cfg!(not(debug_assertions));
70 let targets = Browsers::default();
71 let printer_options = PrinterOptions {
72 minify,
73 targets: targets.into(),
74 ..Default::default()
75 };
76
77 let parsed_css = stylesheet
78 .to_css(printer_options)
79 .map_err(|e| format!("CSS generation error: {:?}", e))?;
80
81 let final_css = if should_scope {
83 scope_css_advanced(&parsed_css.code, scope_class)
84 } else {
85 parsed_css.code
86 };
87
88 Ok(final_css)
89}
90
91#[proc_macro]
93pub fn style(input: TokenStream) -> TokenStream {
94 let css_lit = parse_macro_input!(input as LitStr);
95 let css = css_lit.value();
96
97 let class_name = generate_class_name(&css);
99
100 let scoped_css = match process_css(&css, &class_name, true) {
102 Ok(scoped) => scoped,
103 Err(e) => {
104 let span = css_lit.span();
106 let error_msg = format_error_with_context(&e, &css, span);
107 return syn::Error::new(span, error_msg).to_compile_error().into();
108 }
109 };
110
111 let expanded = quote! {
113 {
114 static STYLE: &str = #scoped_css;
116 static CLASS: &str = #class_name;
117
118 rustyle::register_style(CLASS, STYLE);
120
121 #[cfg(all(feature = "csr", target_arch = "wasm32"))]
123 {
124 rustyle::csr::inject_styles_csr(STYLE);
125 }
126
127 CLASS
128 }
129 };
130
131 TokenStream::from(expanded)
132}
133
134#[proc_macro]
150pub fn style_signal(input: TokenStream) -> TokenStream {
151 let css_lit = parse_macro_input!(input as LitStr);
152 let css = css_lit.value();
153
154 let (base_css_template, expressions) = extract_reactive_expressions(&css);
156
157 let class_name = generate_class_name(&base_css_template);
159
160 let scoped_base_css = match process_css(&base_css_template, &class_name, true) {
162 Ok(css) => css,
163 Err(e) => {
164 return syn::Error::new(css_lit.span(), format!("Failed to process CSS: {}", e))
165 .to_compile_error()
166 .into();
167 }
168 };
169
170 if expressions.is_empty() {
172 let expanded = quote! {
174 {
175 static CLASS: &str = #class_name;
176 static BASE_CSS: &str = #scoped_base_css;
177
178 rustyle::register_style(CLASS, BASE_CSS);
179
180 #[cfg(all(feature = "csr", target_arch = "wasm32"))]
181 {
182 rustyle::csr::inject_styles_csr(BASE_CSS);
183 }
184
185 CLASS
186 }
187 };
188 return TokenStream::from(expanded);
189 }
190
191 let mut eval_and_replace_code = Vec::new();
196 for (i, expr) in expressions.iter().enumerate() {
197 let placeholder = format!("__RUSTYLE_EXPR_{}__", i);
198 let placeholder_lit = placeholder.clone();
199 let expr_code = &expr.code;
200
201 let var_name = syn::Ident::new(&format!("expr_val_{}", i), proc_macro2::Span::call_site());
203
204 eval_and_replace_code.push(quote! {
205 let #var_name = format!("{}", #expr_code);
206 css = css.replace(#placeholder_lit, &#var_name);
207 });
208 }
209
210 let expanded = quote! {
212 {
213 use leptos::*;
214
215 static CLASS: &str = #class_name;
216 static BASE_CSS_TEMPLATE: &str = #base_css_template;
217
218 let mut css = BASE_CSS_TEMPLATE.to_string();
220 #(#eval_and_replace_code)*
221
222 #[cfg(all(feature = "csr", target_arch = "wasm32"))]
224 {
225 rustyle::reactive::inject_reactive_style(CLASS, &css);
226 }
227
228 rustyle::register_style(CLASS, &css);
230
231 CLASS
232 }
233 };
234
235 TokenStream::from(expanded)
236}
237
238fn extract_reactive_expressions(css: &str) -> (String, Vec<ReactiveExpression>) {
241 use regex::Regex;
242
243 let mut expressions = Vec::new();
244 let mut base_css = css.to_string();
245
246 let expr_re = Regex::new(r"#\{([^}]+)\}").unwrap();
248
249 let mut offset = 0;
250 for cap in expr_re.captures_iter(css) {
251 let full_match = cap.get(0).unwrap();
252 let expr_str = cap.get(1).unwrap().as_str();
253
254 let expr_code = match syn::parse_str::<syn::Expr>(expr_str) {
256 Ok(expr) => expr,
257 Err(_) => {
258 continue;
260 }
261 };
262
263 let placeholder = format!("__RUSTYLE_EXPR_{}__", expressions.len());
264
265 let start = full_match.start() - offset;
267 let end = full_match.end() - offset;
268 base_css.replace_range(start..end, &placeholder);
269 offset += full_match.len() - placeholder.len();
270
271 expressions.push(ReactiveExpression {
272 code: expr_code,
273 placeholder,
274 });
275 }
276
277 (base_css, expressions)
278}
279
280struct ReactiveExpression {
282 code: syn::Expr,
283 placeholder: String,
284}
285
286#[proc_macro]
304pub fn style_with_vars(input: TokenStream) -> TokenStream {
305 let css_lit = parse_macro_input!(input as LitStr);
306 let css = css_lit.value();
307
308 let (variables, regular_css) = parse_css_variables(&css);
310
311 let class_name = generate_class_name(&css);
313
314 let mut full_css = String::new();
316 if !variables.is_empty() {
317 full_css.push_str(&format!(".{} {{\n", class_name));
318 for (var_name, var_value) in &variables {
319 full_css.push_str(&format!(" {}: {};\n", var_name, var_value));
320 }
321 full_css.push_str("}\n\n");
322 }
323
324 full_css.push_str(®ular_css);
326
327 let scoped_css = match process_css(&full_css, &class_name, true) {
329 Ok(css) => css,
330 Err(e) => {
331 return syn::Error::new(
332 css_lit.span(),
333 format!("Failed to process CSS with variables: {}", e),
334 )
335 .to_compile_error()
336 .into();
337 }
338 };
339
340 let expanded = quote! {
342 {
343 static CLASS: &str = #class_name;
344 static STYLE: &str = #scoped_css;
345
346 rustyle::register_style(CLASS, STYLE);
348
349 #[cfg(all(feature = "csr", target_arch = "wasm32"))]
351 {
352 rustyle::csr::inject_styles_csr(STYLE);
353 }
354
355 CLASS
356 }
357 };
358
359 TokenStream::from(expanded)
360}
361
362fn parse_css_variables(css: &str) -> (Vec<(String, String)>, String) {
364 use regex::Regex;
365
366 let mut variables = Vec::new();
367 let mut regular_css = css.to_string();
368
369 let var_re = Regex::new(r"--([a-zA-Z0-9_-]+)\s*:\s*([^;]+);").unwrap();
371
372 let mut offset = 0;
373 for cap in var_re.captures_iter(css) {
374 let full_match = cap.get(0).unwrap();
375 let var_name = format!("--{}", cap.get(1).unwrap().as_str());
376 let var_value = cap.get(2).unwrap().as_str().trim().to_string();
377
378 variables.push((var_name.clone(), var_value));
379
380 let start = full_match.start() - offset;
383 let end = full_match.end() - offset;
384 regular_css.replace_range(start..end, "");
385 offset += full_match.len();
386 }
387
388 (variables, regular_css)
389}
390
391#[proc_macro]
393pub fn global_style(input: TokenStream) -> TokenStream {
394 let css_lit = parse_macro_input!(input as LitStr);
395 let css = css_lit.value();
396
397 let parsed_css = match process_css(&css, "", false) {
399 Ok(css) => css,
400 Err(e) => {
401 return syn::Error::new(css_lit.span(), format!("Failed to parse CSS: {}", e))
402 .to_compile_error()
403 .into();
404 }
405 };
406
407 let expanded = quote! {
409 {
410 static STYLE: &str = #parsed_css;
411
412 rustyle::register_global_style(STYLE);
414
415 #[cfg(all(feature = "csr", target_arch = "wasm32"))]
416 {
417 rustyle::csr::inject_styles_csr(STYLE);
418 }
419
420 ()
421 }
422 };
423
424 TokenStream::from(expanded)
425}
426
427#[proc_macro]
429pub fn keyframes(input: TokenStream) -> TokenStream {
430 let css_lit = parse_macro_input!(input as LitStr);
431 let css = css_lit.value();
432
433 let keyframes_css = match process_css(&css, "", false) {
435 Ok(css) => css,
436 Err(e) => {
437 return syn::Error::new(
438 css_lit.span(),
439 format!("Failed to parse keyframes CSS: {}", e),
440 )
441 .to_compile_error()
442 .into();
443 }
444 };
445
446 let expanded = quote! {
448 {
449 static KEYFRAMES: &str = #keyframes_css;
450
451 rustyle::register_global_style(KEYFRAMES);
453
454 #[cfg(all(feature = "csr", target_arch = "wasm32"))]
455 {
456 rustyle::csr::inject_styles_csr(KEYFRAMES);
457 }
458
459 ()
460 }
461 };
462
463 TokenStream::from(expanded)
464}
465
466#[proc_macro]
468pub fn container_style(input: TokenStream) -> TokenStream {
469 let css_lit = parse_macro_input!(input as LitStr);
470 let css = css_lit.value();
471
472 let container_css = match process_css(&css, "", false) {
474 Ok(css) => css,
475 Err(e) => {
476 return syn::Error::new(
477 css_lit.span(),
478 format!("Failed to parse container query CSS: {}", e),
479 )
480 .to_compile_error()
481 .into();
482 }
483 };
484
485 let expanded = quote! {
487 {
488 static CONTAINER_STYLE: &str = #container_css;
489
490 rustyle::register_global_style(CONTAINER_STYLE);
492
493 #[cfg(all(feature = "csr", target_arch = "wasm32"))]
494 {
495 rustyle::csr::inject_styles_csr(CONTAINER_STYLE);
496 }
497
498 ()
499 }
500 };
501
502 TokenStream::from(expanded)
503}
504
505#[proc_macro]
507pub fn layer_style(input: TokenStream) -> TokenStream {
508 let css_lit = parse_macro_input!(input as LitStr);
509 let css = css_lit.value();
510
511 let layer_css = match process_css(&css, "", false) {
513 Ok(css) => css,
514 Err(e) => {
515 return syn::Error::new(css_lit.span(), format!("Failed to parse layer CSS: {}", e))
516 .to_compile_error()
517 .into();
518 }
519 };
520
521 let expanded = quote! {
523 {
524 static LAYER_STYLE: &str = #layer_css;
525
526 rustyle::register_global_style(LAYER_STYLE);
528
529 #[cfg(all(feature = "csr", target_arch = "wasm32"))]
530 {
531 rustyle::csr::inject_styles_csr(LAYER_STYLE);
532 }
533
534 ()
535 }
536 };
537
538 TokenStream::from(expanded)
539}
540
541#[proc_macro]
543pub fn media_style(input: TokenStream) -> TokenStream {
544 let css_lit = parse_macro_input!(input as LitStr);
545 let css = css_lit.value();
546
547 let media_css = match process_css(&css, "", false) {
549 Ok(css) => css,
550 Err(e) => {
551 return syn::Error::new(
552 css_lit.span(),
553 format!("Failed to parse media query CSS: {}", e),
554 )
555 .to_compile_error()
556 .into();
557 }
558 };
559
560 let expanded = quote! {
562 {
563 static MEDIA_STYLE: &str = #media_css;
564
565 rustyle::register_global_style(MEDIA_STYLE);
567
568 #[cfg(all(feature = "csr", target_arch = "wasm32"))]
569 {
570 rustyle::csr::inject_styles_csr(MEDIA_STYLE);
571 }
572
573 ()
574 }
575 };
576
577 TokenStream::from(expanded)
578}
579
580#[proc_macro]
582pub fn view_transition(input: TokenStream) -> TokenStream {
583 let css_lit = parse_macro_input!(input as LitStr);
584 let css = css_lit.value();
585
586 let transition_css = match process_css(&css, "", false) {
588 Ok(css) => css,
589 Err(e) => {
590 return syn::Error::new(
591 css_lit.span(),
592 format!("Failed to parse view transition CSS: {}", e),
593 )
594 .to_compile_error()
595 .into();
596 }
597 };
598
599 let expanded = quote! {
601 {
602 static TRANSITION: &str = #transition_css;
603
604 rustyle::register_view_transition(TRANSITION);
606
607 #[cfg(all(feature = "csr", target_arch = "wasm32"))]
608 {
609 rustyle::csr::inject_styles_csr(TRANSITION);
610 }
611
612 ()
613 }
614 };
615
616 TokenStream::from(expanded)
617}
618
619#[proc_macro]
621pub fn scope_style(input: TokenStream) -> TokenStream {
622 let css_lit = parse_macro_input!(input as LitStr);
623 let css = css_lit.value();
624
625 let scope_css = match process_css(&css, "", false) {
627 Ok(css) => css,
628 Err(e) => {
629 return syn::Error::new(css_lit.span(), format!("Failed to parse scope CSS: {}", e))
630 .to_compile_error()
631 .into();
632 }
633 };
634
635 let expanded = quote! {
637 {
638 static SCOPE_STYLE: &str = #scope_css;
639
640 rustyle::register_global_style(SCOPE_STYLE);
642
643 #[cfg(all(feature = "csr", target_arch = "wasm32"))]
644 {
645 rustyle::csr::inject_styles_csr(SCOPE_STYLE);
646 }
647
648 ()
649 }
650 };
651
652 TokenStream::from(expanded)
653}
654
655#[proc_macro]
657pub fn starting_style(input: TokenStream) -> TokenStream {
658 let css_lit = parse_macro_input!(input as LitStr);
659 let css = css_lit.value();
660
661 let starting_css = match process_css(&css, "", false) {
663 Ok(css) => css,
664 Err(e) => {
665 return syn::Error::new(
666 css_lit.span(),
667 format!("Failed to parse starting style CSS: {}", e),
668 )
669 .to_compile_error()
670 .into();
671 }
672 };
673
674 let expanded = quote! {
676 {
677 static STARTING_STYLE: &str = #starting_css;
678
679 rustyle::register_global_style(STARTING_STYLE);
681
682 #[cfg(all(feature = "csr", target_arch = "wasm32"))]
683 {
684 rustyle::csr::inject_styles_csr(STARTING_STYLE);
685 }
686
687 ()
688 }
689 };
690
691 TokenStream::from(expanded)
692}
693
694#[proc_macro]
708pub fn css_module(input: TokenStream) -> TokenStream {
709 let path_lit = parse_macro_input!(input as LitStr);
710 let path_str = path_lit.value();
711
712 let file_path = if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
715 let mut path = PathBuf::from(manifest_dir);
716 path.push(&path_str);
717 path
718 } else {
719 PathBuf::from(&path_str)
721 };
722
723 let css_content = match std::fs::read_to_string(&file_path) {
725 Ok(content) => content,
726 Err(e) => {
727 return syn::Error::new(
728 path_lit.span(),
729 format!("Failed to read CSS file {}: {}. Make sure the path is relative to your Cargo.toml or use an absolute path.", path_str, e),
730 )
731 .to_compile_error()
732 .into();
733 }
734 };
735
736 let module_id = generate_class_name(&format!("{}{}", path_str, css_content));
738
739 let class_names = extract_css_classes(&css_content);
741
742 let scoped_class_map: Vec<(String, String)> = class_names
744 .iter()
745 .map(|class| {
746 let scoped = format!("{}-{}", module_id, class);
747 (class.clone(), scoped)
748 })
749 .collect();
750
751 let scoped_css = scope_css_with_class_map(&css_content, &module_id, &scoped_class_map);
753
754 let processed_css = match process_css(&scoped_css, &module_id, false) {
756 Ok(css) => css,
757 Err(e) => {
758 return syn::Error::new(
759 path_lit.span(),
760 format!("Failed to process CSS from {}: {}", path_str, e),
761 )
762 .to_compile_error()
763 .into();
764 }
765 };
766
767 let class_insertions: Vec<_> = scoped_class_map
769 .iter()
770 .map(|(orig, scoped)| {
771 let orig_str = orig.clone();
772 let scoped_str = scoped.clone();
773 quote! {
774 class_names.insert(#orig_str.to_string(), #scoped_str.to_string());
775 }
776 })
777 .collect();
778
779 let expanded = quote! {
780 {
781 use std::collections::HashMap;
782
783 let mut class_names = HashMap::new();
785 #(#class_insertions)*
786
787 let module = rustyle::CssModule {
789 name: #path_str.to_string(),
790 css: #processed_css.to_string(),
791 class_names,
792 module_id: #module_id.to_string(),
793 };
794
795 module.register();
797
798 rustyle::CssModuleClasses::new(module)
800 }
801 };
802
803 TokenStream::from(expanded)
804}
805
806fn extract_css_classes(css: &str) -> Vec<String> {
808 use regex::Regex;
809
810 let class_re = Regex::new(r"\.([a-zA-Z_-][a-zA-Z0-9_-]*)").unwrap();
811 let mut classes = std::collections::HashSet::new();
812
813 for cap in class_re.captures_iter(css) {
814 let class_name = &cap[1];
815 if !class_name.starts_with("rustyle-") {
817 classes.insert(class_name.to_string());
818 }
819 }
820
821 classes.into_iter().collect()
822}
823
824fn format_error_with_context(error: &str, css: &str, _span: proc_macro2::Span) -> String {
826 let mut msg = format!("ā CSS Processing Error: {}\n", error);
827
828 let lines: Vec<&str> = css.lines().take(5).collect();
830 if !lines.is_empty() {
831 msg.push_str("\n\nCode context:\n");
832 for (i, line) in lines.iter().enumerate() {
833 msg.push_str(&format!(" {:4} | {}\n", i + 1, line));
834 }
835 if css.lines().count() > 5 {
836 msg.push_str(" ...\n");
837 }
838 }
839
840 if error.contains("parse") || error.contains("syntax") {
842 msg.push_str("\nš” Common fixes:");
843 msg.push_str("\n - Check for missing semicolons (;)");
844 msg.push_str("\n - Ensure all braces { } are balanced");
845 msg.push_str("\n - Verify property names are spelled correctly");
846 }
847
848 msg.push_str("\n\nFor help, visit: https://github.com/usvx/rustyle");
849 msg
850}
851
852fn scope_css_with_class_map(css: &str, _module_id: &str, class_map: &[(String, String)]) -> String {
854 use regex::Regex;
855
856 let mut scoped = css.to_string();
857
858 for (original, scoped_name) in class_map {
860 let pattern = format!(r"\.({})(?![a-zA-Z0-9_-])", regex::escape(original));
862 if let Ok(re) = Regex::new(&pattern) {
863 scoped = re
864 .replace_all(&scoped, |_caps: ®ex::Captures| {
865 format!(".{}", scoped_name)
866 })
867 .to_string();
868 }
869 }
870
871 scoped
872}