tailwind_rs_postcss/
import_processor.rs

1//! @import Processing System
2//!
3//! This module provides comprehensive @import statement processing functionality
4//! for CSS imports, dependency resolution, and import optimization.
5
6use regex::Regex;
7use std::collections::{HashMap, HashSet};
8use std::path::Path;
9use thiserror::Error;
10
11/// Main import processor for handling @import statements
12pub struct ImportProcessor {
13    resolver: ImportResolver,
14    cache: ImportCache,
15    config: ImportConfig,
16    dependency_graph: DependencyGraph,
17}
18
19impl ImportProcessor {
20    /// Create a new import processor with default configuration
21    pub fn new() -> Self {
22        Self::with_config(ImportConfig::default())
23    }
24
25    /// Create a new import processor with custom configuration
26    pub fn with_config(config: ImportConfig) -> Self {
27        Self {
28            resolver: ImportResolver::new(config.clone()),
29            cache: ImportCache::new(),
30            config,
31            dependency_graph: DependencyGraph::new(),
32        }
33    }
34
35    /// Process @import statements in CSS
36    pub fn process_imports(&mut self, css: &str, base_path: &str) -> Result<String, ImportError> {
37        let start_time = std::time::Instant::now();
38        let mut processed_css = String::new();
39        let mut processed_imports = Vec::new();
40        let mut dependencies = Vec::new();
41
42        for line in css.lines() {
43            if line.trim().starts_with("@import") {
44                let import_statement = self.parse_import_statement(line)?;
45                let resolved_path = self
46                    .resolver
47                    .resolve(&import_statement.import_path, base_path)?;
48
49                // Check for circular dependencies
50                if let Err(circular) = self
51                    .dependency_graph
52                    .add_dependency(base_path, &resolved_path)
53                {
54                    match self.config.handle_circular {
55                        CircularHandling::Error => return Err(circular),
56                        CircularHandling::Warn => {
57                            eprintln!("Warning: Circular dependency detected: {}", resolved_path);
58                        }
59                        CircularHandling::Ignore => {}
60                    }
61                }
62
63                // Get import content
64                let content = self
65                    .resolver
66                    .get_import_content(&import_statement.import_path, base_path)?;
67
68                // Recursively process imports if enabled
69                let processed_content = if self.config.inline_imports {
70                    self.process_imports(&content, &resolved_path)?
71                } else {
72                    content
73                };
74
75                // Wrap in media query if needed
76                let final_content = if let Some(media) = &import_statement.media_query {
77                    format!("@media {} {{\n{}\n}}", media, processed_content)
78                } else {
79                    processed_content
80                };
81
82                processed_css.push_str(&final_content);
83                processed_css.push('\n');
84
85                let content_size = final_content.len();
86                processed_imports.push(ImportInfo {
87                    original_path: import_statement.import_path.clone(),
88                    resolved_path: resolved_path.clone(),
89                    content: final_content.clone(),
90                    size: content_size,
91                    processed: true,
92                });
93
94                dependencies.push(resolved_path);
95            } else {
96                processed_css.push_str(line);
97                processed_css.push('\n');
98            }
99        }
100
101        let _processing_time = start_time.elapsed();
102
103        // Cache the result
104        self.cache
105            .cache_content(base_path.to_string(), processed_css.clone());
106
107        Ok(processed_css)
108    }
109
110    /// Process imports with advanced options
111    pub fn process_imports_advanced(
112        &mut self,
113        css: &str,
114        options: &ImportOptions,
115    ) -> Result<ImportResult, ImportError> {
116        let start_time = std::time::Instant::now();
117        let mut processed_css = String::new();
118        let mut processed_imports = Vec::new();
119        let mut dependencies = Vec::new();
120        let mut circular_dependencies = Vec::new();
121
122        for line in css.lines() {
123            if line.trim().starts_with("@import") {
124                let import_statement = self.parse_import_statement(line)?;
125                let resolved_path = self
126                    .resolver
127                    .resolve(&import_statement.import_path, &options.search_paths[0])?;
128
129                // Check for circular dependencies
130                if let Err(circular) = self
131                    .dependency_graph
132                    .add_dependency(&options.search_paths[0], &resolved_path)
133                {
134                    circular_dependencies.push(circular.to_string());
135                    match options.handle_circular {
136                        CircularHandling::Error => return Err(circular),
137                        CircularHandling::Warn => {
138                            eprintln!("Warning: Circular dependency detected: {}", resolved_path);
139                        }
140                        CircularHandling::Ignore => {}
141                    }
142                }
143
144                // Get import content
145                let content = self
146                    .resolver
147                    .get_import_content(&import_statement.import_path, &options.search_paths[0])?;
148
149                // Recursively process imports if enabled
150                let processed_content = if options.inline_imports {
151                    self.process_imports(&content, &resolved_path)?
152                } else {
153                    content
154                };
155
156                // Wrap in media query if needed
157                let final_content = if let Some(media) = &import_statement.media_query {
158                    format!("@media {} {{\n{}\n}}", media, processed_content)
159                } else {
160                    processed_content
161                };
162
163                processed_css.push_str(&final_content);
164                processed_css.push('\n');
165
166                let content_size = final_content.len();
167                processed_imports.push(ImportInfo {
168                    original_path: import_statement.import_path.clone(),
169                    resolved_path: resolved_path.clone(),
170                    content: final_content.clone(),
171                    size: content_size,
172                    processed: true,
173                });
174
175                dependencies.push(resolved_path);
176            } else {
177                processed_css.push_str(line);
178                processed_css.push('\n');
179            }
180        }
181
182        let processing_time = start_time.elapsed();
183        let total_imports = processed_imports.len();
184        let processed_imports_count = processed_imports.iter().filter(|i| i.processed).count();
185        let skipped_imports = total_imports - processed_imports_count;
186        let total_size = processed_css.len();
187
188        Ok(ImportResult {
189            processed_css,
190            imports_processed: processed_imports,
191            dependencies,
192            circular_dependencies,
193            statistics: ImportStatistics {
194                total_imports,
195                processed_imports: processed_imports_count,
196                skipped_imports,
197                total_size,
198                processing_time,
199            },
200        })
201    }
202
203    /// Resolve import path
204    pub fn resolve_import_path(
205        &self,
206        import_path: &str,
207        base_path: &str,
208    ) -> Result<String, ImportError> {
209        self.resolver.resolve(import_path, base_path)
210    }
211
212    /// Parse import statement from CSS line
213    fn parse_import_statement(&self, line: &str) -> Result<ImportStatement, ImportError> {
214        let import_pattern =
215            Regex::new(r#"@import\s+(?:url\()?["']?([^"')]+)["']?\)?(?:\s+([^;]+))?;"#).unwrap();
216
217        if let Some(cap) = import_pattern.captures(line) {
218            let import_path = cap.get(1).unwrap().as_str().to_string();
219            let media_query = cap.get(2).map(|m| m.as_str().to_string());
220
221            Ok(ImportStatement {
222                line_number: 0, // Will be set by caller
223                import_path,
224                media_query,
225            })
226        } else {
227            Err(ImportError::InvalidImportStatement {
228                line: line.to_string(),
229            })
230        }
231    }
232
233    /// Extract media query from import line
234    fn extract_media_query(&self, line: &str) -> Option<String> {
235        let media_pattern = Regex::new(r#"@import\s+[^;]+;\s*(.+)"#).unwrap();
236        if let Some(cap) = media_pattern.captures(line) {
237            Some(cap.get(1).unwrap().as_str().to_string())
238        } else {
239            None
240        }
241    }
242
243    /// Optimize imports by removing duplicates and unused imports
244    pub fn optimize_imports(&self, css: &str) -> Result<String, ImportError> {
245        let mut optimized_css = String::new();
246        let mut seen_imports = HashSet::new();
247
248        for line in css.lines() {
249            if line.trim().starts_with("@import") {
250                let import_statement = self.parse_import_statement(line)?;
251                if !seen_imports.contains(&import_statement.import_path) {
252                    seen_imports.insert(import_statement.import_path);
253                    optimized_css.push_str(line);
254                    optimized_css.push('\n');
255                }
256            } else {
257                optimized_css.push_str(line);
258                optimized_css.push('\n');
259            }
260        }
261
262        Ok(optimized_css)
263    }
264}
265
266/// Import resolver for handling path resolution
267pub struct ImportResolver {
268    search_paths: Vec<String>,
269    extensions: Vec<String>,
270    config: ResolverConfig,
271}
272
273impl ImportResolver {
274    /// Create new import resolver
275    pub fn new(config: ImportConfig) -> Self {
276        Self {
277            search_paths: config.search_paths.clone(),
278            extensions: config.extensions.clone(),
279            config: ResolverConfig::default(),
280        }
281    }
282
283    /// Resolve import path to file path
284    pub fn resolve(&self, import_path: &str, base_path: &str) -> Result<String, ImportError> {
285        // Handle different import path types
286        if import_path.starts_with("http://") || import_path.starts_with("https://") {
287            return Ok(import_path.to_string()); // External URL
288        }
289
290        if import_path.starts_with("//") {
291            return Ok(format!("https:{}", import_path)); // Protocol-relative URL
292        }
293
294        if import_path.starts_with('/') {
295            // Absolute path
296            return Ok(import_path.to_string());
297        }
298
299        // Relative path
300        let base_dir =
301            Path::new(base_path)
302                .parent()
303                .ok_or_else(|| ImportError::InvalidBasePath {
304                    path: base_path.to_string(),
305                })?;
306
307        let resolved_path = base_dir.join(import_path);
308
309        // Try different extensions
310        for ext in &self.extensions {
311            let path_with_ext = format!("{}{}", resolved_path.display(), ext);
312            if Path::new(&path_with_ext).exists() {
313                return Ok(path_with_ext);
314            }
315        }
316
317        // Try without extension
318        if Path::new(&resolved_path).exists() {
319            return Ok(resolved_path.to_string_lossy().to_string());
320        }
321
322        // Try search paths
323        for search_path in &self.search_paths {
324            let full_path = Path::new(search_path).join(import_path);
325            if Path::new(&full_path).exists() {
326                return Ok(full_path.to_string_lossy().to_string());
327            }
328        }
329
330        Err(ImportError::ImportNotFound {
331            path: import_path.to_string(),
332        })
333    }
334
335    /// Check if import exists
336    pub fn import_exists(&self, import_path: &str, base_path: &str) -> bool {
337        self.resolve(import_path, base_path).is_ok()
338    }
339
340    /// Get import content
341    pub fn get_import_content(
342        &self,
343        import_path: &str,
344        base_path: &str,
345    ) -> Result<String, ImportError> {
346        let resolved_path = self.resolve(import_path, base_path)?;
347        std::fs::read_to_string(&resolved_path).map_err(|_| ImportError::FileReadError {
348            path: resolved_path,
349        })
350    }
351}
352
353/// Dependency graph for tracking import dependencies
354pub struct DependencyGraph {
355    nodes: HashMap<String, ImportNode>,
356    edges: HashMap<String, Vec<String>>,
357    visited: HashSet<String>,
358}
359
360impl DependencyGraph {
361    /// Create new dependency graph
362    pub fn new() -> Self {
363        Self {
364            nodes: HashMap::new(),
365            edges: HashMap::new(),
366            visited: HashSet::new(),
367        }
368    }
369
370    /// Add import dependency
371    pub fn add_dependency(&mut self, from: &str, to: &str) -> Result<(), ImportError> {
372        // Check for circular dependency
373        if self.has_circular_dependency(to, from) {
374            return Err(ImportError::CircularDependency {
375                path: to.to_string(),
376            });
377        }
378
379        self.edges
380            .entry(from.to_string())
381            .or_insert_with(Vec::new)
382            .push(to.to_string());
383
384        Ok(())
385    }
386
387    /// Check for circular dependencies
388    pub fn has_circular_dependency(&self, start: &str, target: &str) -> bool {
389        let mut visited = HashSet::new();
390        let mut recursion_stack = HashSet::new();
391
392        self.dfs_circular_detection(start, target, &mut visited, &mut recursion_stack)
393    }
394
395    /// DFS for circular dependency detection
396    fn dfs_circular_detection(
397        &self,
398        node: &str,
399        target: &str,
400        visited: &mut HashSet<String>,
401        recursion_stack: &mut HashSet<String>,
402    ) -> bool {
403        if node == target {
404            return true;
405        }
406
407        if recursion_stack.contains(node) {
408            return false;
409        }
410
411        if visited.contains(node) {
412            return false;
413        }
414
415        visited.insert(node.to_string());
416        recursion_stack.insert(node.to_string());
417
418        if let Some(dependencies) = self.edges.get(node) {
419            for dependency in dependencies {
420                if self.dfs_circular_detection(dependency, target, visited, recursion_stack) {
421                    return true;
422                }
423            }
424        }
425
426        recursion_stack.remove(node);
427        false
428    }
429
430    /// Get import order (topological sort)
431    pub fn get_import_order(&self, start: &str) -> Result<Vec<String>, ImportError> {
432        let mut visited = HashSet::new();
433        let mut recursion_stack = HashSet::new();
434        let mut order = Vec::new();
435
436        self.dfs_topological_sort(start, &mut visited, &mut recursion_stack, &mut order)?;
437
438        Ok(order)
439    }
440
441    /// DFS for topological sort
442    fn dfs_topological_sort(
443        &self,
444        node: &str,
445        visited: &mut HashSet<String>,
446        recursion_stack: &mut HashSet<String>,
447        order: &mut Vec<String>,
448    ) -> Result<(), ImportError> {
449        if recursion_stack.contains(node) {
450            return Err(ImportError::CircularDependency {
451                path: node.to_string(),
452            });
453        }
454
455        if visited.contains(node) {
456            return Ok(());
457        }
458
459        visited.insert(node.to_string());
460        recursion_stack.insert(node.to_string());
461
462        if let Some(dependencies) = self.edges.get(node) {
463            for dependency in dependencies {
464                self.dfs_topological_sort(dependency, visited, recursion_stack, order)?;
465            }
466        }
467
468        recursion_stack.remove(node);
469        order.push(node.to_string());
470        Ok(())
471    }
472}
473
474/// Import cache for performance optimization
475pub struct ImportCache {
476    file_cache: HashMap<String, String>,
477    dependency_cache: HashMap<String, Vec<String>>,
478    processed_cache: HashMap<String, String>,
479}
480
481impl ImportCache {
482    /// Create new import cache
483    pub fn new() -> Self {
484        Self {
485            file_cache: HashMap::new(),
486            dependency_cache: HashMap::new(),
487            processed_cache: HashMap::new(),
488        }
489    }
490
491    /// Get cached content
492    pub fn get_cached_content(&self, path: &str) -> Option<&String> {
493        self.file_cache.get(path)
494    }
495
496    /// Cache content
497    pub fn cache_content(&mut self, path: String, content: String) {
498        self.file_cache.insert(path, content);
499    }
500
501    /// Get cached dependencies
502    pub fn get_cached_dependencies(&self, path: &str) -> Option<&Vec<String>> {
503        self.dependency_cache.get(path)
504    }
505
506    /// Cache dependencies
507    pub fn cache_dependencies(&mut self, path: String, dependencies: Vec<String>) {
508        self.dependency_cache.insert(path, dependencies);
509    }
510
511    /// Get cached processed content
512    pub fn get_cached_processed(&self, path: &str) -> Option<&String> {
513        self.processed_cache.get(path)
514    }
515
516    /// Cache processed content
517    pub fn cache_processed(&mut self, path: String, content: String) {
518        self.processed_cache.insert(path, content);
519    }
520}
521
522/// Import node for dependency graph
523#[derive(Debug, Clone)]
524pub struct ImportNode {
525    pub path: String,
526    pub dependencies: Vec<String>,
527    pub processed: bool,
528}
529
530/// Import statement representation
531#[derive(Debug, Clone)]
532pub struct ImportStatement {
533    pub line_number: usize,
534    pub import_path: String,
535    pub media_query: Option<String>,
536}
537
538/// Configuration for import processing
539#[derive(Debug, Clone)]
540pub struct ImportConfig {
541    pub search_paths: Vec<String>,
542    pub extensions: Vec<String>,
543    pub inline_imports: bool,
544    pub preserve_imports: bool,
545    pub optimize_imports: bool,
546    pub handle_circular: CircularHandling,
547    pub max_depth: usize,
548}
549
550impl Default for ImportConfig {
551    fn default() -> Self {
552        Self {
553            search_paths: vec![".".to_string()],
554            extensions: vec![".css".to_string(), ".scss".to_string(), ".sass".to_string()],
555            inline_imports: true,
556            preserve_imports: false,
557            optimize_imports: true,
558            handle_circular: CircularHandling::Warn,
559            max_depth: 10,
560        }
561    }
562}
563
564/// Circular dependency handling strategy
565#[derive(Debug, Clone)]
566pub enum CircularHandling {
567    Error,
568    Warn,
569    Ignore,
570}
571
572/// Advanced import processing options
573#[derive(Debug, Clone)]
574pub struct ImportOptions {
575    pub search_paths: Vec<String>,
576    pub extensions: Vec<String>,
577    pub inline_imports: bool,
578    pub preserve_imports: bool,
579    pub optimize_imports: bool,
580    pub handle_circular: CircularHandling,
581    pub max_depth: usize,
582    pub source_map: bool,
583}
584
585/// Result of import processing
586#[derive(Debug, Clone)]
587pub struct ImportResult {
588    pub processed_css: String,
589    pub imports_processed: Vec<ImportInfo>,
590    pub dependencies: Vec<String>,
591    pub circular_dependencies: Vec<String>,
592    pub statistics: ImportStatistics,
593}
594
595/// Information about processed import
596#[derive(Debug, Clone)]
597pub struct ImportInfo {
598    pub original_path: String,
599    pub resolved_path: String,
600    pub content: String,
601    pub size: usize,
602    pub processed: bool,
603}
604
605/// Statistics for import processing
606#[derive(Debug, Clone)]
607pub struct ImportStatistics {
608    pub total_imports: usize,
609    pub processed_imports: usize,
610    pub skipped_imports: usize,
611    pub total_size: usize,
612    pub processing_time: std::time::Duration,
613}
614
615/// Resolver configuration
616#[derive(Debug, Clone)]
617pub struct ResolverConfig {
618    pub follow_symlinks: bool,
619    pub case_sensitive: bool,
620    pub allow_external: bool,
621}
622
623impl Default for ResolverConfig {
624    fn default() -> Self {
625        Self {
626            follow_symlinks: true,
627            case_sensitive: false,
628            allow_external: true,
629        }
630    }
631}
632
633/// Error types for import processing
634#[derive(Debug, Error)]
635pub enum ImportError {
636    #[error("Import not found: {path}")]
637    ImportNotFound { path: String },
638
639    #[error("Circular dependency detected: {path}")]
640    CircularDependency { path: String },
641
642    #[error("Invalid base path: {path}")]
643    InvalidBasePath { path: String },
644
645    #[error("Import depth exceeded: {depth}")]
646    ImportDepthExceeded { depth: usize },
647
648    #[error("Failed to read import file: {path}")]
649    FileReadError { path: String },
650
651    #[error("Invalid import statement: {line}")]
652    InvalidImportStatement { line: String },
653}
654
655#[cfg(test)]
656mod tests {
657    use super::*;
658
659    #[test]
660    fn test_basic_import_processing() {
661        let mut processor = ImportProcessor::new();
662        let css = "@import 'styles.css';";
663        let result = processor.process_imports(css, "/path/to/base");
664        // This will fail in test environment due to missing files, but tests the structure
665        assert!(result.is_err());
666    }
667
668    #[test]
669    fn test_import_statement_parsing() {
670        let processor = ImportProcessor::new();
671        let line = "@import 'styles.css' screen and (max-width: 768px);";
672        let statement = processor.parse_import_statement(line);
673        assert!(statement.is_ok());
674
675        let statement = statement.unwrap();
676        assert_eq!(statement.import_path, "styles.css");
677        assert_eq!(
678            statement.media_query,
679            Some("screen and (max-width: 768px)".to_string())
680        );
681    }
682
683    #[test]
684    fn test_dependency_graph() {
685        let mut graph = DependencyGraph::new();
686
687        // Add dependencies
688        assert!(graph.add_dependency("a.css", "b.css").is_ok());
689        assert!(graph.add_dependency("b.css", "c.css").is_ok());
690
691        // Test circular dependency
692        assert!(graph.add_dependency("c.css", "a.css").is_err());
693    }
694
695    #[test]
696    fn test_import_cache() {
697        let mut cache = ImportCache::new();
698        cache.cache_content("test.css".to_string(), "body { color: red; }".to_string());
699
700        let cached = cache.get_cached_content("test.css");
701        assert!(cached.is_some());
702        assert_eq!(cached.unwrap(), "body { color: red; }");
703    }
704
705    #[test]
706    fn test_import_optimization() {
707        let processor = ImportProcessor::new();
708        let css = r#"
709            @import 'styles.css';
710            @import 'styles.css';
711            @import 'components.css';
712            .custom { color: red; }
713        "#;
714
715        let result = processor.optimize_imports(css);
716        assert!(result.is_ok());
717
718        let optimized = result.unwrap();
719        let import_count = optimized.matches("@import").count();
720        assert_eq!(import_count, 2); // Duplicate removed
721    }
722}