diff --git a/src/interpreter/style.rs b/src/interpreter/style.rs index cfb70d6..5b033ff 100644 --- a/src/interpreter/style.rs +++ b/src/interpreter/style.rs @@ -461,6 +461,7 @@ impl StyleRule { pub struct StyleSheet { rules: Vec, index: Option, + epoch: u64, } impl Default for StyleSheet { @@ -468,6 +469,7 @@ impl Default for StyleSheet { Self { rules: Vec::new(), index: None, + epoch: 0, } } } @@ -481,11 +483,13 @@ impl StyleSheet { for part in split_selectors(&selector) { self.rules.push(StyleRule::build(part, properties.clone())); } + self.index = None; } pub fn build_index(&mut self) { + self.epoch += 1; let mut index = StyleIndex::new(); - index.epoch += 1; + index.epoch = self.epoch; for (i, rule) in self.rules.iter().enumerate() { let specificity = rule.selector.specificity(); @@ -1205,3 +1209,153 @@ pub fn parse_size(s: &str) -> Option { .parse::() .ok() } + +#[cfg(test)] +mod tests { + use super::*; + + fn make_style_sheet(rules: &[(&str, &[(&str, &str)])]) -> StyleSheet { + let mut ss = StyleSheet::new(); + for (selector, props) in rules { + let mut map = HashMap::new(); + for (k, v) in *props { + map.insert(k.to_string(), v.to_string()); + } + ss.add_rule(selector.to_string(), map); + } + ss.build_index(); + ss + } + + #[test] + fn test_style_index_basic() { + let ss = make_style_sheet(&[ + ("Button", &[("color", "red")]), + ("Button.primary", &[("color", "blue")]), + ("#submit", &[("font-size", "20")]), + ("Label", &[("color", "green")]), + ]); + + let attrs = HashMap::new(); + let structural = StructuralContext::default(); + + // Button class="" should match "Button" rule + let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs); + assert_eq!(matched.len(), 1, "Button should match 1 rule"); + assert_eq!(matched[0].get("color").map(|s| s.as_str()), Some("red")); + + // Button class="primary" should match both "Button" and "Button.primary" + let matched = ss.matching_rules("Button", None, &["primary"], &[], &structural, &[], &[], &attrs); + assert_eq!(matched.len(), 2, "Button.primary should match 2 rules"); + // The more specific rule comes first (or last depending on order) + let colors: Vec<&str> = matched.iter().filter_map(|m| m.get("color").map(|s| s.as_str())).collect(); + assert!(colors.contains(&"red")); + assert!(colors.contains(&"blue")); + + // Button id="submit" should match "Button" and "#submit" + let matched = ss.matching_rules("Button", Some("submit"), &[], &[], &structural, &[], &[], &attrs); + assert_eq!(matched.len(), 2, "Button#submit should match 2 rules"); + assert!(matched.iter().any(|m| m.contains_key("color"))); + assert!(matched.iter().any(|m| m.contains_key("font-size"))); + + // Label should match "Label" + let matched = ss.matching_rules("Label", None, &[], &[], &structural, &[], &[], &attrs); + assert_eq!(matched.len(), 1); + assert_eq!(matched[0].get("color").map(|s| s.as_str()), Some("green")); + } + + #[test] + fn test_style_index_empty() { + let ss = make_style_sheet(&[]); + let attrs = HashMap::new(); + let structural = StructuralContext::default(); + + let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs); + assert!(matched.is_empty()); + } + + #[test] + fn test_style_index_pseudo() { + let ss = make_style_sheet(&[ + ("Button:hover", &[("color", "red")]), + ("Button", &[("color", "blue")]), + ]); + + let attrs = HashMap::new(); + let structural = StructuralContext::default(); + + // No pseudo + let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs); + assert_eq!(matched.len(), 1); + assert_eq!(matched[0].get("color").map(|s| s.as_str()), Some("blue")); + + // With hover pseudo + let matched = ss.matching_rules("Button", None, &[], &["hover"], &structural, &[], &[], &attrs); + assert_eq!(matched.len(), 2, "Button:hover should match 2 rules with pseudo"); + } + + #[test] + fn test_style_index_complex() { + let ss = make_style_sheet(&[ + ("Panel > Button", &[("color", "red")]), + ("Panel Button", &[("font-size", "16")]), + ]); + + let attrs = HashMap::new(); + let structural = StructuralContext::default(); + + // Button with Panel ancestor + let ancestors = [AncestorInfo::new("Panel", vec![])]; + let matched = ss.matching_rules("Button", None, &[], &[], &structural, &ancestors, &[], &attrs); + assert_eq!(matched.len(), 2, "Both complex rules should match"); + + // Button without Panel ancestor + let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs); + assert_eq!(matched.len(), 0, "No matching without Panel ancestor"); + } + + #[test] + fn test_style_index_off_by_one_same_as_fallback() { + // Verify index gives same results as fallback for various inputs + let ss = make_style_sheet(&[ + ("*", &[("margin", "0")]), + ("Button", &[("padding", "10")]), + ("Button.primary#submit", &[("color", "red")]), + ("Label:hover", &[("color", "blue")]), + (".highlight", &[("background", "yellow")]), + ]); + + let attrs: HashMap = HashMap::new(); + let structural = StructuralContext::default(); + + // Verify index matches expected behavior + let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs); + assert!(!matched.is_empty(), "Button should match universal rule"); + assert!(matched.iter().any(|m| m.get("margin").map(|s| s.as_str()) == Some("0")), "Should include universal margin"); + assert!(matched.iter().any(|m| m.get("padding").map(|s| s.as_str()) == Some("10")), "Should include Button padding"); + + let matched = ss.matching_rules("Button", Some("submit"), &["primary"], &[], &structural, &[], &[], &attrs); + assert!(matched.iter().any(|m| m.contains_key("color")), "#submit.primary should match color rule"); + + let matched = ss.matching_rules("Label", None, &["highlight"], &[], &structural, &[], &[], &attrs); + assert!(matched.iter().any(|m| m.contains_key("background")), "highlight class should match"); + + let matched = ss.matching_rules("Panel", None, &[], &[], &structural, &[], &[], &attrs); + assert!(matched.iter().any(|m| m.contains_key("margin")), "Panel should match universal rule"); + } + + #[test] + fn test_style_index_epoch_increases() { + let mut ss = make_style_sheet(&[("Button", &[("color", "red")])]); + let epoch1 = ss.index.as_ref().unwrap().epoch; + ss.add_rule("Label".to_string(), { + let mut m = HashMap::new(); + m.insert("color".to_string(), "blue".to_string()); + m + }); + assert!(ss.index.is_none(), "add_rule should invalidate index"); + ss.build_index(); + let epoch2 = ss.index.as_ref().unwrap().epoch; + assert!(epoch2 > epoch1, "build_index should increase epoch"); + } +}