Skip to content

Add CSS :where() selector support to jsar-runtime #99

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions crates/jsbindings/css/selectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub(crate) struct Component {
pub combinator: Option<crate::css_parser::ffi::SelectorComponentCombinator>,
pub name: Option<String>,
pub pseudo_class_type: Option<crate::css_parser::ffi::PseudoClassType>,
pub selector_list: Option<SelectorList>, // For functional pseudo-classes like :where()
}

impl Component {
Expand All @@ -30,6 +31,8 @@ impl Component {
ComponentImpl::PseudoElement(_) => SelectorComponentType::PseudoElement,
ComponentImpl::NonTSPseudoClass(_) => SelectorComponentType::PseudoClass,
ComponentImpl::Combinator(_) => SelectorComponentType::Combinator,
ComponentImpl::Is(_) => SelectorComponentType::PseudoClass, // :is() support
ComponentImpl::Where(_) => SelectorComponentType::PseudoClass, // :where() support
_ => SelectorComponentType::Unsupported,
},
combinator: match handle {
Expand All @@ -52,6 +55,14 @@ impl Component {
NonTSPseudoClass::FocusWithin => PseudoClassType::FocusWithin,
_ => PseudoClassType::Unknown,
}),
ComponentImpl::Where(_) => Some(PseudoClassType::Where),
ComponentImpl::Is(_) => Some(PseudoClassType::Is),
_ => None,
},

selector_list: match handle {
ComponentImpl::Where(selector_list) => Some(SelectorList::new(selector_list)),
ComponentImpl::Is(selector_list) => Some(SelectorList::new(selector_list)), // Also support :is() while we're at it
_ => None,
},
}
Expand Down
198 changes: 198 additions & 0 deletions crates/jsbindings/css_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,10 @@ pub(crate) mod ffi {
FocusWithin,
#[cxx_name = "kTargetCurrent"]
TargetCurrent,
#[cxx_name = "kWhere"]
Where,
#[cxx_name = "kIs"]
Is,
#[cxx_name = "kUnknown"]
Unknown,
#[cxx_name = "kUnset"]
Expand Down Expand Up @@ -899,6 +903,11 @@ pub(crate) mod ffi {
#[cxx_name = "getComponentPseudoClassType"]
fn get_component_pseudo_class_type(component: &PrismComponent) -> PseudoClassType;

/// Returns the selector list for functional pseudo-classes like :where() and :is().
/// Returns None if the component is not a functional pseudo-class.
#[cxx_name = "getComponentSelectorList"]
fn get_component_selector_list(component: &PrismComponent) -> Result<Box<PrismSelectorList>>;

/// Returns the component list length.
#[cxx_name = "getComponentListLength"]
fn get_component_list_len(list: &PrismComponentList) -> usize;
Expand Down Expand Up @@ -1359,6 +1368,14 @@ fn get_component_pseudo_class_type(component: &PrismComponent) -> ffi::PseudoCla
}
}

fn get_component_selector_list(component: &PrismComponent) -> Result<Box<PrismSelectorList>, String> {
if let Some(selector_list) = &component.selector_list {
Ok(Box::new(selector_list.clone()))
} else {
Err("Component is not a functional pseudo-class".into())
}
}

fn get_component_list_len(list: &PrismComponentList) -> usize {
list.len()
}
Expand Down Expand Up @@ -1627,6 +1644,187 @@ mod tests {
}
}

#[test]
fn test_functional_pseudo_classes() {
let parser = CSSParser::default();

// Test :is() selector
let selectors = parser.parse_selectors(":is(h1, h2, h3)");
assert!(selectors.is_some());
let selectors = selectors.unwrap();
assert_eq!(selectors.len(), 1);

let selector = selectors.item(0).unwrap();
let component = selector.components.item(0).unwrap();
assert_eq!(component.pseudo_class_type, Some(crate::css_parser::ffi::PseudoClassType::Is));

let inner_selectors = component.selector_list.as_ref().unwrap();
assert_eq!(inner_selectors.len(), 3);

// Test combination of :where() and :is()
let selectors = parser.parse_selectors("div:where(.a, .b):is(:hover, :focus)");
assert!(selectors.is_some());
let selectors = selectors.unwrap();
assert_eq!(selectors.len(), 1);

let selector = selectors.item(0).unwrap();
assert_eq!(selector.components.len(), 3); // div, :where(), :is()

let div_component = selector.components.item(0).unwrap();
assert_eq!(div_component.name.as_ref().unwrap(), "div");

let where_component = selector.components.item(1).unwrap();
assert_eq!(where_component.pseudo_class_type, Some(crate::css_parser::ffi::PseudoClassType::Where));

let is_component = selector.components.item(2).unwrap();
assert_eq!(is_component.pseudo_class_type, Some(crate::css_parser::ffi::PseudoClassType::Is));

println!("✓ All functional pseudo-class tests passed!");
}

#[test]
fn test_where_selector() {
let parser = CSSParser::default();

// Test basic :where() selector with two simple selectors
let selectors = parser.parse_selectors(":where(div, span)");
assert!(selectors.is_some());
let selectors = selectors.unwrap();
assert_eq!(selectors.len(), 1);

let selector = selectors.item(0).unwrap();
assert_eq!(selector.components.len(), 1);

let component = selector.components.item(0).unwrap();
assert_eq!(component.tag, crate::css_parser::ffi::SelectorComponentType::PseudoClass);
assert_eq!(component.pseudo_class_type, Some(crate::css_parser::ffi::PseudoClassType::Where));

// Check that :where() contains the expected inner selectors
assert!(component.selector_list.is_some());
let inner_selectors = component.selector_list.as_ref().unwrap();
assert_eq!(inner_selectors.len(), 2);

// First inner selector should be "div"
let div_selector = inner_selectors.item(0).unwrap();
let div_component = div_selector.components.item(0).unwrap();
assert_eq!(div_component.tag, crate::css_parser::ffi::SelectorComponentType::LocalName);
assert_eq!(div_component.name.as_ref().unwrap(), "div");

// Second inner selector should be "span"
let span_selector = inner_selectors.item(1).unwrap();
let span_component = span_selector.components.item(0).unwrap();
assert_eq!(span_component.tag, crate::css_parser::ffi::SelectorComponentType::LocalName);
assert_eq!(span_component.name.as_ref().unwrap(), "span");

// Test :where() with class selectors
let selectors = parser.parse_selectors("p:where(.class1, .class2)");
assert!(selectors.is_some());
let selectors = selectors.unwrap();
assert_eq!(selectors.len(), 1);

let selector = selectors.item(0).unwrap();
assert_eq!(selector.components.len(), 2);

// First component should be "p"
let p_component = selector.components.item(0).unwrap();
assert_eq!(p_component.tag, crate::css_parser::ffi::SelectorComponentType::LocalName);
assert_eq!(p_component.name.as_ref().unwrap(), "p");

// Second component should be :where(.class1, .class2)
let where_component = selector.components.item(1).unwrap();
assert_eq!(where_component.tag, crate::css_parser::ffi::SelectorComponentType::PseudoClass);
assert_eq!(where_component.pseudo_class_type, Some(crate::css_parser::ffi::PseudoClassType::Where));

let inner_selectors = where_component.selector_list.as_ref().unwrap();
assert_eq!(inner_selectors.len(), 2);

// Check inner class selectors
let class1_selector = inner_selectors.item(0).unwrap();
let class1_component = class1_selector.components.item(0).unwrap();
assert_eq!(class1_component.tag, crate::css_parser::ffi::SelectorComponentType::Class);
assert_eq!(class1_component.name.as_ref().unwrap(), "class1");

let class2_selector = inner_selectors.item(1).unwrap();
let class2_component = class2_selector.components.item(0).unwrap();
assert_eq!(class2_component.tag, crate::css_parser::ffi::SelectorComponentType::Class);
assert_eq!(class2_component.name.as_ref().unwrap(), "class2");

// Test nested :where() selectors
let selectors = parser.parse_selectors(":where(:where(div), span)");
assert!(selectors.is_some());
let selectors = selectors.unwrap();
assert_eq!(selectors.len(), 1);

let selector = selectors.item(0).unwrap();
assert_eq!(selector.components.len(), 1);

let outer_where = selector.components.item(0).unwrap();
assert_eq!(outer_where.pseudo_class_type, Some(crate::css_parser::ffi::PseudoClassType::Where));

let outer_inner_selectors = outer_where.selector_list.as_ref().unwrap();
assert_eq!(outer_inner_selectors.len(), 2);

// First inner selector should be another :where(div)
let nested_where_selector = outer_inner_selectors.item(0).unwrap();
let nested_where_component = nested_where_selector.components.item(0).unwrap();
assert_eq!(nested_where_component.pseudo_class_type, Some(crate::css_parser::ffi::PseudoClassType::Where));

let nested_inner_selectors = nested_where_component.selector_list.as_ref().unwrap();
assert_eq!(nested_inner_selectors.len(), 1);

let div_nested_selector = nested_inner_selectors.item(0).unwrap();
let div_nested_component = div_nested_selector.components.item(0).unwrap();
assert_eq!(div_nested_component.name.as_ref().unwrap(), "div");

println!("✓ All :where() selector tests passed!");
}

#[test]
fn test_where_selector_in_stylesheet() {
let css_text = r#"
:where(h1, h2, h3) {
margin: 0;
padding: 0;
}

.container :where(.button, .link) {
color: blue;
}

p:where(.highlight, .important) {
font-weight: bold;
}
"#;

let sheet = CSSParser::default().parse_stylesheet(css_text, "");
assert!(sheet.rules_len() > 0);

// Check that rules with :where() selectors are parsed correctly
for i in 0..sheet.rules_len() {
if let Some(rule) = sheet.get_rule(i) {
match rule {
crate::css::stylesheets::CssRule::Style(style_rule) => {
let selectors = &style_rule.selectors;
// Just ensure selectors are parsed without errors
assert!(selectors.len() > 0);

for j in 0..selectors.len() {
if let Some(selector) = selectors.item(j) {
// Verify selector components exist
assert!(selector.components.len() > 0);
}
}
}
_ => {
// Skip other rule types
}
}
}
}

println!("✓ :where() selector in stylesheet parsing works correctly!");
}

#[test]
fn test_parse_selectors() {
let parser = CSSParser::default();
Expand Down
1 change: 0 additions & 1 deletion crates/jsbindings/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#![allow(unused_variables)]
#![allow(clippy::uninlined_format_args)]
#![allow(deprecated)]
#![feature(concat_idents)]

extern crate ctor;
extern crate jsar_jsbinding_macro;
Expand Down