rustyle_css/optimization/
critical.rs

1//! Critical CSS extraction
2//!
3//! Extracts above-the-fold CSS for faster initial page load.
4
5use regex::Regex;
6use std::collections::{HashMap, HashSet};
7
8/// Extract critical CSS from a stylesheet based on HTML content
9///
10/// This function:
11/// - Parses the HTML to identify above-the-fold elements
12/// - Matches CSS rules to those elements
13/// - Extracts only the relevant CSS rules
14///
15/// # Arguments
16/// * `css` - The full CSS stylesheet
17/// * `html` - The HTML content to analyze (optional, if None, extracts common critical patterns)
18/// * `route` - Optional route identifier for route-specific extraction
19///
20/// # Returns
21/// The critical CSS string containing only rules needed for above-the-fold content
22pub fn extract_critical_css(css: &str, html: Option<&str>, route: Option<&str>) -> String {
23    let _ = route; // Route parameter reserved for future use
24                   // If HTML is provided, analyze it to find used selectors
25    let used_selectors = if let Some(html_content) = html {
26        extract_selectors_from_html(html_content)
27    } else {
28        // Default critical selectors (common above-the-fold elements)
29        get_default_critical_selectors()
30    };
31
32    // Extract CSS rules that match the used selectors
33    let result = extract_matching_css_rules(css, &used_selectors);
34
35    // If no matches found, return at least some CSS (better than empty)
36    if result.is_empty() {
37        // Return first few rules as fallback
38        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() // Last resort: return original
43        }
44    } else {
45        result
46    }
47}
48
49/// Extract selectors from HTML by finding classes, IDs, and element types
50fn extract_selectors_from_html(html: &str) -> HashSet<String> {
51    let mut selectors = HashSet::new();
52
53    // Extract class names
54    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    // Extract IDs
63    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    // Extract element types (common above-the-fold elements)
70    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    // Add common critical selectors
78    selectors.extend(get_default_critical_selectors());
79
80    selectors
81}
82
83/// Get default critical selectors for common above-the-fold content
84fn get_default_critical_selectors() -> HashSet<String> {
85    let mut selectors = HashSet::new();
86
87    // Common above-the-fold elements
88    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    // Common utility classes
103    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
114/// Extract CSS rules that match the given selectors
115fn extract_matching_css_rules(css: &str, selectors: &HashSet<String>) -> String {
116    let mut critical_rules = Vec::new();
117
118    // Split CSS into individual rules
119    let rules = split_css_into_rules(css);
120
121    for rule in rules {
122        // Check if any selector in the rule matches our critical selectors
123        if rule_matches_selectors(&rule, selectors) {
124            critical_rules.push(rule);
125        }
126    }
127
128    // Also include @rules (media queries, keyframes, etc.) as they're often critical
129    let at_rules = extract_at_rules(css);
130    critical_rules.extend(at_rules);
131
132    critical_rules.join("\n\n")
133}
134
135/// Split CSS into individual rules
136fn 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
181/// Check if a CSS rule matches any of the critical selectors
182fn rule_matches_selectors(rule: &str, selectors: &HashSet<String>) -> bool {
183    // Extract selectors from the rule (before the first '{')
184    if let Some(selector_part) = rule.split('{').next() {
185        let selector_part = selector_part.trim();
186
187        // Split by comma to get individual selectors
188        for selector in selector_part.split(',') {
189            let selector = selector.trim();
190
191            // Check if this selector matches any critical selector
192            for critical_sel in selectors {
193                if selector_contains(selector, critical_sel) {
194                    return true;
195                }
196            }
197        }
198    }
199
200    false
201}
202
203/// Check if a CSS selector contains a critical selector
204fn selector_contains(selector: &str, critical: &str) -> bool {
205    // Simple matching: check if critical selector appears in the full selector
206    selector.contains(critical) || 
207    // Handle class selectors
208    (critical.starts_with('.') && selector.contains(critical)) ||
209    // Handle ID selectors
210    (critical.starts_with('#') && selector.contains(critical)) ||
211    // Handle element selectors at the start
212    (selector.trim().starts_with(critical) && selector.chars().nth(critical.len()).map_or(true, |c| !c.is_alphanumeric() && c != '-' && c != '_'))
213}
214
215/// Extract @rules (media queries, keyframes, etc.)
216fn 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
227/// Split CSS by route for code splitting
228///
229/// This function analyzes CSS and splits it by route, keeping route-specific
230/// rules separate from common/shared rules.
231pub fn split_css_by_route(css: &str, routes: &[&str]) -> HashMap<String, String> {
232    let mut split = HashMap::new();
233
234    // Extract common CSS (rules not specific to any route)
235    let common_css = extract_common_css(css);
236
237    // For each route, combine common CSS with route-specific CSS
238    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
253/// Extract common CSS (rules not specific to any route)
254fn extract_common_css(css: &str) -> String {
255    // For now, return all CSS as common
256    // A full implementation would identify route-specific patterns
257    // (e.g., rules with route-specific class names)
258    css.to_string()
259}
260
261/// Extract route-specific CSS
262fn extract_route_specific_css(css: &str, route: &str) -> String {
263    // Extract rules that contain route-specific identifiers
264    // This is a simplified implementation
265    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}