1use std::collections::{HashMap, HashSet};
7use std::path::Path;
8use regex::Regex;
9use thiserror::Error;
10
11pub struct ImportProcessor {
13 resolver: ImportResolver,
14 cache: ImportCache,
15 config: ImportConfig,
16 dependency_graph: DependencyGraph,
17}
18
19impl ImportProcessor {
20 pub fn new() -> Self {
22 Self::with_config(ImportConfig::default())
23 }
24
25 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 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 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 let content = self.resolver.get_import_content(&import_statement.import_path, base_path)?;
60
61 let processed_content = if self.config.inline_imports {
63 self.process_imports(&content, &resolved_path)?
64 } else {
65 content
66 };
67
68 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 self.cache.cache_content(base_path.to_string(), processed_css.clone());
98
99 Ok(processed_css)
100 }
101
102 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 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 let content = self.resolver.get_import_content(&import_statement.import_path, &options.search_paths[0])?;
129
130 let processed_content = if options.inline_imports {
132 self.process_imports(&content, &resolved_path)?
133 } else {
134 content
135 };
136
137 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 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 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, import_path,
200 media_query,
201 })
202 } else {
203 Err(ImportError::InvalidImportStatement { line: line.to_string() })
204 }
205 }
206
207 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 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
240pub struct ImportResolver {
242 search_paths: Vec<String>,
243 extensions: Vec<String>,
244 config: ResolverConfig,
245}
246
247impl ImportResolver {
248 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 pub fn resolve(&self, import_path: &str, base_path: &str) -> Result<String, ImportError> {
259 if import_path.starts_with("http://") || import_path.starts_with("https://") {
261 return Ok(import_path.to_string()); }
263
264 if import_path.starts_with("//") {
265 return Ok(format!("https:{}", import_path)); }
267
268 if import_path.starts_with('/') {
269 return Ok(import_path.to_string());
271 }
272
273 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 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 if Path::new(&resolved_path).exists() {
289 return Ok(resolved_path.to_string_lossy().to_string());
290 }
291
292 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 pub fn import_exists(&self, import_path: &str, base_path: &str) -> bool {
305 self.resolve(import_path, base_path).is_ok()
306 }
307
308 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
316pub struct DependencyGraph {
318 nodes: HashMap<String, ImportNode>,
319 edges: HashMap<String, Vec<String>>,
320 visited: HashSet<String>,
321}
322
323impl DependencyGraph {
324 pub fn new() -> Self {
326 Self {
327 nodes: HashMap::new(),
328 edges: HashMap::new(),
329 visited: HashSet::new(),
330 }
331 }
332
333 pub fn add_dependency(&mut self, from: &str, to: &str) -> Result<(), ImportError> {
335 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 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 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 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 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
432pub 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 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 pub fn get_cached_content(&self, path: &str) -> Option<&String> {
451 self.file_cache.get(path)
452 }
453
454 pub fn cache_content(&mut self, path: String, content: String) {
456 self.file_cache.insert(path, content);
457 }
458
459 pub fn get_cached_dependencies(&self, path: &str) -> Option<&Vec<String>> {
461 self.dependency_cache.get(path)
462 }
463
464 pub fn cache_dependencies(&mut self, path: String, dependencies: Vec<String>) {
466 self.dependency_cache.insert(path, dependencies);
467 }
468
469 pub fn get_cached_processed(&self, path: &str) -> Option<&String> {
471 self.processed_cache.get(path)
472 }
473
474 pub fn cache_processed(&mut self, path: String, content: String) {
476 self.processed_cache.insert(path, content);
477 }
478}
479
480#[derive(Debug, Clone)]
482pub struct ImportNode {
483 pub path: String,
484 pub dependencies: Vec<String>,
485 pub processed: bool,
486}
487
488#[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#[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#[derive(Debug, Clone)]
524pub enum CircularHandling {
525 Error,
526 Warn,
527 Ignore,
528}
529
530#[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#[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#[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#[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#[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#[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 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 assert!(graph.add_dependency("a.css", "b.css").is_ok());
644 assert!(graph.add_dependency("b.css", "c.css").is_ok());
645
646 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); }
677}