tailwind_rs_postcss/purger/
content_scanner.rs

1//! Content scanner for finding used CSS classes
2
3use std::collections::HashSet;
4use std::fs;
5use std::path::Path;
6use super::types::*;
7
8/// Content scanner for finding used CSS classes
9pub struct ContentScanner {
10    file_extensions: Vec<String>,
11    class_patterns: Vec<String>,
12}
13
14impl ContentScanner {
15    /// Create new content scanner
16    pub fn new() -> Self {
17        Self {
18            file_extensions: Self::get_default_extensions(),
19            class_patterns: Self::get_default_patterns(),
20        }
21    }
22    
23    /// Scan content for used classes
24    pub fn scan_content(&self, content_paths: &[String]) -> Result<HashSet<String>, PurgeError> {
25        let mut used_classes = HashSet::new();
26        
27        for path in content_paths {
28            let classes = self.scan_file(path)?;
29            used_classes.extend(classes);
30        }
31        
32        Ok(used_classes)
33    }
34    
35    /// Scan content with advanced options
36    pub fn scan_content_advanced(&self, content_paths: &[String], options: &PurgeOptions) -> Result<HashSet<String>, PurgeError> {
37        let mut used_classes = HashSet::new();
38        
39        for path in content_paths {
40            if self.should_scan_file(path, options) {
41                let classes = self.scan_file(path)?;
42                used_classes.extend(classes);
43            }
44        }
45        
46        Ok(used_classes)
47    }
48    
49    /// Scan a single file for classes
50    fn scan_file(&self, path: &str) -> Result<HashSet<String>, PurgeError> {
51        let content = fs::read_to_string(path)
52            .map_err(|e| PurgeError::FileReadingFailed { 
53                path: path.to_string(), 
54                error: e.to_string() 
55            })?;
56        
57        let file_type = self.detect_file_type(path);
58        self.extract_classes_from_content(&content, &file_type)
59    }
60    
61    /// Extract classes from content based on file type
62    fn extract_classes_from_content(&self, content: &str, file_type: &FileType) -> Result<HashSet<String>, PurgeError> {
63        let mut classes = HashSet::new();
64        
65        match file_type {
66            FileType::Html => {
67                classes.extend(self.extract_from_html(content));
68            }
69            FileType::JavaScript | FileType::TypeScript => {
70                classes.extend(self.extract_from_js(content));
71            }
72            FileType::Rust => {
73                classes.extend(self.extract_from_rust(content));
74            }
75            FileType::Vue => {
76                classes.extend(self.extract_from_vue(content));
77            }
78            FileType::Svelte => {
79                classes.extend(self.extract_from_svelte(content));
80            }
81            FileType::Other(_) => {
82                classes.extend(self.extract_generic(content));
83            }
84        }
85        
86        Ok(classes)
87    }
88    
89    /// Extract classes from HTML content
90    fn extract_from_html(&self, content: &str) -> HashSet<String> {
91        let mut classes = HashSet::new();
92        let class_pattern = regex::Regex::new(r#"class\s*=\s*["']([^"']+)["']"#).unwrap();
93        
94        for cap in class_pattern.captures_iter(content) {
95            let class_attr = &cap[1];
96            for class_name in class_attr.split_whitespace() {
97                classes.insert(class_name.to_string());
98            }
99        }
100        
101        classes
102    }
103    
104    /// Extract classes from JavaScript/TypeScript content
105    fn extract_from_js(&self, content: &str) -> HashSet<String> {
106        let mut classes = HashSet::new();
107        
108        // Look for className patterns
109        let class_patterns = vec![
110            r#"className\s*=\s*["']([^"']+)["']"#,
111            r#"class\s*=\s*["']([^"']+)["']"#,
112            r#"class:\s*["']([^"']+)["']"#,
113        ];
114        
115        for pattern in class_patterns {
116            let regex = regex::Regex::new(pattern).unwrap();
117            for cap in regex.captures_iter(content) {
118                let class_attr = &cap[1];
119                for class_name in class_attr.split_whitespace() {
120                    classes.insert(class_name.to_string());
121                }
122            }
123        }
124        
125        classes
126    }
127    
128    /// Extract classes from Rust content
129    fn extract_from_rust(&self, content: &str) -> HashSet<String> {
130        let mut classes = HashSet::new();
131        
132        // Look for class! macro patterns
133        let class_patterns = vec![
134            r#"class!\s*\(\s*["']([^"']+)["']"#,
135            r#"class\s*=\s*["']([^"']+)["']"#,
136        ];
137        
138        for pattern in class_patterns {
139            let regex = regex::Regex::new(pattern).unwrap();
140            for cap in regex.captures_iter(content) {
141                let class_attr = &cap[1];
142                for class_name in class_attr.split_whitespace() {
143                    classes.insert(class_name.to_string());
144                }
145            }
146        }
147        
148        classes
149    }
150    
151    /// Extract classes from Vue content
152    fn extract_from_vue(&self, content: &str) -> HashSet<String> {
153        let mut classes = HashSet::new();
154        
155        // Look for class patterns in Vue templates
156        let class_patterns = vec![
157            r#"class\s*=\s*["']([^"']+)["']"#,
158            r#":class\s*=\s*["']([^"']+)["']"#,
159        ];
160        
161        for pattern in class_patterns {
162            let regex = regex::Regex::new(pattern).unwrap();
163            for cap in regex.captures_iter(content) {
164                let class_attr = &cap[1];
165                for class_name in class_attr.split_whitespace() {
166                    classes.insert(class_name.to_string());
167                }
168            }
169        }
170        
171        classes
172    }
173    
174    /// Extract classes from Svelte content
175    fn extract_from_svelte(&self, content: &str) -> HashSet<String> {
176        let mut classes = HashSet::new();
177        
178        // Look for class patterns in Svelte templates
179        let class_patterns = vec![
180            r#"class\s*=\s*["']([^"']+)["']"#,
181            r#"class:\s*["']([^"']+)["']"#,
182        ];
183        
184        for pattern in class_patterns {
185            let regex = regex::Regex::new(pattern).unwrap();
186            for cap in regex.captures_iter(content) {
187                let class_attr = &cap[1];
188                for class_name in class_attr.split_whitespace() {
189                    classes.insert(class_name.to_string());
190                }
191            }
192        }
193        
194        classes
195    }
196    
197    /// Extract classes using generic patterns
198    fn extract_generic(&self, content: &str) -> HashSet<String> {
199        let mut classes = HashSet::new();
200        
201        // Use generic class patterns
202        for pattern in &self.class_patterns {
203            let regex = regex::Regex::new(pattern).unwrap();
204            for cap in regex.captures_iter(content) {
205                let class_attr = &cap[1];
206                for class_name in class_attr.split_whitespace() {
207                    classes.insert(class_name.to_string());
208                }
209            }
210        }
211        
212        classes
213    }
214    
215    /// Detect file type from path
216    fn detect_file_type(&self, path: &str) -> FileType {
217        let path = Path::new(path);
218        let extension = path.extension()
219            .and_then(|ext| ext.to_str())
220            .unwrap_or("");
221        
222        match extension {
223            "html" | "htm" => FileType::Html,
224            "js" | "jsx" => FileType::JavaScript,
225            "ts" | "tsx" => FileType::TypeScript,
226            "rs" => FileType::Rust,
227            "vue" => FileType::Vue,
228            "svelte" => FileType::Svelte,
229            _ => FileType::Other(extension.to_string()),
230        }
231    }
232    
233    /// Check if file should be scanned based on options
234    fn should_scan_file(&self, path: &str, options: &PurgeOptions) -> bool {
235        // Check include patterns
236        if !options.include_patterns.is_empty() {
237            let should_include = options.include_patterns.iter()
238                .any(|pattern| path.contains(pattern));
239            if !should_include {
240                return false;
241            }
242        }
243        
244        // Check exclude patterns
245        if options.exclude_patterns.iter().any(|pattern| path.contains(pattern)) {
246            return false;
247        }
248        
249        true
250    }
251    
252    /// Get default file extensions
253    fn get_default_extensions() -> Vec<String> {
254        vec![
255            "html".to_string(),
256            "htm".to_string(),
257            "js".to_string(),
258            "jsx".to_string(),
259            "ts".to_string(),
260            "tsx".to_string(),
261            "rs".to_string(),
262            "vue".to_string(),
263            "svelte".to_string(),
264        ]
265    }
266    
267    /// Get default class patterns
268    fn get_default_patterns() -> Vec<String> {
269        vec![
270            r#"class\s*=\s*["']([^"']+)["']"#.to_string(),
271            r#"className\s*=\s*["']([^"']+)["']"#.to_string(),
272            r#"class!\s*\(\s*["']([^"']+)["']"#.to_string(),
273        ]
274    }
275}