rustyle_css/optimization/
minify.rs1use lightningcss::{
6 stylesheet::{ParserOptions, PrinterOptions, StyleSheet},
7 targets::Browsers,
8};
9
10pub fn minify_css(css: &str) -> Result<String, String> {
12 let stylesheet = StyleSheet::parse(
13 css,
14 ParserOptions {
15 filename: "style.css".to_string(),
16 ..Default::default()
17 },
18 )
19 .map_err(|e| format!("CSS parsing error: {:?}", e))?;
20
21 let targets = Browsers::default();
22 let printer_options = PrinterOptions {
23 minify: true,
24 targets: targets.into(),
25 ..Default::default()
26 };
27
28 let result = stylesheet
29 .to_css(printer_options)
30 .map_err(|e| format!("CSS generation error: {:?}", e))?;
31
32 Ok(result.code)
33}
34
35pub fn remove_unused_css(css: &str, used_selectors: &[&str]) -> String {
47 if used_selectors.is_empty() {
48 return String::new();
50 }
51
52 let used_set: std::collections::HashSet<&str> = used_selectors.iter().copied().collect();
53
54 let rules = split_css_into_rules(css);
56
57 let used_rules: Vec<String> = rules
59 .into_iter()
60 .filter(|rule| {
61 let matches = rule_matches_any_selector(rule, &used_set);
63 matches || rule.contains("body") || rule.contains("html") || rule.contains("*")
65 })
66 .collect();
67
68 let at_rules = extract_at_rules(css);
70
71 let mut result = used_rules.join("\n\n");
73 if !at_rules.is_empty() {
74 if !result.is_empty() {
75 result.push_str("\n\n");
76 }
77 result.push_str(&at_rules.join("\n\n"));
78 }
79
80 result
81}
82
83fn split_css_into_rules(css: &str) -> Vec<String> {
85 let mut rules = Vec::new();
86 let mut current_rule = String::new();
87 let mut brace_depth = 0;
88 let mut in_string = false;
89 let mut string_char = '\0';
90
91 for ch in css.chars() {
92 match ch {
93 '{' if !in_string => {
94 brace_depth += 1;
95 current_rule.push(ch);
96 }
97 '}' if !in_string => {
98 current_rule.push(ch);
99 brace_depth -= 1;
100 if brace_depth == 0 {
101 let rule = current_rule.trim().to_string();
102 if !rule.is_empty() {
103 rules.push(rule);
104 }
105 current_rule.clear();
106 }
107 }
108 '"' | '\'' if !in_string => {
109 in_string = true;
110 string_char = ch;
111 current_rule.push(ch);
112 }
113 _ if in_string && ch == string_char => {
114 in_string = false;
115 current_rule.push(ch);
116 }
117 _ => {
118 if brace_depth > 0 || !current_rule.trim().is_empty() {
119 current_rule.push(ch);
120 }
121 }
122 }
123 }
124
125 if !current_rule.trim().is_empty() {
126 rules.push(current_rule.trim().to_string());
127 }
128
129 rules
130}
131
132fn rule_matches_any_selector(rule: &str, used_selectors: &std::collections::HashSet<&str>) -> bool {
134 if let Some(selector_part) = rule.split('{').next() {
136 let selector_part = selector_part.trim();
137
138 for selector in selector_part.split(',') {
140 let selector = selector.trim();
141
142 for used_sel in used_selectors {
144 if selector_contains(selector, used_sel) {
145 return true;
146 }
147 }
148
149 for used_sel in used_selectors {
151 if selector.contains(used_sel) || used_sel.contains(selector) {
152 return true;
153 }
154 }
155 }
156 }
157
158 false
159}
160
161fn selector_contains(selector: &str, used: &str) -> bool {
163 if selector == used {
165 return true;
166 }
167
168 if used.starts_with('.') && selector.contains(used) {
170 return true;
171 }
172
173 if used.starts_with('#') && selector.contains(used) {
175 return true;
176 }
177
178 if !used.starts_with('.') && !used.starts_with('#') {
180 if selector.split_whitespace().any(|part| part == used) {
182 return true;
183 }
184 }
185
186 false
187}
188
189fn extract_at_rules(css: &str) -> Vec<String> {
191 let mut at_rules = Vec::new();
192
193 let mut in_at_rule = false;
195 let mut current_rule = String::new();
196 let mut brace_depth = 0;
197
198 for line in css.lines() {
199 if line.trim().starts_with('@') && !in_at_rule {
200 in_at_rule = true;
201 current_rule = line.to_string();
202 brace_depth = line.matches('{').count() as i32 - line.matches('}').count() as i32;
203 } else if in_at_rule {
204 current_rule.push('\n');
205 current_rule.push_str(line);
206 brace_depth += line.matches('{').count() as i32 - line.matches('}').count() as i32;
207
208 if brace_depth == 0 {
209 at_rules.push(current_rule.trim().to_string());
210 current_rule.clear();
211 in_at_rule = false;
212 }
213 }
214 }
215
216 if !current_rule.trim().is_empty() {
218 at_rules.push(current_rule.trim().to_string());
219 }
220
221 at_rules
222}