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