1use regex::Regex;
7use std::collections::{HashMap, HashSet};
8use std::path::Path;
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
46 .resolver
47 .resolve(&import_statement.import_path, base_path)?;
48
49 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 let content = self
65 .resolver
66 .get_import_content(&import_statement.import_path, base_path)?;
67
68 let processed_content = if self.config.inline_imports {
70 self.process_imports(&content, &resolved_path)?
71 } else {
72 content
73 };
74
75 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 self.cache
105 .cache_content(base_path.to_string(), processed_css.clone());
106
107 Ok(processed_css)
108 }
109
110 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 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 let content = self
146 .resolver
147 .get_import_content(&import_statement.import_path, &options.search_paths[0])?;
148
149 let processed_content = if options.inline_imports {
151 self.process_imports(&content, &resolved_path)?
152 } else {
153 content
154 };
155
156 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 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 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, import_path,
224 media_query,
225 })
226 } else {
227 Err(ImportError::InvalidImportStatement {
228 line: line.to_string(),
229 })
230 }
231 }
232
233 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 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
266pub struct ImportResolver {
268 search_paths: Vec<String>,
269 extensions: Vec<String>,
270 config: ResolverConfig,
271}
272
273impl ImportResolver {
274 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 pub fn resolve(&self, import_path: &str, base_path: &str) -> Result<String, ImportError> {
285 if import_path.starts_with("http://") || import_path.starts_with("https://") {
287 return Ok(import_path.to_string()); }
289
290 if import_path.starts_with("//") {
291 return Ok(format!("https:{}", import_path)); }
293
294 if import_path.starts_with('/') {
295 return Ok(import_path.to_string());
297 }
298
299 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 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 if Path::new(&resolved_path).exists() {
319 return Ok(resolved_path.to_string_lossy().to_string());
320 }
321
322 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 pub fn import_exists(&self, import_path: &str, base_path: &str) -> bool {
337 self.resolve(import_path, base_path).is_ok()
338 }
339
340 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
353pub struct DependencyGraph {
355 nodes: HashMap<String, ImportNode>,
356 edges: HashMap<String, Vec<String>>,
357 visited: HashSet<String>,
358}
359
360impl DependencyGraph {
361 pub fn new() -> Self {
363 Self {
364 nodes: HashMap::new(),
365 edges: HashMap::new(),
366 visited: HashSet::new(),
367 }
368 }
369
370 pub fn add_dependency(&mut self, from: &str, to: &str) -> Result<(), ImportError> {
372 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 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 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 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 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
474pub 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 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 pub fn get_cached_content(&self, path: &str) -> Option<&String> {
493 self.file_cache.get(path)
494 }
495
496 pub fn cache_content(&mut self, path: String, content: String) {
498 self.file_cache.insert(path, content);
499 }
500
501 pub fn get_cached_dependencies(&self, path: &str) -> Option<&Vec<String>> {
503 self.dependency_cache.get(path)
504 }
505
506 pub fn cache_dependencies(&mut self, path: String, dependencies: Vec<String>) {
508 self.dependency_cache.insert(path, dependencies);
509 }
510
511 pub fn get_cached_processed(&self, path: &str) -> Option<&String> {
513 self.processed_cache.get(path)
514 }
515
516 pub fn cache_processed(&mut self, path: String, content: String) {
518 self.processed_cache.insert(path, content);
519 }
520}
521
522#[derive(Debug, Clone)]
524pub struct ImportNode {
525 pub path: String,
526 pub dependencies: Vec<String>,
527 pub processed: bool,
528}
529
530#[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#[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#[derive(Debug, Clone)]
566pub enum CircularHandling {
567 Error,
568 Warn,
569 Ignore,
570}
571
572#[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#[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#[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#[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#[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#[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 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 assert!(graph.add_dependency("a.css", "b.css").is_ok());
689 assert!(graph.add_dependency("b.css", "c.css").is_ok());
690
691 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); }
722}