rustyle_css/optimization/
critical.rs1use regex::Regex;
6use std::collections::{HashMap, HashSet};
7
8pub fn extract_critical_css(css: &str, html: Option<&str>, route: Option<&str>) -> String {
23 let _ = route; let used_selectors = if let Some(html_content) = html {
26 extract_selectors_from_html(html_content)
27 } else {
28 get_default_critical_selectors()
30 };
31
32 let result = extract_matching_css_rules(css, &used_selectors);
34
35 if result.is_empty() {
37 let rules = split_css_into_rules(css);
39 if !rules.is_empty() {
40 rules.into_iter().take(3).collect::<Vec<_>>().join("\n\n")
41 } else {
42 css.to_string() }
44 } else {
45 result
46 }
47}
48
49fn extract_selectors_from_html(html: &str) -> HashSet<String> {
51 let mut selectors = HashSet::new();
52
53 let class_re = Regex::new(r#"class\s*=\s*["']([^"']+)["']"#).unwrap();
55 for cap in class_re.captures_iter(html) {
56 let classes = cap.get(1).unwrap().as_str();
57 for class in classes.split_whitespace() {
58 selectors.insert(format!(".{}", class.trim()));
59 }
60 }
61
62 let id_re = Regex::new(r#"id\s*=\s*["']([^"']+)["']"#).unwrap();
64 for cap in id_re.captures_iter(html) {
65 let id = cap.get(1).unwrap().as_str();
66 selectors.insert(format!("#{}", id.trim()));
67 }
68
69 let element_re = Regex::new(r"<(body|header|nav|main|section|article|h1|h2|h3|h4|h5|h6|p|div|span|a|img|button|input|form)(?:\s|>)").unwrap();
71 for cap in element_re.captures_iter(html) {
72 if let Some(element) = cap.get(1) {
73 selectors.insert(element.as_str().to_string());
74 }
75 }
76
77 selectors.extend(get_default_critical_selectors());
79
80 selectors
81}
82
83fn get_default_critical_selectors() -> HashSet<String> {
85 let mut selectors = HashSet::new();
86
87 selectors.insert("body".to_string());
89 selectors.insert("html".to_string());
90 selectors.insert("header".to_string());
91 selectors.insert("nav".to_string());
92 selectors.insert("main".to_string());
93 selectors.insert("h1".to_string());
94 selectors.insert("h2".to_string());
95 selectors.insert("h3".to_string());
96 selectors.insert("p".to_string());
97 selectors.insert("a".to_string());
98 selectors.insert("img".to_string());
99 selectors.insert("button".to_string());
100 selectors.insert("input".to_string());
101
102 selectors.insert(".container".to_string());
104 selectors.insert(".header".to_string());
105 selectors.insert(".nav".to_string());
106 selectors.insert(".main".to_string());
107 selectors.insert(".hero".to_string());
108 selectors.insert(".button".to_string());
109 selectors.insert(".btn".to_string());
110
111 selectors
112}
113
114fn extract_matching_css_rules(css: &str, selectors: &HashSet<String>) -> String {
116 let mut critical_rules = Vec::new();
117
118 let rules = split_css_into_rules(css);
120
121 for rule in rules {
122 if rule_matches_selectors(&rule, selectors) {
124 critical_rules.push(rule);
125 }
126 }
127
128 let at_rules = extract_at_rules(css);
130 critical_rules.extend(at_rules);
131
132 critical_rules.join("\n\n")
133}
134
135fn split_css_into_rules(css: &str) -> Vec<String> {
137 let mut rules = Vec::new();
138 let mut current_rule = String::new();
139 let mut brace_depth = 0;
140 let mut in_string = false;
141 let mut string_char = '\0';
142
143 for ch in css.chars() {
144 match ch {
145 '{' if !in_string => {
146 brace_depth += 1;
147 current_rule.push(ch);
148 }
149 '}' if !in_string => {
150 current_rule.push(ch);
151 brace_depth -= 1;
152 if brace_depth == 0 {
153 rules.push(current_rule.trim().to_string());
154 current_rule.clear();
155 }
156 }
157 '"' | '\'' if !in_string => {
158 in_string = true;
159 string_char = ch;
160 current_rule.push(ch);
161 }
162 _ if in_string && ch == string_char => {
163 in_string = false;
164 current_rule.push(ch);
165 }
166 _ => {
167 if brace_depth > 0 || !current_rule.trim().is_empty() {
168 current_rule.push(ch);
169 }
170 }
171 }
172 }
173
174 if !current_rule.trim().is_empty() {
175 rules.push(current_rule.trim().to_string());
176 }
177
178 rules
179}
180
181fn rule_matches_selectors(rule: &str, selectors: &HashSet<String>) -> bool {
183 if let Some(selector_part) = rule.split('{').next() {
185 let selector_part = selector_part.trim();
186
187 for selector in selector_part.split(',') {
189 let selector = selector.trim();
190
191 for critical_sel in selectors {
193 if selector_contains(selector, critical_sel) {
194 return true;
195 }
196 }
197 }
198 }
199
200 false
201}
202
203fn selector_contains(selector: &str, critical: &str) -> bool {
205 selector.contains(critical) ||
207 (critical.starts_with('.') && selector.contains(critical)) ||
209 (critical.starts_with('#') && selector.contains(critical)) ||
211 (selector.trim().starts_with(critical) && selector.chars().nth(critical.len()).map_or(true, |c| !c.is_alphanumeric() && c != '-' && c != '_'))
213}
214
215fn extract_at_rules(css: &str) -> Vec<String> {
217 let mut at_rules = Vec::new();
218 let at_rule_re = Regex::new(r"@[^{]+\{[^}]*\}").unwrap();
219
220 for cap in at_rule_re.find_iter(css) {
221 at_rules.push(cap.as_str().to_string());
222 }
223
224 at_rules
225}
226
227pub fn split_css_by_route(css: &str, routes: &[&str]) -> HashMap<String, String> {
232 let mut split = HashMap::new();
233
234 let common_css = extract_common_css(css);
236
237 for route in routes {
239 let route_specific = extract_route_specific_css(css, route);
240 let combined = if common_css.is_empty() {
241 route_specific
242 } else if route_specific.is_empty() {
243 common_css.clone()
244 } else {
245 format!("{}\n\n{}", common_css, route_specific)
246 };
247 split.insert(route.to_string(), combined);
248 }
249
250 split
251}
252
253fn extract_common_css(css: &str) -> String {
255 css.to_string()
259}
260
261fn extract_route_specific_css(css: &str, route: &str) -> String {
263 let route_pattern = format!("-{}", route);
266 let rules = split_css_into_rules(css);
267
268 rules
269 .into_iter()
270 .filter(|rule| rule.contains(&route_pattern))
271 .collect::<Vec<_>>()
272 .join("\n\n")
273}