tailwind_rs_postcss/purger/
content_scanner.rs1use std::collections::HashSet;
4use std::fs;
5use std::path::Path;
6use super::types::*;
7
8pub struct ContentScanner {
10 file_extensions: Vec<String>,
11 class_patterns: Vec<String>,
12}
13
14impl ContentScanner {
15 pub fn new() -> Self {
17 Self {
18 file_extensions: Self::get_default_extensions(),
19 class_patterns: Self::get_default_patterns(),
20 }
21 }
22
23 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 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 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 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 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 fn extract_from_js(&self, content: &str) -> HashSet<String> {
106 let mut classes = HashSet::new();
107
108 let class_patterns = vec["']"#,
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 fn extract_from_rust(&self, content: &str) -> HashSet<String> {
130 let mut classes = HashSet::new();
131
132 let class_patterns = vec["']"#,
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 fn extract_from_vue(&self, content: &str) -> HashSet<String> {
153 let mut classes = HashSet::new();
154
155 let class_patterns = vec["']"#,
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 fn extract_from_svelte(&self, content: &str) -> HashSet<String> {
176 let mut classes = HashSet::new();
177
178 let class_patterns = vec["']"#,
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 fn extract_generic(&self, content: &str) -> HashSet<String> {
199 let mut classes = HashSet::new();
200
201 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 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 fn should_scan_file(&self, path: &str, options: &PurgeOptions) -> bool {
235 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 if options.exclude_patterns.iter().any(|pattern| path.contains(pattern)) {
246 return false;
247 }
248
249 true
250 }
251
252 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 fn get_default_patterns() -> Vec<String> {
269 vec["']"#.to_string(),
271 r#"className\s*=\s*["']([^"']+)["']"#.to_string(),
272 r#"class!\s*\(\s*["']([^"']+)["']"#.to_string(),
273 ]
274 }
275}