use super::types::*;
use regex::Regex;
use std::collections::HashSet;
pub struct RuleFilter {
css_rule_pattern: Regex,
comment_pattern: Regex,
keyframe_pattern: Regex,
media_query_pattern: Regex,
}
impl RuleFilter {
pub fn new() -> Self {
Self {
css_rule_pattern: Regex::new(r"([^{]+)\s*\{([^}]+)\}").unwrap(),
comment_pattern: Regex::new(r"/\*.*?\*/").unwrap(),
keyframe_pattern: Regex::new(r"@keyframes\s+([^{]+)\s*\{([^}]+)\}").unwrap(),
media_query_pattern: Regex::new(r"@media\s+([^{]+)\s*\{([^}]+)\}").unwrap(),
}
}
pub fn filter_rules(
&self,
css: &str,
used_classes: &HashSet<String>,
config: &PurgeConfig,
) -> Result<String, PurgeError> {
let mut filtered_css = String::new();
let mut _rules_removed = 0;
let mut _rules_kept = 0;
for cap in self.css_rule_pattern.captures_iter(css) {
let selector = cap[1].trim();
let properties = cap[2].trim();
if self.should_keep_rule(selector, used_classes, config) {
let rule = format!("{} {{\n{}\n}}", selector, properties);
filtered_css.push_str(&rule);
_rules_kept += 1;
} else {
_rules_removed += 1;
}
}
if config.preserve_comments {
filtered_css = self.preserve_comments(&filtered_css);
}
if config.preserve_keyframes {
filtered_css = self.preserve_keyframes(&filtered_css);
}
if config.preserve_media_queries {
filtered_css = self.preserve_media_queries(&filtered_css);
}
Ok(filtered_css)
}
pub fn filter_rules_advanced(
&self,
css: &str,
used_classes: &HashSet<String>,
config: &PurgeConfig,
options: &PurgeOptions,
) -> Result<String, PurgeError> {
let mut filtered_css = self.filter_rules(css, used_classes, config)?;
if options.minify_output {
filtered_css = self.minify_css(&filtered_css);
}
if !options.preserve_whitespace {
filtered_css = self.remove_whitespace(&filtered_css);
}
Ok(filtered_css)
}
fn should_keep_rule(
&self,
selector: &str,
used_classes: &HashSet<String>,
config: &PurgeConfig,
) -> bool {
if config
.safelist
.iter()
.any(|pattern| selector.contains(pattern))
{
return true;
}
if config
.blocklist
.iter()
.any(|pattern| selector.contains(pattern))
{
return false;
}
let selector_classes = self.extract_classes_from_selector(selector);
selector_classes
.iter()
.any(|class| used_classes.contains(class))
}
fn extract_classes_from_selector(&self, selector: &str) -> HashSet<String> {
let mut classes = HashSet::new();
let parts: Vec<&str> = selector.split(&[' ', '>', '+', '~', ','][..]).collect();
for part in parts {
let part = part.trim();
if part.starts_with('.') {
let class_name = part.strip_prefix('.').unwrap_or(part);
classes.insert(class_name.to_string());
}
}
classes
}
fn preserve_comments(&self, css: &str) -> String {
css.to_string()
}
fn preserve_keyframes(&self, css: &str) -> String {
css.to_string()
}
fn preserve_media_queries(&self, css: &str) -> String {
css.to_string()
}
fn minify_css(&self, css: &str) -> String {
css.replace('\n', "").replace(" ", " ").trim().to_string()
}
fn remove_whitespace(&self, css: &str) -> String {
css.replace('\n', "")
.replace('\t', "")
.replace(" ", " ")
.trim()
.to_string()
}
}