use std::collections::HashMap;
use crate::stylesheet::{
apply_rule, selector_matches, ComputedStyle, Rule, SelectorPart, Specificity, StyleSheet,
};
pub struct CompiledStyleSheet {
rules: Vec<Rule>,
type_rules: HashMap<String, Vec<usize>>,
class_rules: HashMap<String, Vec<usize>>,
id_rules: HashMap<String, Vec<usize>>,
universal_rules: Vec<usize>,
pub generation: u64,
}
impl CompiledStyleSheet {
pub fn compile(sheet: &StyleSheet, generation: u64) -> Self {
let mut type_rules: HashMap<String, Vec<usize>> = HashMap::new();
let mut class_rules: HashMap<String, Vec<usize>> = HashMap::new();
let mut id_rules: HashMap<String, Vec<usize>> = HashMap::new();
let mut universal_rules: Vec<usize> = Vec::new();
for (idx, rule) in sheet.rules.iter().enumerate() {
if rule.selectors.is_empty() {
universal_rules.push(idx);
continue;
}
let mut bucketed = false;
for selector in &rule.selectors {
if let Some(first_part) = selector.parts.first() {
bucketed = true;
match first_part {
SelectorPart::Type(name) => {
type_rules.entry(name.clone()).or_default().push(idx);
}
SelectorPart::Class(name) => {
class_rules.entry(name.clone()).or_default().push(idx);
}
SelectorPart::Id(name) => {
id_rules.entry(name.clone()).or_default().push(idx);
}
}
} else {
universal_rules.push(idx);
}
}
if !bucketed {
universal_rules.push(idx);
}
}
let sort_by_source = |indices: &mut Vec<usize>, rules: &[Rule]| {
indices.sort_by_key(|&i| rules[i].source_order);
indices.dedup();
};
for v in type_rules.values_mut() {
sort_by_source(v, &sheet.rules);
}
for v in class_rules.values_mut() {
sort_by_source(v, &sheet.rules);
}
for v in id_rules.values_mut() {
sort_by_source(v, &sheet.rules);
}
sort_by_source(&mut universal_rules, &sheet.rules);
Self {
rules: sheet.rules.clone(),
type_rules,
class_rules,
id_rules,
universal_rules,
generation,
}
}
pub fn compute_style(
&self,
widget_type: &str,
classes: &[&str],
id: Option<&str>,
) -> ComputedStyle {
let mut candidate_indices: Vec<usize> = Vec::new();
if let Some(idxs) = self.type_rules.get(widget_type) {
candidate_indices.extend_from_slice(idxs);
}
for class in classes {
if let Some(idxs) = self.class_rules.get(*class) {
candidate_indices.extend_from_slice(idxs);
}
}
if let Some(id_str) = id {
if let Some(idxs) = self.id_rules.get(id_str) {
candidate_indices.extend_from_slice(idxs);
}
}
candidate_indices.extend_from_slice(&self.universal_rules);
candidate_indices.sort_unstable();
candidate_indices.dedup();
let mut matches: Vec<(usize, Specificity)> = Vec::new();
for idx in candidate_indices {
let rule = &self.rules[idx];
for selector in &rule.selectors {
if selector_matches(selector, widget_type, classes, id) {
matches.push((idx, selector.specificity));
break; }
}
}
matches.sort_by(|a, b| {
a.1.cmp(&b.1).then(
self.rules[a.0]
.source_order
.cmp(&self.rules[b.0].source_order),
)
});
let mut result = ComputedStyle::default();
for (idx, _) in &matches {
apply_rule(&mut result, &self.rules[*idx].style);
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::stylesheet::StyleSheet;
fn compile_css(css: &str) -> (StyleSheet, CompiledStyleSheet) {
let sheet = StyleSheet::parse(css).stylesheet;
let compiled = CompiledStyleSheet::compile(&sheet, 1);
(sheet, compiled)
}
fn check_equivalence(css: &str, inputs: &[(&str, Vec<&str>, Option<&str>)]) {
let (sheet, compiled) = compile_css(css);
for (wtype, classes, id) in inputs {
let expected = sheet.compute_style(wtype, classes, *id);
let actual = compiled.compute_style(wtype, classes, *id);
assert_eq!(
expected, actual,
"divergence for widget_type={wtype:?} classes={classes:?} id={id:?}"
);
}
}
#[test]
fn test_compiled_matches_uncompiled_simple_selector() {
check_equivalence(
".button { color: #ff0000; }",
&[
("button", vec!["button"], None),
("label", vec!["button"], None),
("button", vec![], None),
],
);
}
#[test]
fn test_compiled_matches_uncompiled_compound_selector() {
check_equivalence(
".button.primary { background: #0000ff; }",
&[
("button", vec!["button", "primary"], None),
("button", vec!["button"], None),
("button", vec!["primary"], None),
("label", vec!["button", "primary"], None),
],
);
}
#[test]
fn test_compiled_matches_uncompiled_grouped_selector() {
check_equivalence(
"button, label { color: #000000; }",
&[
("button", vec![], None),
("label", vec![], None),
("input", vec![], None),
],
);
}
#[test]
fn test_specificity_tiebreak_preserved_post_compile() {
check_equivalence(
"button { color: #ff0000; } #submit { color: #00ff00; }",
&[("button", vec![], Some("submit")), ("button", vec![], None)],
);
}
#[test]
fn test_compiled_matches_uncompiled_ambiguous_grouped() {
check_equivalence(
"button, .foo { color: #ff0000; } button { color: #0000ff; }",
&[
("button", vec!["foo"], None),
("button", vec![], None),
("label", vec!["foo"], None),
],
);
}
#[test]
fn test_compiled_matches_uncompiled_cross_check() {
let css = r#"
button { color: #111111; padding: 8px; }
.primary { background: #7aa2f7; }
button.primary { font-size: 14px; }
#cancel { color: #ff0000; }
label, input { font-size: 12px; }
.disabled { opacity: 0.5; }
"#;
let inputs: &[(&str, Vec<&str>, Option<&str>)] = &[
("button", vec![], None),
("button", vec!["primary"], None),
("button", vec!["primary", "disabled"], None),
("button", vec!["disabled"], Some("cancel")),
("label", vec![], None),
("input", vec!["primary"], None),
("input", vec!["disabled"], None),
("span", vec![], None),
];
check_equivalence(css, inputs);
}
}