use super::types::*;
use std::collections::HashSet;
use std::fs;
use std::path::Path;
pub struct ContentScanner {
file_extensions: Vec<String>,
class_patterns: Vec<String>,
}
impl ContentScanner {
pub fn new() -> Self {
Self {
file_extensions: Self::get_default_extensions(),
class_patterns: Self::get_default_patterns(),
}
}
pub fn scan_content(&self, content_paths: &[String]) -> Result<HashSet<String>, PurgeError> {
let mut used_classes = HashSet::new();
for path in content_paths {
let classes = self.scan_file(path)?;
used_classes.extend(classes);
}
Ok(used_classes)
}
pub fn scan_content_advanced(
&self,
content_paths: &[String],
options: &PurgeOptions,
) -> Result<HashSet<String>, PurgeError> {
let mut used_classes = HashSet::new();
for path in content_paths {
if self.should_scan_file(path, options) {
let classes = self.scan_file(path)?;
used_classes.extend(classes);
}
}
Ok(used_classes)
}
fn scan_file(&self, path: &str) -> Result<HashSet<String>, PurgeError> {
let content = fs::read_to_string(path).map_err(|e| PurgeError::FileReadingFailed {
path: path.to_string(),
error: e.to_string(),
})?;
let file_type = self.detect_file_type(path);
self.extract_classes_from_content(&content, &file_type)
}
fn extract_classes_from_content(
&self,
content: &str,
file_type: &FileType,
) -> Result<HashSet<String>, PurgeError> {
let mut classes = HashSet::new();
match file_type {
FileType::Html => {
classes.extend(self.extract_from_html(content));
}
FileType::JavaScript | FileType::TypeScript => {
classes.extend(self.extract_from_js(content));
}
FileType::Rust => {
classes.extend(self.extract_from_rust(content));
}
FileType::Vue => {
classes.extend(self.extract_from_vue(content));
}
FileType::Svelte => {
classes.extend(self.extract_from_svelte(content));
}
FileType::Other(_) => {
classes.extend(self.extract_generic(content));
}
}
Ok(classes)
}
fn extract_from_html(&self, content: &str) -> HashSet<String> {
let mut classes = HashSet::new();
let class_pattern = regex::Regex::new(r#"class\s*=\s*["']([^"']+)["']"#).unwrap();
for cap in class_pattern.captures_iter(content) {
let class_attr = &cap[1];
for class_name in class_attr.split_whitespace() {
classes.insert(class_name.to_string());
}
}
classes
}
fn extract_from_js(&self, content: &str) -> HashSet<String> {
let mut classes = HashSet::new();
let class_patterns = vec["']"#,
r#"class\s*=\s*["']([^"']+)["']"#,
r#"class:\s*["']([^"']+)["']"#,
];
for pattern in class_patterns {
let regex = regex::Regex::new(pattern).unwrap();
for cap in regex.captures_iter(content) {
let class_attr = &cap[1];
for class_name in class_attr.split_whitespace() {
classes.insert(class_name.to_string());
}
}
}
classes
}
fn extract_from_rust(&self, content: &str) -> HashSet<String> {
let mut classes = HashSet::new();
let class_patterns = vec["']"#,
r#"class\s*=\s*["']([^"']+)["']"#,
];
for pattern in class_patterns {
let regex = regex::Regex::new(pattern).unwrap();
for cap in regex.captures_iter(content) {
let class_attr = &cap[1];
for class_name in class_attr.split_whitespace() {
classes.insert(class_name.to_string());
}
}
}
classes
}
fn extract_from_vue(&self, content: &str) -> HashSet<String> {
let mut classes = HashSet::new();
let class_patterns = vec["']"#,
r#":class\s*=\s*["']([^"']+)["']"#,
];
for pattern in class_patterns {
let regex = regex::Regex::new(pattern).unwrap();
for cap in regex.captures_iter(content) {
let class_attr = &cap[1];
for class_name in class_attr.split_whitespace() {
classes.insert(class_name.to_string());
}
}
}
classes
}
fn extract_from_svelte(&self, content: &str) -> HashSet<String> {
let mut classes = HashSet::new();
let class_patterns = vec["']"#,
r#"class:\s*["']([^"']+)["']"#,
];
for pattern in class_patterns {
let regex = regex::Regex::new(pattern).unwrap();
for cap in regex.captures_iter(content) {
let class_attr = &cap[1];
for class_name in class_attr.split_whitespace() {
classes.insert(class_name.to_string());
}
}
}
classes
}
fn extract_generic(&self, content: &str) -> HashSet<String> {
let mut classes = HashSet::new();
for pattern in &self.class_patterns {
let regex = regex::Regex::new(pattern).unwrap();
for cap in regex.captures_iter(content) {
let class_attr = &cap[1];
for class_name in class_attr.split_whitespace() {
classes.insert(class_name.to_string());
}
}
}
classes
}
fn detect_file_type(&self, path: &str) -> FileType {
let path = Path::new(path);
let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
match extension {
"html" | "htm" => FileType::Html,
"js" | "jsx" => FileType::JavaScript,
"ts" | "tsx" => FileType::TypeScript,
"rs" => FileType::Rust,
"vue" => FileType::Vue,
"svelte" => FileType::Svelte,
_ => FileType::Other(extension.to_string()),
}
}
fn should_scan_file(&self, path: &str, options: &PurgeOptions) -> bool {
if !options.include_patterns.is_empty() {
let should_include = options
.include_patterns
.iter()
.any(|pattern| path.contains(pattern));
if !should_include {
return false;
}
}
if options
.exclude_patterns
.iter()
.any(|pattern| path.contains(pattern))
{
return false;
}
true
}
fn get_default_extensions() -> Vec<String> {
vec![
"html".to_string(),
"htm".to_string(),
"js".to_string(),
"jsx".to_string(),
"ts".to_string(),
"tsx".to_string(),
"rs".to_string(),
"vue".to_string(),
"svelte".to_string(),
]
}
fn get_default_patterns() -> Vec<String> {
vec["']"#.to_string(),
r#"className\s*=\s*["']([^"']+)["']"#.to_string(),
r#"class!\s*\(\s*["']([^"']+)["']"#.to_string(),
]
}
}