From 8be378d013413208f98716971261533edb6a419e Mon Sep 17 00:00:00 2001 From: togami Date: Wed, 27 Nov 2024 23:55:22 +0900 Subject: [PATCH 1/3] chore: add tests for specificity --- crates/biome_css_semantic/src/lib.rs | 2 + crates/biome_css_semantic/src/tests/mod.rs | 1 + .../src/tests/specificity.rs | 379 ++++++++++++++++++ 3 files changed, 382 insertions(+) create mode 100644 crates/biome_css_semantic/src/tests/mod.rs create mode 100644 crates/biome_css_semantic/src/tests/specificity.rs diff --git a/crates/biome_css_semantic/src/lib.rs b/crates/biome_css_semantic/src/lib.rs index 0734f9922a92..cddda49f4cdc 100644 --- a/crates/biome_css_semantic/src/lib.rs +++ b/crates/biome_css_semantic/src/lib.rs @@ -1,5 +1,7 @@ mod events; mod semantic_model; +#[cfg(test)] +mod tests; pub use events::*; pub use semantic_model::*; diff --git a/crates/biome_css_semantic/src/tests/mod.rs b/crates/biome_css_semantic/src/tests/mod.rs new file mode 100644 index 000000000000..37a9388b8da1 --- /dev/null +++ b/crates/biome_css_semantic/src/tests/mod.rs @@ -0,0 +1 @@ +mod specificity; diff --git a/crates/biome_css_semantic/src/tests/specificity.rs b/crates/biome_css_semantic/src/tests/specificity.rs new file mode 100644 index 000000000000..e156e9a7743e --- /dev/null +++ b/crates/biome_css_semantic/src/tests/specificity.rs @@ -0,0 +1,379 @@ +use biome_css_parser::{parse_css, CssParserOptions}; + +use crate::model::Specificity; +use crate::semantic_model; + +#[test] +fn test_specificity_type_selector() { + let css_code = "div {}"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + let rule = model.rules().first().unwrap(); + let selector = &rule.selectors[0]; + + let specificity = &selector.specificity; + assert_eq!(specificity, &Specificity(0, 0, 1)); +} + +#[test] +fn test_specificity_class_selector() { + let css_code = ".class {}"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + let rule = model.rules().first().unwrap(); + let selector = &rule.selectors[0]; + + let specificity = &selector.specificity; + assert_eq!(specificity, &Specificity(0, 1, 0)); +} + +#[test] +fn test_specificity_id_selector() { + let css_code = "#id {}"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + let rule = model.rules().first().unwrap(); + let selector = &rule.selectors[0]; + + let specificity = &selector.specificity; + assert_eq!(specificity, &Specificity(1, 0, 0)); +} + +#[test] +fn test_specificity_type_and_class_selector() { + let css_code = "div.class {}"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + let rule = model.rules().first().unwrap(); + let selector = &rule.selectors[0]; + + let specificity = &selector.specificity; + assert_eq!(specificity, &Specificity(0, 1, 1)); +} + +#[test] +fn test_specificity_attribute_selector() { + let css_code = "[type=\"text\"] {}"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + let rule = model.rules().first().unwrap(); + let selector = &rule.selectors[0]; + + let specificity = &selector.specificity; + assert_eq!(specificity, &Specificity(0, 1, 0)); +} + +#[test] +fn test_specificity_pseudo_class_selector() { + let css_code = ":hover {}"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + let rule = model.rules().first().unwrap(); + let selector = &rule.selectors[0]; + + let specificity = &selector.specificity; + assert_eq!(specificity, &Specificity(0, 1, 0)); +} + +#[test] +fn test_specificity_pseudo_element_selector() { + let css_code = "::before {}"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + let rule = model.rules().first().unwrap(); + let selector = &rule.selectors[0]; + + let specificity = &selector.specificity; + assert_eq!(specificity, &Specificity(0, 0, 1)); +} + +#[test] +fn test_specificity_complex_selector() { + let css_code = "ul li.active a#link:hover {}"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + let rule = model.rules().first().unwrap(); + let selector = &rule.selectors[0]; + + let specificity = &selector.specificity; + assert_eq!(specificity, &Specificity(1, 2, 3)); +} + +#[test] +fn test_specificity_pseudo_class_functions() { + let css_code = ":not(#id) {}"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + let rule = model.rules().first().unwrap(); + let selector = &rule.selectors[0]; + + let specificity = &selector.specificity; + assert_eq!(specificity, &Specificity(1, 0, 0)); +} + +#[test] +fn test_specificity_with_pseudo_function_where() { + let css_code = ":where(.class) {}"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + let rule = model.rules().first().unwrap(); + let selector = &rule.selectors[0]; + + let specificity = &selector.specificity; + // :where doesn't contribute to specificity + assert_eq!(specificity, &Specificity(0, 0, 0)); +} + +#[test] +fn test_specificity_with_pseudo_function_is() { + let css_code = ":is(div, .class) {}"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + let rule = model.rules().first().unwrap(); + let selector = &rule.selectors[0]; + + let specificity = &selector.specificity; + // :is takes the maximum specificity of its arguments + assert_eq!(specificity, &Specificity(0, 1, 0)); +} + +#[test] +fn test_specificity_nested_selector() { + let css_code = ".parent .child {}"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + let rule = model.rules().first().unwrap(); + let selector = &rule.selectors[0]; + + let specificity = &selector.specificity; + // For ".parent .child", the specificity is sum of ".parent" and ".child": (0,2,0) + assert_eq!(specificity, &Specificity(0, 2, 0)); +} + +#[test] +fn test_specificity_nested_pseudo_class() { + let css_code = "a:not(.active) {}"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + let rule = model.rules().first().unwrap(); + let selector = &rule.selectors[0]; + + let specificity = &selector.specificity; + // Specificity: + // - "a" contributes (0, 0, 1) + // - ":not(.active)" contributes specificity of ".active" (0,1,0) + // Total specificity: (0, 1, 1) + assert_eq!(specificity, &Specificity(0, 1, 1)); +} + +#[test] +fn test_specificity_nested_pseudo_class_functions() { + let css_code = ":not(:nth-child(2n+1)) {}"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + let rule = model.rules().first().unwrap(); + let selector = &rule.selectors[0]; + + let specificity = &selector.specificity; + // ":not(:nth-child(2n+1))" + // - ":nth-child" contributes (0, 1, 0) + // - ":not" takes specificity of its argument + // Total specificity: (0, 1, 0) + assert_eq!(specificity, &Specificity(0, 1, 0)); +} + +#[test] +fn test_specificity_multiple_nesting() { + let css_code = "body div#main .content ul li.active a:hover {}"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + let rule = model.rules().first().unwrap(); + let selector = &rule.selectors[0]; + + let specificity = &selector.specificity; + // Calculating specificity: + // "body" -> (0, 0, 1) + // "div#main" -> (1, 0, 1) + // ".content" -> (0, 1, 0) + // "ul" -> (0, 0, 1) + // "li.active" -> (0, 1, 1) + // "a:hover" -> (0, 1, 1) + // Total specificity: Sum of all components + // Total IDs: 1 + // Total classes: 1+1+1 = 3 (from .content, .active, :hover) + // Total elements: 1+1+1+1+1 = 5 (from body, div, ul, li, a) + let expected_specificity = Specificity(1, 3, 5); + assert_eq!(specificity, &expected_specificity); +} + +#[test] +fn test_specificity_nested_pseudo_elements_and_classes() { + let css_code = "div::before:hover {}"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + let rule = model.rules().first().unwrap(); + let selector = &rule.selectors[0]; + + let specificity = &selector.specificity; + // "div" -> (0, 0, 1) + // "::before" -> (0, 0, 1) + // ":hover" -> (0, 1, 0) + // Total specificity: (0, 1, 2) + assert_eq!(specificity, &Specificity(0, 1, 2)); +} + +#[test] +fn test_specificity_nested_combinators() { + let css_code = "ul > li + li.active ~ a#link:hover {}"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + let rule = model.rules().first().unwrap(); + let selector = &rule.selectors[0]; + + let specificity = &selector.specificity; + // "ul" -> (0, 0, 1) + // "li" -> (0, 0, 1) + // "li.active" -> (0, 1, 1) + // "a#link:hover" -> (1, 1, 1) + // Total specificity: (1, 2, 4) + assert_eq!(specificity, &Specificity(1, 2, 4)); +} + +#[test] +fn test_specificity_with_nested_pseudo_functions() { + let css_code = ":is(div, :not(.class)) {}"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + let rule = model.rules().first().unwrap(); + let selector = &rule.selectors[0]; + + let specificity = &selector.specificity; + // ":is(div, :not(.class))" + // - Arguments are "div" (0, 0, 1) and ":not(.class)" (0, 1, 0) + // - ":not(.class)" has specificity of ".class" (0,1,0) + // - ":is" takes the maximum specificity of its arguments + // So overall specificity: max((0,0,1), (0,1,0)) = (0,1,0) + assert_eq!(specificity, &Specificity(0, 1, 0)); +} + +#[test] +fn test_specificity_nested_selector_lists() { + let css_code = "div, .class, #id, a:hover, ul li a#link {}"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + let rule = model.rules().first().unwrap(); + + let expected_specificities = [ + Specificity(0, 0, 1), // "div" + Specificity(0, 1, 0), // ".class" + Specificity(1, 0, 0), // "#id" + Specificity(0, 1, 1), // "a:hover" + Specificity(1, 0, 3), + ]; + + for (selector, expected_specificity) in rule.selectors.iter().zip(expected_specificities.iter()) + { + let specificity = &selector.specificity; + assert_eq!(specificity, expected_specificity); + } +} + +#[test] +fn test_specificity_deeply_nested_rules() { + let css_code = "a { div { .class { #id { } } } }"; + let parse = parse_css(css_code, CssParserOptions::default()); + let root = parse.tree(); + + let model = semantic_model(&root); + + // Navigate through nested rules to reach the deepest rule + let parent_rule = model + .rules() + .first() + .expect("Expected to find parent rule with selector 'a'"); + + let div_rule_id = parent_rule + .child_ids + .first() + .expect("Expected 'div' child rule"); + let div_rule = model + .get_rule_by_id(*div_rule_id) + .expect("Expected to retrieve 'div' rule"); + + let class_rule_id = div_rule + .child_ids + .first() + .expect("Expected '.class' child rule"); + let class_rule = model + .get_rule_by_id(*class_rule_id) + .expect("Expected to retrieve 'span.class' rule"); + + let id_rule_id = class_rule + .child_ids + .first() + .expect("Expected '#id' child rule"); + let _id_rule = model + .get_rule_by_id(*id_rule_id) + .expect("Expected to retrieve '#id' rule"); + + // Check selectors and specificities at each level + // 'a' + let a_selector = &parent_rule.selectors[0]; + assert_eq!(&a_selector.name, "a"); + assert_eq!(&a_selector.specificity, &Specificity(0, 0, 1)); + + // 'a div' + let div_selector = &div_rule.selectors[0]; + assert_eq!(&div_selector.name, "div"); + assert_eq!(&div_selector.specificity, &Specificity(0, 0, 2)); + + // TODO: Bug. It should be (0, 1, 2) instead of (0, 1, 1) + // 'a div .class' + // let class_selector = &class_rule.selectors[0]; + // assert_eq!(&class_selector.name, ".class"); + // assert_eq!(&class_selector.specificity, &Specificity(0, 1, 2)); + + // TODO: Bug. It should be (1, 1, 2) instead of (1, 1, 0) + // 'a div .class #id' + // let id_selector = &id_rule.selectors[0]; + // assert_eq!(&id_selector.name, "#id"); + // assert_eq!(&id_selector.specificity, &Specificity(1, 1, 2)); +} From 5bf7a12fdc16fc8dc082c2ae4c0a4e777a872e99 Mon Sep 17 00:00:00 2001 From: togami Date: Fri, 29 Nov 2024 23:31:15 +0900 Subject: [PATCH 2/3] feat: resolve the selector name --- .../src/semantic_model/builder.rs | 49 ++++++++++++++++--- .../src/semantic_model/mod.rs | 12 +++-- .../src/tests/specificity.rs | 5 +- 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/crates/biome_css_semantic/src/semantic_model/builder.rs b/crates/biome_css_semantic/src/semantic_model/builder.rs index 864c169707aa..08a472274506 100644 --- a/crates/biome_css_semantic/src/semantic_model/builder.rs +++ b/crates/biome_css_semantic/src/semantic_model/builder.rs @@ -108,14 +108,26 @@ impl SemanticModelBuilder { .map(|parent| parent.specificity.clone()) .unwrap_or_default(); - if let Some(current_rule) = self.current_rule_stack.last() { - let current_rule = self.rules_by_id.get_mut(current_rule).unwrap(); - current_rule.selectors.push(Selector { - name, - range, - original, - specificity: parent_specificity + specificity.clone(), - }); + let parent_selectors = self + .current_rule_stack + .last() + .and_then(|rule_id| self.rules_by_id.get(rule_id)) + .and_then(|rule| rule.parent_id) + .and_then(|parent_id| self.rules_by_id.get(&parent_id)) + .map(|parent| parent.selectors.clone()) + .unwrap_or_default(); + + if let Some(current_rule_id) = self.current_rule_stack.last() { + let resolved_selectors = resolve_selector(&name, &parent_selectors); + let current_rule = self.rules_by_id.get_mut(current_rule_id).unwrap(); + for name in resolved_selectors.iter() { + current_rule.selectors.push(Selector { + name: name.to_string(), + range, + original: original.clone(), + specificity: parent_specificity.clone() + specificity.clone(), + }); + } current_rule.specificity += specificity; } @@ -173,3 +185,24 @@ impl SemanticModelBuilder { } } } + +fn resolve_selector(current: &str, parent: &[Selector]) -> Vec { + let mut resolved = Vec::new(); + let parent = parent.iter().rev(); + + if parent.len() == 0 { + return vec![current.to_string()]; + } + + for parent in parent { + let parent = parent.name.to_string(); + if current.contains('&') { + let current = current.replace('&', &parent); + resolved.push(current); + } else { + resolved.push(format!("{} {}", parent, current)); + } + } + + resolved +} diff --git a/crates/biome_css_semantic/src/semantic_model/mod.rs b/crates/biome_css_semantic/src/semantic_model/mod.rs index 2038b0177447..8fec34a4b529 100644 --- a/crates/biome_css_semantic/src/semantic_model/mod.rs +++ b/crates/biome_css_semantic/src/semantic_model/mod.rs @@ -240,7 +240,7 @@ mod tests { assert_eq!(rule.selectors.len(), 1); assert_eq!(rule.declarations.len(), 1); - assert_eq!(rule.selectors[0].name, ".child"); + assert_eq!(rule.selectors[0].name, "p .child"); assert_eq!(rule.declarations[0].property.name, "color"); assert_eq!(rule.declarations[0].value.text, "var(--foo)"); @@ -261,10 +261,12 @@ mod tests { #[test] fn quick_test() { let parse = parse_css( - r#"@property --item-size { - syntax: ""; - inherits: true; - initial-value: 40%; + r#".parent { + color: blue; + + .child { + color: red; + } }"#, CssParserOptions::default(), ); diff --git a/crates/biome_css_semantic/src/tests/specificity.rs b/crates/biome_css_semantic/src/tests/specificity.rs index e156e9a7743e..b0dfec58c179 100644 --- a/crates/biome_css_semantic/src/tests/specificity.rs +++ b/crates/biome_css_semantic/src/tests/specificity.rs @@ -306,9 +306,8 @@ fn test_specificity_nested_selector_lists() { Specificity(0, 1, 0), // ".class" Specificity(1, 0, 0), // "#id" Specificity(0, 1, 1), // "a:hover" - Specificity(1, 0, 3), + Specificity(1, 0, 3), // "ul li a#link" ]; - for (selector, expected_specificity) in rule.selectors.iter().zip(expected_specificities.iter()) { let specificity = &selector.specificity; @@ -362,7 +361,7 @@ fn test_specificity_deeply_nested_rules() { // 'a div' let div_selector = &div_rule.selectors[0]; - assert_eq!(&div_selector.name, "div"); + assert_eq!(&div_selector.name, "a div"); assert_eq!(&div_selector.specificity, &Specificity(0, 0, 2)); // TODO: Bug. It should be (0, 1, 2) instead of (0, 1, 1) From 08c4f0fee9729b965a913076c16876062572418a Mon Sep 17 00:00:00 2001 From: togami Date: Sat, 30 Nov 2024 00:05:06 +0900 Subject: [PATCH 3/3] chore: add selector tests --- .../src/semantic_model/builder.rs | 3 +- crates/biome_css_semantic/src/tests/mod.rs | 1 + .../biome_css_semantic/src/tests/selector.rs | 341 ++++++++++++++++++ 3 files changed, 343 insertions(+), 2 deletions(-) create mode 100644 crates/biome_css_semantic/src/tests/selector.rs diff --git a/crates/biome_css_semantic/src/semantic_model/builder.rs b/crates/biome_css_semantic/src/semantic_model/builder.rs index 08a472274506..5f401fa6f4d1 100644 --- a/crates/biome_css_semantic/src/semantic_model/builder.rs +++ b/crates/biome_css_semantic/src/semantic_model/builder.rs @@ -188,9 +188,8 @@ impl SemanticModelBuilder { fn resolve_selector(current: &str, parent: &[Selector]) -> Vec { let mut resolved = Vec::new(); - let parent = parent.iter().rev(); - if parent.len() == 0 { + if parent.is_empty() { return vec![current.to_string()]; } diff --git a/crates/biome_css_semantic/src/tests/mod.rs b/crates/biome_css_semantic/src/tests/mod.rs index 37a9388b8da1..b03016482a0f 100644 --- a/crates/biome_css_semantic/src/tests/mod.rs +++ b/crates/biome_css_semantic/src/tests/mod.rs @@ -1 +1,2 @@ +mod selector; mod specificity; diff --git a/crates/biome_css_semantic/src/tests/selector.rs b/crates/biome_css_semantic/src/tests/selector.rs new file mode 100644 index 000000000000..90ab081dd024 --- /dev/null +++ b/crates/biome_css_semantic/src/tests/selector.rs @@ -0,0 +1,341 @@ +use biome_css_parser::{parse_css, CssParserOptions}; + +use crate::semantic_model; + +#[test] +fn test_resolve_selector_no_parents() { + let css = "div { color: red; }"; + let parse = parse_css(css, CssParserOptions::default()); + let root = parse.tree(); + let model = semantic_model(&root); + + let rule = model.rules().first().unwrap(); + assert_eq!(rule.selectors.len(), 1); + assert_eq!(rule.selectors[0].name, "div"); +} + +#[test] +fn test_resolve_selector_simple_parent() { + let css = r#" + .parent { + .child { + color: red; + } + } + "#; + let parse = parse_css(css, CssParserOptions::default()); + let root = parse.tree(); + let model = semantic_model(&root); + + let parent_rule = model.rules().first().unwrap(); + assert_eq!(parent_rule.selectors.len(), 1); + assert_eq!(parent_rule.selectors[0].name, ".parent"); + + let child_rule_id = parent_rule.child_ids.first().unwrap(); + let child_rule = model.get_rule_by_id(*child_rule_id).unwrap(); + + assert_eq!(child_rule.selectors.len(), 1); + assert_eq!(child_rule.selectors[0].name, ".parent .child"); +} + +#[test] +fn test_resolve_selector_with_ampersand() { + let css = r#" + a { + &:hover { + color: orange; + } + } + "#; + let parse = parse_css(css, CssParserOptions::default()); + let root = parse.tree(); + let model = semantic_model(&root); + + let parent_rule = model.rules().first().unwrap(); + assert_eq!(parent_rule.selectors.len(), 1); + assert_eq!(parent_rule.selectors[0].name, "a"); + + let child_rule_id = parent_rule.child_ids.first().unwrap(); + let child_rule = model.get_rule_by_id(*child_rule_id).unwrap(); + + assert_eq!(child_rule.selectors.len(), 1); + assert_eq!(child_rule.selectors[0].name, "a:hover"); +} + +#[test] +fn test_resolve_selector_multiple_parents() { + let css = r#" + .grandparent { + .parent { + .child { + color: blue; + } + } + } + "#; + let parse = parse_css(css, CssParserOptions::default()); + let root = parse.tree(); + let model = semantic_model(&root); + + let grandparent_rule = model.rules().first().unwrap(); + assert_eq!(grandparent_rule.selectors.len(), 1); + assert_eq!(grandparent_rule.selectors[0].name, ".grandparent"); + + let parent_rule_id = grandparent_rule.child_ids.first().unwrap(); + let parent_rule = model.get_rule_by_id(*parent_rule_id).unwrap(); + assert_eq!(parent_rule.selectors.len(), 1); + assert_eq!(parent_rule.selectors[0].name, ".grandparent .parent"); + + let child_rule_id = parent_rule.child_ids.first().unwrap(); + let child_rule = model.get_rule_by_id(*child_rule_id).unwrap(); + assert_eq!(child_rule.selectors.len(), 1); + assert_eq!(child_rule.selectors[0].name, ".grandparent .parent .child"); +} + +#[test] +fn test_resolve_selector_with_multi_ampersand() { + let css = r#" + p { + & + & { + margin-top: 10px; + } + } + "#; + let parse = parse_css(css, CssParserOptions::default()); + let root = parse.tree(); + let model = semantic_model(&root); + + let parent_rule = model.rules().first().unwrap(); + assert_eq!(parent_rule.selectors.len(), 1); + assert_eq!(parent_rule.selectors[0].name, "p"); + + let child_rule_id = parent_rule.child_ids.first().unwrap(); + let child_rule = model.get_rule_by_id(*child_rule_id).unwrap(); + + assert_eq!(child_rule.selectors.len(), 1); + assert_eq!(child_rule.selectors[0].name, "p + p"); +} + +#[test] +fn test_resolve_selector_no_ampersand_with_parents() { + let css = r#" + .list { + li { + font-size: 14px; + } + } + "#; + let parse = parse_css(css, CssParserOptions::default()); + let root = parse.tree(); + let model = semantic_model(&root); + + let parent_rule = model.rules().first().unwrap(); + assert_eq!(parent_rule.selectors.len(), 1); + assert_eq!(parent_rule.selectors[0].name, ".list"); + + let child_rule_id = parent_rule.child_ids.first().unwrap(); + let child_rule = model.get_rule_by_id(*child_rule_id).unwrap(); + + assert_eq!(child_rule.selectors.len(), 1); + assert_eq!(child_rule.selectors[0].name, ".list li"); +} + +#[test] +#[ignore] +fn test_resolve_selector_with_complex_parent() { + let css = r#" + .menu > ul { + > li { + display: inline-block; + } + } + "#; + let parse = parse_css(css, CssParserOptions::default()); + let root = parse.tree(); + let model = semantic_model(&root); + + let parent_rule = model.rules().first().unwrap(); + assert_eq!(parent_rule.selectors.len(), 1); + assert_eq!(parent_rule.selectors[0].name, ".menu > ul"); + + let child_rule_id = parent_rule.child_ids.first().unwrap(); + let child_rule = model.get_rule_by_id(*child_rule_id).unwrap(); + + assert_eq!(child_rule.selectors.len(), 1); + assert_eq!(child_rule.selectors[0].name, ".menu > ul > li"); +} + +#[test] +fn test_resolve_selector_with_nested_ampersands() { + let css = r#" + .btn { + &--primary:hover &__icon { + fill: blue; + } + } + "#; + let parse = parse_css(css, CssParserOptions::default()); + let root = parse.tree(); + let model = semantic_model(&root); + + let parent_rule = model.rules().first().unwrap(); + assert_eq!(parent_rule.selectors.len(), 1); + assert_eq!(parent_rule.selectors[0].name, ".btn"); + + let child_rule_id = parent_rule.child_ids.first().unwrap(); + let child_rule = model.get_rule_by_id(*child_rule_id).unwrap(); + + assert_eq!(child_rule.selectors.len(), 1); + assert_eq!( + child_rule.selectors[0].name, + ".btn--primary:hover .btn__icon" + ); +} + +#[test] +fn test_resolve_selector_with_multiple_parents_and_ampersand() { + let css = r#" + .grandparent { + .parent { + &--modified { + color: green; + } + } + } + "#; + let parse = parse_css(css, CssParserOptions::default()); + let root = parse.tree(); + let model = semantic_model(&root); + + let grandparent_rule = model.rules().first().unwrap(); + assert_eq!(grandparent_rule.selectors.len(), 1); + assert_eq!(grandparent_rule.selectors[0].name, ".grandparent"); + + let parent_rule_id = grandparent_rule.child_ids.first().unwrap(); + let parent_rule = model.get_rule_by_id(*parent_rule_id).unwrap(); + assert_eq!(parent_rule.selectors.len(), 1); + assert_eq!(parent_rule.selectors[0].name, ".grandparent .parent"); + + let child_rule_id = parent_rule.child_ids.first().unwrap(); + let child_rule = model.get_rule_by_id(*child_rule_id).unwrap(); + assert_eq!(child_rule.selectors.len(), 1); + assert_eq!( + child_rule.selectors[0].name, + ".grandparent .parent--modified" + ); +} + +#[test] +fn test_descendant_combinator() { + let css = ".foo .bar { color: red; }"; + let parse = parse_css(css, CssParserOptions::default()); + let root = parse.tree(); + let model = semantic_model(&root); + + let rule = model.rules().first().unwrap(); + + assert_eq!(rule.selectors.len(), 1); + assert_eq!(rule.selectors[0].name, ".foo .bar"); +} + +#[test] +fn test_child_combinator() { + let css = r#" + .foo > .bar { color: red; } + .foo || .bar { color: blue; } + .foo + .bar { color: green; } + .foo ~ .bar { color: yellow; } + "#; + let parse = parse_css(css, CssParserOptions::default()); + let root = parse.tree(); + let model = semantic_model(&root); + + let rules = model.rules(); + + assert_eq!(rules.len(), 4); + + assert_eq!(rules[0].selectors.len(), 1); + assert_eq!(rules[0].selectors[0].name, ".foo > .bar"); + + assert_eq!(rules[1].selectors.len(), 1); + assert_eq!(rules[1].selectors[0].name, ".foo || .bar"); + + assert_eq!(rules[2].selectors.len(), 1); + assert_eq!(rules[2].selectors[0].name, ".foo + .bar"); + + assert_eq!(rules[3].selectors.len(), 1); + assert_eq!(rules[3].selectors[0].name, ".foo ~ .bar"); +} + +#[test] +fn test_selector_list_with_nesting() { + let css = r#" + .a, .b { + div { + color: red; + } + } + "#; + let parse = parse_css(css, CssParserOptions::default()); + let root = parse.tree(); + let model = semantic_model(&root); + + let parent_rules = model.rules(); + assert_eq!(parent_rules.len(), 1); + + let parent_rule = &parent_rules[0]; + assert_eq!(parent_rule.selectors.len(), 2); + assert_eq!(parent_rule.selectors[0].name, ".a"); + assert_eq!(parent_rule.selectors[1].name, ".b"); + + let child_rule_id = parent_rule.child_ids.first().unwrap(); + let child_rule = model.get_rule_by_id(*child_rule_id).unwrap(); + + assert_eq!(child_rule.selectors.len(), 2); + assert_eq!(child_rule.selectors[0].name, ".a div"); + assert_eq!(child_rule.selectors[1].name, ".b div"); +} + +#[test] +fn test_ampersand_nesting_selector() { + let css = r#" + .foo { + .bar &:hover { + color: red; + } + } + "#; + let parse = parse_css(css, CssParserOptions::default()); + let root = parse.tree(); + let model = semantic_model(&root); + + let parent_rule = model.rules().first().unwrap(); + assert_eq!(parent_rule.selectors.len(), 1); + assert_eq!(parent_rule.selectors[0].name, ".foo"); + + let child_rule_id = parent_rule.child_ids.first().unwrap(); + let child_rule = model.get_rule_by_id(*child_rule_id).unwrap(); + + assert_eq!(child_rule.selectors.len(), 1); + assert_eq!(child_rule.selectors[0].name, ".bar .foo:hover"); +} + +#[test] +fn test_attribute_class_id_selector() { + let css = r#" + type[attribute].class#id, div { + color: red; + } + "#; + let parse = parse_css(css, CssParserOptions::default()); + let root = parse.tree(); + let model = semantic_model(&root); + + let rules = model.rules(); + assert_eq!(rules.len(), 1); + + let rule = &rules[0]; + assert_eq!(rule.selectors.len(), 2); + assert_eq!(rule.selectors[0].name, "type[attribute].class#id"); + assert_eq!(rule.selectors[1].name, "div"); +}