tailwind_rs_postcss/purger/
content_scanner.rs1use super::types::*;
4use std::collections::HashSet;
5use std::fs;
6use std::path::Path;
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(
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 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 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 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 fn extract_from_js(&self, content: &str) -> HashSet<String> {
113 let mut classes = HashSet::new();
114
115 let class_patterns = vec["']"#,
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 fn extract_from_rust(&self, content: &str) -> HashSet<String> {
137 let mut classes = HashSet::new();
138
139 let class_patterns = vec["']"#,
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 fn extract_from_vue(&self, content: &str) -> HashSet<String> {
160 let mut classes = HashSet::new();
161
162 let class_patterns = vec["']"#,
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 fn extract_from_svelte(&self, content: &str) -> HashSet<String> {
183 let mut classes = HashSet::new();
184
185 let class_patterns = vec["']"#,
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 fn extract_generic(&self, content: &str) -> HashSet<String> {
206 let mut classes = HashSet::new();
207
208 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 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 fn should_scan_file(&self, path: &str, options: &PurgeOptions) -> bool {
240 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 if options
253 .exclude_patterns
254 .iter()
255 .any(|pattern| path.contains(pattern))
256 {
257 return false;
258 }
259
260 true
261 }
262
263 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 fn get_default_patterns() -> Vec<String> {
280 vec["']"#.to_string(),
282 r#"className\s*=\s*["']([^"']+)["']"#.to_string(),
283 r#"class!\s*\(\s*["']([^"']+)["']"#.to_string(),
284 ]
285 }
286}