rustyle_macros/
lib.rs

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
30/// Advanced CSS scoping using regex-based transformation
31/// This properly handles all selector types and @rules
32fn scope_css_advanced(css: &str, scope_class: &str) -> String {
33    use regex::Regex;
34
35    // Scope class selectors: .class -> .scope-class.class
36    let class_re = Regex::new(r"\.([a-zA-Z_-][a-zA-Z0-9_-]*)").unwrap();
37    let scoped = class_re.replace_all(css, |caps: &regex::Captures| {
38        let class_name = &caps[1];
39        // Don't double-scope if already scoped
40        if class_name.starts_with("rustyle-") {
41            format!(".{}", class_name)
42        } else {
43            format!(".{}.{}", scope_class, class_name)
44        }
45    });
46
47    // Scope ID selectors: #id -> .scope-class #id (descendant)
48    let id_re = Regex::new(r"#([a-zA-Z_-][a-zA-Z0-9_-]*)").unwrap();
49    let scoped = id_re.replace_all(&scoped, |caps: &regex::Captures| {
50        format!(".{} #{}", scope_class, &caps[1])
51    });
52
53    // Scope element selectors at the start of rules: element -> .scope-class element
54    // But be careful not to scope elements in pseudo-selectors or @rules
55    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: &regex::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    // Parse CSS to validate syntax
65    let stylesheet = parse_css(css)?;
66
67    // Convert back to CSS string first
68    // Minify in release mode for better performance
69    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    // Apply scoping if needed
82    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/// Main style macro for compile-time CSS processing
92#[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    // Generate class name from CSS content
98    let class_name = generate_class_name(&css);
99
100    // Process CSS with proper scoping
101    let scoped_css = match process_css(&css, &class_name, true) {
102        Ok(scoped) => scoped,
103        Err(e) => {
104            // Extract line number from error if possible
105            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    // Generate code that registers the style and returns the class name
112    let expanded = quote! {
113        {
114            // Register style at compile time
115            static STYLE: &str = #scoped_css;
116            static CLASS: &str = #class_name;
117
118            // Register style for SSR
119            rustyle::register_style(CLASS, STYLE);
120
121            // Inject style in CSR mode
122            #[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/// Reactive style macro that supports Leptos signals in CSS values
135///
136/// Usage:
137/// ```rust
138/// let (count, _) = create_signal(cx, 0);
139/// let class = style_signal! {
140///     .counter {
141///         font-size: #{(count() * 2 + 16)}px;
142///         color: #{if count() > 10 { "red" } else { "blue" }};
143///     }
144/// };
145/// ```
146///
147/// The macro parses CSS with embedded Rust expressions in `#{...}` blocks,
148/// extracts signal dependencies, and generates reactive update code.
149#[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    // Parse CSS to extract Rust expressions in #{...} blocks
155    let (base_css_template, expressions) = extract_reactive_expressions(&css);
156
157    // Generate class name from base CSS
158    let class_name = generate_class_name(&base_css_template);
159
160    // Process base CSS (without expressions) for scoping
161    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    // Generate reactive style code
171    if expressions.is_empty() {
172        // No reactive expressions, just use regular style
173        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    // Generate code for reactive style with signal dependencies
192    // The base_css_template already has placeholders from extract_reactive_expressions
193
194    // Generate expression evaluation and replacement code
195    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        // Create unique variable name for each expression
202        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    // Generate the reactive style builder code
211    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            // Build final CSS by evaluating expressions and replacing placeholders
219            let mut css = BASE_CSS_TEMPLATE.to_string();
220            #(#eval_and_replace_code)*
221
222            // Register the reactive style
223            #[cfg(all(feature = "csr", target_arch = "wasm32"))]
224            {
225                rustyle::reactive::inject_reactive_style(CLASS, &css);
226            }
227
228            // Also register for SSR
229            rustyle::register_style(CLASS, &css);
230
231            CLASS
232        }
233    };
234
235    TokenStream::from(expanded)
236}
237
238/// Extract Rust expressions from CSS string
239/// Returns (base_css_with_placeholders, vec_of_expressions)
240fn 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    // Match #{...} blocks
247    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        // Try to parse the expression as Rust code
255        let expr_code = match syn::parse_str::<syn::Expr>(expr_str) {
256            Ok(expr) => expr,
257            Err(_) => {
258                // If parsing fails, treat as string literal
259                continue;
260            }
261        };
262
263        let placeholder = format!("__RUSTYLE_EXPR_{}__", expressions.len());
264
265        // Replace the #{...} with placeholder
266        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
280/// Represents a reactive expression extracted from CSS
281struct ReactiveExpression {
282    code: syn::Expr,
283    placeholder: String,
284}
285
286/// Style macro with CSS variables support
287///
288/// Usage:
289/// ```rust
290/// let class = style_with_vars! {
291///     --primary-color: #007bff;
292///     --spacing: 16px;
293///     
294///     .component {
295///         color: var(--primary-color);
296///         padding: var(--spacing);
297///     }
298/// };
299/// ```
300///
301/// This macro parses CSS variables (--name: value) and regular CSS,
302/// ensuring variables are properly scoped and available for use.
303#[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    // Parse CSS to separate variables from regular CSS
309    let (variables, regular_css) = parse_css_variables(&css);
310
311    // Generate class name
312    let class_name = generate_class_name(&css);
313
314    // Build CSS with variables in :root scope for this component
315    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    // Add the regular CSS (which may reference the variables)
325    full_css.push_str(&regular_css);
326
327    // Process the full CSS with scoping
328    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    // Generate code that registers the style
341    let expanded = quote! {
342        {
343            static CLASS: &str = #class_name;
344            static STYLE: &str = #scoped_css;
345
346            // Register style for SSR
347            rustyle::register_style(CLASS, STYLE);
348
349            // Inject style in CSR mode
350            #[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
362/// Parse CSS to extract variables (--name: value) and separate from regular CSS
363fn 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    // Match CSS variable declarations: --variable-name: value;
370    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        // Remove the variable declaration from regular CSS
381        // (it will be added back in the :root scope)
382        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/// Global style macro for un-scoped styles
392#[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    // Process CSS without scoping
398    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    // Generate code that injects global styles
408    let expanded = quote! {
409        {
410            static STYLE: &str = #parsed_css;
411
412            // Register global style
413            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/// Keyframes macro for @keyframes animations
428#[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    // Process keyframes CSS (no scoping needed)
434    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    // Generate code that registers keyframes
447    let expanded = quote! {
448        {
449            static KEYFRAMES: &str = #keyframes_css;
450
451            // Register keyframes
452            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/// Container query style macro
467#[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    // Process container query CSS
473    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    // Generate code that registers container styles
486    let expanded = quote! {
487        {
488            static CONTAINER_STYLE: &str = #container_css;
489
490            // Register container style
491            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/// Layer style macro for @layer
506#[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    // Process layer CSS
512    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    // Generate code that registers layer styles
522    let expanded = quote! {
523        {
524            static LAYER_STYLE: &str = #layer_css;
525
526            // Register layer style
527            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/// Media query style macro
542#[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    // Process media query CSS
548    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    // Generate code that registers media query styles
561    let expanded = quote! {
562        {
563            static MEDIA_STYLE: &str = #media_css;
564
565            // Register media query style
566            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/// View transition macro
581#[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    // Process view transition CSS
587    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    // Generate code that registers view transition
600    let expanded = quote! {
601        {
602            static TRANSITION: &str = #transition_css;
603
604            // Register view transition
605            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/// @scope rule macro
620#[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    // Process scope CSS
626    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    // Generate code that registers scope style
636    let expanded = quote! {
637        {
638            static SCOPE_STYLE: &str = #scope_css;
639
640            // Register scope style
641            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/// @starting-style macro
656#[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    // Process starting style CSS
662    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    // Generate code that registers starting style
675    let expanded = quote! {
676        {
677            static STARTING_STYLE: &str = #starting_css;
678
679            // Register starting style
680            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/// CSS Module macro for loading CSS files at compile time
695///
696/// Usage:
697/// ```rust
698/// let module = css_module!("styles/button.css");
699/// let button_class = module.class("button");
700/// ```
701///
702/// This macro:
703/// - Reads the CSS file at compile time
704/// - Generates scoped class names
705/// - Creates a type-safe accessor for class names
706/// - Registers the styles automatically
707#[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    // Resolve the file path
713    // First try relative to CARGO_MANIFEST_DIR (works in build scripts)
714    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        // Fallback: try relative to current directory
720        PathBuf::from(&path_str)
721    };
722
723    // Read the CSS file
724    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    // Generate module ID from file path
737    let module_id = generate_class_name(&format!("{}{}", path_str, css_content));
738
739    // Extract class names from CSS
740    let class_names = extract_css_classes(&css_content);
741
742    // Generate scoped class names
743    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    // Scope the CSS
752    let scoped_css = scope_css_with_class_map(&css_content, &module_id, &scoped_class_map);
753
754    // Process CSS through parser
755    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    // Generate code that creates the module and registers styles
768    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            // Create class name map
784            let mut class_names = HashMap::new();
785            #(#class_insertions)*
786
787            // Create CSS module
788            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            // Register the module
796            module.register();
797
798            // Create type-safe accessor
799            rustyle::CssModuleClasses::new(module)
800        }
801    };
802
803    TokenStream::from(expanded)
804}
805
806/// Extract class names from CSS content
807fn 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        // Skip if already scoped
816        if !class_name.starts_with("rustyle-") {
817            classes.insert(class_name.to_string());
818        }
819    }
820
821    classes.into_iter().collect()
822}
823
824/// Format error message with context and suggestions
825fn 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    // Add code context (first few lines of CSS)
829    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    // Add suggestions for common errors
841    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
852/// Scope CSS by replacing class names with scoped versions
853fn 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    // Replace each class selector with its scoped version
859    for (original, scoped_name) in class_map {
860        // Match .original (word boundary to avoid partial matches)
861        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: &regex::Captures| {
865                    format!(".{}", scoped_name)
866                })
867                .to_string();
868        }
869    }
870
871    scoped
872}