tailwind_rs_postcss/purger/
content_scanner.rs

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