diff --git a/Libraries/LibWeb/CSS/Rust/build.rs b/Libraries/LibWeb/CSS/Rust/build.rs index e1f928161dcd6..823215a4d1b07 100644 --- a/Libraries/LibWeb/CSS/Rust/build.rs +++ b/Libraries/LibWeb/CSS/Rust/build.rs @@ -562,6 +562,13 @@ fn main() -> Result<(), Box> { ("FfiSimpleSelector", "SimpleSelector"), ("FfiCompoundSelector", "CompoundSelector"), ("FfiSelector", "Selector"), + ("FfiElement", "Element"), + ("FfiElementQualifiedName", "ElementQualifiedName"), + ("FfiInternedStringList", "InternedStringList"), + ("FfiDomStringView", "DomStringView"), + ("FfiDomAttribute", "DomAttribute"), + ("FfiResolvedNamespaceType", "ResolvedNamespaceType"), + ("FfiResolvedNamespace", "ResolvedNamespace"), ("FfiElementAndShadowHost", "ElementAndShadowHost"), ] { selector_config diff --git a/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs b/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs index 269cfb1a109fd..73eec87212f42 100644 --- a/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs +++ b/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs @@ -53,8 +53,7 @@ define_ffi_ops! { StyleGroupCloneEntry => "styleGroupCloneEntries", StyleGroupFreeEntry => "styleGroupFreeEntries", // Callbacks: Rust -> C++. - SelectorSimpleSelectorCallback => "selectorSimpleSelectorCallbacks", - SelectorTreeNavigationCallback => "selectorTreeNavigationCallbacks", + SelectorDomReadCallback => "selectorDomReadCallbacks", SelectorMetadataCallback => "selectorMetadataCallbacks", CascadePropertyDisallowedCallback => "cascadePropertyDisallowedCallbacks", CascadeResolveUnresolvedCallback => "cascadeResolveUnresolvedCallbacks", diff --git a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs index d9a862055022e..a39718494f8dd 100644 --- a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs +++ b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs @@ -65,16 +65,17 @@ pub struct QualifiedName { pub namespace: SelectorString, pub name: SelectorString, pub lowercase_name: SelectorString, - /// Pointer to the C++ simple selector this was compiled from, so that matching callbacks can - /// compare its interned strings without copying. Null in unit tests. - cxx_simple_selector: RetainedCxxPointer, + interned_name: Option, + interned_lowercase_name: Option, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct NameSelector { pub name: SelectorString, - /// See [`QualifiedName::cxx_simple_selector`]. - cxx_simple_selector: RetainedCxxPointer, + /// The one-word identity of the C++ `Utf16FlyString` backing `name`. This is present for + /// selectors compiled from C++ and allows the live DOM wrapper to compare interned names + /// without crossing the FFI. + interned_name: Option, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -149,9 +150,8 @@ pub struct PseudoClassSelector { pub languages: Box<[SelectorString]>, pub direction: Option, pub identifier: Option, + pub identifier_identity: Option, pub levels: Box<[i64]>, - /// See [`QualifiedName::cxx_simple_selector`]. - cxx_simple_selector: RetainedCxxPointer, } impl PseudoClassSelector { @@ -164,8 +164,8 @@ impl PseudoClassSelector { languages: Box::new([]), direction: None, identifier: None, + identifier_identity: None, levels: Box::new([]), - cxx_simple_selector: RetainedCxxPointer::default(), } } } @@ -1479,6 +1479,37 @@ pub enum FfiDirection { Other, } +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +// NB: Constructed by C++ through the FFI. +#[allow(dead_code)] +pub enum FfiMeterValueState { + NotMeter, + EvenLessGood, + Suboptimal, + Optimal, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +// NB: Constructed by C++ through the FFI. +#[allow(dead_code)] +pub enum FfiRequiredState { + NotApplicable, + Optional, + Required, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +// NB: Constructed by C++ through the FFI. +#[allow(dead_code)] +pub enum FfiValidityState { + NotApplicable, + Invalid, + Valid, +} + #[derive(Clone, Copy)] #[repr(u8)] // NB: Constructed by C++ through the FFI. @@ -1511,7 +1542,8 @@ pub struct FfiStringView { #[repr(C)] pub struct FfiSimpleSelector { pub selector_type: FfiSimpleSelectorType, - pub cxx_simple_selector: *const c_void, + pub interned_name: *const usize, + pub interned_lowercase_name: *const usize, pub namespace_type: NamespaceType, pub namespace: FfiStringView, pub name: FfiStringView, @@ -1558,6 +1590,466 @@ pub struct RustSelector { selector: Rc, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(C)] +/// A DOM element borrowed for one selector-matching call. +/// +/// This handle contains no DOM-derived state. All facts are read from C++ on demand so mutations +/// observed by later matching calls cannot be hidden by retained data. +pub struct FfiElement { + pub pointer: *const c_void, +} + +impl FfiElement { + fn qualified_name(self) -> FfiElementQualifiedName { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_qualified_name(self.pointer) } + } + + fn id(self) -> Option { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element. A non-null result points at its + // current pinned `Utf16FlyString` storage for this matching call. + unsafe { selector_ffi_element_id(self.pointer).as_ref().copied() } + } + + unsafe fn id_value<'a>(self) -> DomStringView<'a> { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element. C++ returns its current ID with + // backing storage owned by the element for this matching call. + DomStringView { + // SAFETY: C++ guarantees that the returned string view remains valid for matching. + view: unsafe { selector_ffi_element_id_value(self.pointer) }, + marker: PhantomData, + } + } + + fn id_and_class_names_are_case_insensitive(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_id_and_class_names_are_case_insensitive(self.pointer) } + } + + fn namespace_is_null(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_namespace_is_null(self.pointer) } + } + + fn is_html_element_in_html_document(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_html_element_in_html_document(self.pointer) } + } + + fn is_document_root(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_document_root(self.pointer) } + } + + fn is_link(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_link(self.pointer) } + } + + fn is_fullscreen(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_fullscreen(self.pointer) } + } + + fn heading_level(self) -> Option { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + match unsafe { selector_ffi_element_heading_level(self.pointer) } { + 0 => None, + level => Some(level), + } + } + + fn has_popover_attribute(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_has_popover_attribute(self.pointer) } + } + + fn popover_is_showing(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_popover_is_showing(self.pointer) } + } + + fn direction(self) -> Direction { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + match unsafe { selector_ffi_element_direction(self.pointer) } { + FfiDirection::LeftToRight => Direction::LeftToRight, + FfiDirection::RightToLeft => Direction::RightToLeft, + FfiDirection::None | FfiDirection::Other => unreachable!(), + } + } + + fn has_custom_state(self, state: usize) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element and `state` is the copied identity of + // an interned selector identifier. + unsafe { selector_ffi_element_has_custom_state(self.pointer, state) } + } + + unsafe fn language<'a>(self) -> Option> { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element. C++ returns its current language with + // backing storage owned by the element for this matching call. + let view = unsafe { selector_ffi_element_language(self.pointer) }; + (view.length != 0).then_some(DomStringView { + view, + marker: PhantomData, + }) + } + + fn is_focused(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_focused(self.pointer) } + } + + fn should_indicate_focus(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_should_indicate_focus(self.pointer) } + } + + fn has_focus_within(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_has_focus_within(self.pointer) } + } + + fn is_active(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_active(self.pointer) } + } + + fn is_checked(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_checked(self.pointer) } + } + + fn is_defined(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_defined(self.pointer) } + } + + fn is_disabled(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_disabled(self.pointer) } + } + + fn is_enabled(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_enabled(self.pointer) } + } + + fn is_local_link(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_local_link(self.pointer) } + } + + fn is_placeholder_shown(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_placeholder_shown(self.pointer) } + } + + fn is_target(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_target(self.pointer) } + } + + fn is_unchecked(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_unchecked(self.pointer) } + } + + fn is_media_element(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_media_element(self.pointer) } + } + + fn media_is_blocked(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_media_is_blocked(self.pointer) } + } + + fn media_is_muted(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_media_is_muted(self.pointer) } + } + + fn media_is_paused(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_media_is_paused(self.pointer) } + } + + fn media_is_seeking(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_media_is_seeking(self.pointer) } + } + + fn media_is_stalled(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_media_is_stalled(self.pointer) } + } + + fn is_default(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_default(self.pointer) } + } + + fn meter_value_state(self) -> FfiMeterValueState { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_meter_value_state(self.pointer) } + } + + fn meter_value_is_high(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_meter_value_is_high(self.pointer) } + } + + fn meter_value_is_low(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_meter_value_is_low(self.pointer) } + } + + fn is_hovered(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_hovered(self.pointer) } + } + + fn is_indeterminate(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_indeterminate(self.pointer) } + } + + fn validity_state(self) -> FfiValidityState { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_validity_state(self.pointer) } + } + + fn is_modal(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_modal(self.pointer) } + } + + fn is_open(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_open(self.pointer) } + } + + fn required_state(self) -> FfiRequiredState { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_required_state(self.pointer) } + } + + fn is_read_write(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_read_write(self.pointer) } + } + + fn user_validity_state(self) -> FfiValidityState { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_user_validity_state(self.pointer) } + } + + unsafe fn local_name<'a>(self) -> DomStringView<'a> { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element. C++ returns a string view borrowed + // from its current local name for this matching call. + DomStringView { + // SAFETY: C++ guarantees that the returned string view remains valid for matching. + view: unsafe { selector_ffi_element_local_name(self.pointer) }, + marker: PhantomData, + } + } + + unsafe fn class_name<'a>(self, index: usize) -> DomStringView<'a> { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element and `index` identifies one of its + // current classes. C++ returns a view borrowed for this matching call. + DomStringView { + // SAFETY: The caller guarantees that `index` is within the current class list. + view: unsafe { selector_ffi_element_class_name(self.pointer, index) }, + marker: PhantomData, + } + } + + fn attribute_count(self) -> usize { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_attribute_count(self.pointer) } + } + + unsafe fn attribute<'a>(self, index: usize) -> DomAttribute<'a> { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element and `index` identifies one of its + // current attributes. C++ returns its facts and a value view borrowed for matching. + DomAttribute { + // SAFETY: The caller guarantees that `index` is within the current attribute list. + attribute: unsafe { selector_ffi_element_attribute(self.pointer, index) }, + marker: PhantomData, + } + } + + unsafe fn classes<'a>(self) -> &'a [usize] { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element. C++ returns its current class storage, + // borrowed for this matching call. + let classes = unsafe { selector_ffi_element_classes(self.pointer) }; + if classes.count == 0 { + return &[]; + } + assert!(!classes.data.is_null()); + // SAFETY: C++ guarantees that `Utf16FlyString` has the size and alignment of `usize` and + // that the element's class vector cannot mutate during this matching call. + unsafe { std::slice::from_raw_parts(classes.data, classes.count) } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(C)] +pub struct FfiElementQualifiedName { + pub local_name: *const usize, + pub namespace_: *const usize, +} + +impl FfiElementQualifiedName { + fn local_name(self) -> Option { + // SAFETY: C++ points at the live element's pinned `Utf16FlyString` storage. + unsafe { self.local_name.as_ref().copied() } + } + + fn namespace(self) -> Option { + // SAFETY: C++ points at the live element's pinned `Utf16FlyString` storage. + unsafe { self.namespace_.as_ref().copied() } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(C)] +pub struct FfiInternedStringList { + pub data: *const usize, + pub count: usize, +} + +#[derive(Clone, Copy)] +#[repr(C)] +pub struct FfiDomStringView { + pub data: *const c_void, + pub length: usize, + pub is_ascii: bool, +} + +#[derive(Clone, Copy)] +#[repr(C)] +pub struct FfiDomAttribute { + pub local_name: usize, + pub namespace_: usize, + pub has_namespace: bool, + pub value: FfiDomStringView, +} + +#[derive(Clone, Copy)] +struct DomAttribute<'a> { + attribute: FfiDomAttribute, + marker: PhantomData<&'a FfiCallScope>, +} + +impl<'a> DomAttribute<'a> { + fn local_name(self) -> usize { + self.attribute.local_name + } + + fn namespace(self) -> Option { + self.attribute.has_namespace.then_some(self.attribute.namespace_) + } + + fn value(self) -> DomStringView<'a> { + DomStringView { + view: self.attribute.value, + marker: PhantomData, + } + } +} + +#[derive(Clone, Copy)] +struct DomStringView<'a> { + view: FfiDomStringView, + marker: PhantomData<&'a FfiCallScope>, +} + +impl DomStringView<'_> { + fn len(self) -> usize { + self.view.length + } + + fn code_unit_at(self, index: usize) -> u16 { + assert!(index < self.len()); + assert!(!self.view.data.is_null()); + if self.view.is_ascii { + // SAFETY: C++ guarantees that an ASCII view points at `length` bytes. + return u16::from(unsafe { *(self.view.data.cast::().add(index)) }); + } + // SAFETY: C++ guarantees that a UTF-16 view points at `length` aligned code units. + unsafe { *(self.view.data.cast::().add(index)) } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u8)] +// NB: Constructed by C++ through the FFI. +#[allow(dead_code)] +pub enum FfiResolvedNamespaceType { + Missing, + Null, + Named, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(C)] +pub struct FfiResolvedNamespace { + pub namespace_type: FfiResolvedNamespaceType, + pub namespace_: usize, +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum FfiNodeKind { Element, @@ -1588,50 +2080,126 @@ impl FfiNode<'_> { assert_eq!(self.kind, FfiNodeKind::Element); self.pointer } + + fn as_element(self) -> FfiElement { + assert_eq!(self.kind, FfiNodeKind::Element); + FfiElement { pointer: self.pointer } + } +} + +impl<'a> FfiNode<'a> { + fn id_value(self) -> DomStringView<'a> { + // SAFETY: `FfiNode` cannot outlive the call scope which pins the C++ element and its + // current ID storage. + unsafe { self.as_element().id_value() } + } + + fn classes(self) -> &'a [usize] { + // SAFETY: `FfiNode` cannot outlive the call scope which pins the C++ element and its + // current class storage. + unsafe { self.as_element().classes() } + } + + fn local_name(self) -> DomStringView<'a> { + // SAFETY: `FfiNode` cannot outlive the call scope which pins the C++ element and its + // current local-name storage. + unsafe { self.as_element().local_name() } + } + + fn class_name(self, index: usize) -> DomStringView<'a> { + // SAFETY: `FfiNode` cannot outlive the call scope which pins the C++ element and its + // current class-name storage. Callers obtain `index` from the same live class list. + unsafe { self.as_element().class_name(index) } + } + + fn attribute_count(self) -> usize { + self.as_element().attribute_count() + } + + fn attribute(self, index: usize) -> DomAttribute<'a> { + // SAFETY: `FfiNode` cannot outlive the call scope which pins the C++ element and its + // current attribute storage. Callers obtain `index` from the live attribute count. + unsafe { self.as_element().attribute(index) } + } + + fn language(self) -> Option> { + // SAFETY: `FfiNode` cannot outlive the call scope which pins the C++ element and its + // current language storage. + unsafe { self.as_element().language() } + } } #[derive(Clone, Copy)] #[repr(C)] pub struct FfiElementAndShadowHost { - pub element: *const c_void, - pub shadow_host: *const c_void, + pub element: FfiElement, + pub shadow_host: FfiElement, } unsafe extern "C" { - fn selector_ffi_matches_universal( - context: *mut c_void, - element: *const c_void, - cxx_simple_selector: *const c_void, - ) -> bool; - fn selector_ffi_matches_tag_name( - context: *mut c_void, - element: *const c_void, - cxx_simple_selector: *const c_void, - matching_mode: TagNameMatchingMode, - ) -> bool; - fn selector_ffi_matches_id(element: *const c_void, cxx_simple_selector: *const c_void) -> bool; - fn selector_ffi_matches_class(element: *const c_void, cxx_simple_selector: *const c_void) -> bool; - fn selector_ffi_matches_attribute( - context: *mut c_void, - element: *const c_void, - cxx_simple_selector: *const c_void, - ) -> bool; - fn selector_ffi_matches_pseudo_class(element: *const c_void, pseudo_class: u8) -> bool; - fn selector_ffi_matches_language(element: *const c_void, language: FfiStringView) -> bool; - fn selector_ffi_matches_direction(element: *const c_void, direction: FfiDirection) -> bool; - fn selector_ffi_matches_state(element: *const c_void, cxx_simple_selector: *const c_void) -> bool; - fn selector_ffi_matches_heading(element: *const c_void, levels: *const i64, level_count: usize) -> bool; - - fn selector_ffi_parent_element(element: *const c_void, shadow_host: *const c_void) -> *const c_void; - fn selector_ffi_parent_element_in_light_tree(element: *const c_void) -> *const c_void; - fn selector_ffi_previous_element_sibling(element: *const c_void) -> *const c_void; - fn selector_ffi_next_element_sibling(element: *const c_void) -> *const c_void; - fn selector_ffi_first_element_child(element: *const c_void) -> *const c_void; - fn selector_ffi_first_element_descendant(element: *const c_void) -> *const c_void; - fn selector_ffi_next_element_descendant(element: *const c_void, root: *const c_void) -> *const c_void; - fn selector_ffi_has_no_element_or_nonempty_text_children(element: *const c_void) -> bool; - fn selector_ffi_has_same_type(first: *const c_void, second: *const c_void) -> bool; - fn selector_ffi_is_document_root(element: *const c_void) -> bool; + fn selector_ffi_element_qualified_name(element: *const c_void) -> FfiElementQualifiedName; + fn selector_ffi_element_id(element: *const c_void) -> *const usize; + fn selector_ffi_element_id_value(element: *const c_void) -> FfiDomStringView; + fn selector_ffi_element_classes(element: *const c_void) -> FfiInternedStringList; + fn selector_ffi_element_id_and_class_names_are_case_insensitive(element: *const c_void) -> bool; + fn selector_ffi_element_namespace_is_null(element: *const c_void) -> bool; + fn selector_ffi_element_is_html_element_in_html_document(element: *const c_void) -> bool; + fn selector_ffi_element_is_document_root(element: *const c_void) -> bool; + fn selector_ffi_element_is_link(element: *const c_void) -> bool; + fn selector_ffi_element_is_fullscreen(element: *const c_void) -> bool; + fn selector_ffi_element_heading_level(element: *const c_void) -> i64; + fn selector_ffi_element_has_popover_attribute(element: *const c_void) -> bool; + fn selector_ffi_element_popover_is_showing(element: *const c_void) -> bool; + fn selector_ffi_element_direction(element: *const c_void) -> FfiDirection; + fn selector_ffi_element_has_custom_state(element: *const c_void, state: usize) -> bool; + fn selector_ffi_element_language(element: *const c_void) -> FfiDomStringView; + fn selector_ffi_element_is_focused(element: *const c_void) -> bool; + fn selector_ffi_element_should_indicate_focus(element: *const c_void) -> bool; + fn selector_ffi_element_has_focus_within(element: *const c_void) -> bool; + fn selector_ffi_element_is_active(element: *const c_void) -> bool; + fn selector_ffi_element_is_checked(element: *const c_void) -> bool; + fn selector_ffi_element_is_defined(element: *const c_void) -> bool; + fn selector_ffi_element_is_disabled(element: *const c_void) -> bool; + fn selector_ffi_element_is_enabled(element: *const c_void) -> bool; + fn selector_ffi_element_is_local_link(element: *const c_void) -> bool; + fn selector_ffi_element_is_placeholder_shown(element: *const c_void) -> bool; + fn selector_ffi_element_is_target(element: *const c_void) -> bool; + fn selector_ffi_element_is_unchecked(element: *const c_void) -> bool; + fn selector_ffi_element_is_media_element(element: *const c_void) -> bool; + fn selector_ffi_element_media_is_blocked(element: *const c_void) -> bool; + fn selector_ffi_element_media_is_muted(element: *const c_void) -> bool; + fn selector_ffi_element_media_is_paused(element: *const c_void) -> bool; + fn selector_ffi_element_media_is_seeking(element: *const c_void) -> bool; + fn selector_ffi_element_media_is_stalled(element: *const c_void) -> bool; + fn selector_ffi_element_is_default(element: *const c_void) -> bool; + fn selector_ffi_element_meter_value_state(element: *const c_void) -> FfiMeterValueState; + fn selector_ffi_element_meter_value_is_high(element: *const c_void) -> bool; + fn selector_ffi_element_meter_value_is_low(element: *const c_void) -> bool; + fn selector_ffi_element_is_hovered(element: *const c_void) -> bool; + fn selector_ffi_element_is_indeterminate(element: *const c_void) -> bool; + fn selector_ffi_element_validity_state(element: *const c_void) -> FfiValidityState; + fn selector_ffi_element_is_modal(element: *const c_void) -> bool; + fn selector_ffi_element_is_open(element: *const c_void) -> bool; + fn selector_ffi_element_required_state(element: *const c_void) -> FfiRequiredState; + fn selector_ffi_element_is_read_write(element: *const c_void) -> bool; + fn selector_ffi_element_user_validity_state(element: *const c_void) -> FfiValidityState; + fn selector_ffi_element_local_name(element: *const c_void) -> FfiDomStringView; + fn selector_ffi_element_class_name(element: *const c_void, index: usize) -> FfiDomStringView; + fn selector_ffi_element_attribute_count(element: *const c_void) -> usize; + fn selector_ffi_element_attribute(element: *const c_void, index: usize) -> FfiDomAttribute; + + fn selector_ffi_default_namespace(context: *mut c_void) -> FfiResolvedNamespace; + fn selector_ffi_resolve_namespace(context: *mut c_void, prefix: FfiStringView) -> FfiResolvedNamespace; + + fn selector_ffi_parent_element(element: *const c_void, shadow_host: *const c_void) -> FfiElement; + fn selector_ffi_parent_element_in_light_tree(element: *const c_void) -> FfiElement; + fn selector_ffi_previous_element_sibling(element: *const c_void) -> FfiElement; + fn selector_ffi_next_element_sibling(element: *const c_void) -> FfiElement; + fn selector_ffi_first_element_child(element: *const c_void) -> FfiElement; + fn selector_ffi_first_element_descendant(element: *const c_void) -> FfiElement; + fn selector_ffi_next_element_descendant(element: *const c_void, root: *const c_void) -> FfiElement; + fn selector_ffi_element_has_element_child(element: *const c_void) -> bool; + fn selector_ffi_element_has_nonempty_text_child(element: *const c_void) -> bool; fn selector_ffi_is_shadow_tree_slot(element: *const c_void) -> bool; fn selector_ffi_slotted_parent(context: *mut c_void, element: *const c_void) -> FfiElementAndShadowHost; @@ -1672,6 +2240,343 @@ fn ffi_string_view(string: &[u16]) -> FfiStringView { } } +fn ascii_lowercase(code_unit: u16) -> u16 { + if (u16::from(b'A')..=u16::from(b'Z')).contains(&code_unit) { + code_unit + u16::from(b'a' - b'A') + } else { + code_unit + } +} + +fn utf16_equals_ignoring_ascii_case(first: DomStringView<'_>, second: &[u16]) -> bool { + first.len() == second.len() + && second + .iter() + .enumerate() + .all(|(index, &second)| ascii_lowercase(first.code_unit_at(index)) == ascii_lowercase(second)) +} + +fn utf16_equals(first: DomStringView<'_>, second: &[u16]) -> bool { + first.len() == second.len() + && second + .iter() + .enumerate() + .all(|(index, &second)| first.code_unit_at(index) == second) +} + +struct SelectorSubtags<'a> { + value: &'a [u16], + position: usize, + finished: bool, +} + +impl<'a> SelectorSubtags<'a> { + fn new(value: &'a [u16]) -> Self { + Self { + value, + position: 0, + finished: false, + } + } +} + +impl Iterator for SelectorSubtags<'_> { + type Item = std::ops::Range; + + fn next(&mut self) -> Option { + if self.finished { + return None; + } + let start = self.position; + while self.position < self.value.len() && self.value[self.position] != u16::from(b'-') { + self.position += 1; + } + let end = self.position; + if self.position < self.value.len() { + self.position += 1; + } else { + self.finished = true; + } + Some(start..end) + } +} + +struct DomSubtags<'a> { + value: DomStringView<'a>, + position: usize, + finished: bool, +} + +impl<'a> DomSubtags<'a> { + fn new(value: DomStringView<'a>) -> Self { + Self { + value, + position: 0, + finished: false, + } + } +} + +impl Iterator for DomSubtags<'_> { + type Item = std::ops::Range; + + fn next(&mut self) -> Option { + if self.finished { + return None; + } + let start = self.position; + while self.position < self.value.len() && self.value.code_unit_at(self.position) != u16::from(b'-') { + self.position += 1; + } + let end = self.position; + if self.position < self.value.len() { + self.position += 1; + } else { + self.finished = true; + } + Some(start..end) + } +} + +fn language_subtags_match( + language_range: &[u16], + range_subtag: &std::ops::Range, + language_tag: DomStringView<'_>, + tag_subtag: &std::ops::Range, +) -> bool { + if range_subtag.len() == 1 && language_range[range_subtag.start] == u16::from(b'*') { + return true; + } + range_subtag.len() == tag_subtag.len() + && (0..range_subtag.len()).all(|offset| { + ascii_lowercase(language_range[range_subtag.start + offset]) + == ascii_lowercase(language_tag.code_unit_at(tag_subtag.start + offset)) + }) +} + +fn is_ascii_alphanumeric(code_unit: u16) -> bool { + (u16::from(b'0')..=u16::from(b'9')).contains(&code_unit) + || (u16::from(b'A')..=u16::from(b'Z')).contains(&code_unit) + || (u16::from(b'a')..=u16::from(b'z')).contains(&code_unit) +} + +// https://www.rfc-editor.org/rfc/rfc4647#section-3.3.2 +fn language_range_matches_tag(language_range: &[u16], language_tag: DomStringView<'_>) -> bool { + // 1. Split both the extended language range and the language tag being compared into a list + // of subtags by dividing on the hyphen (%x2D) character. + let mut range_subtags = SelectorSubtags::new(language_range); + let mut tag_subtags = DomSubtags::new(language_tag); + + // Two subtags match if either they are the same when compared case-insensitively or the + // language range's subtag is the wildcard '*'. + + // 2. Begin with the first subtag in each list. If the first subtag in the range does not match + // the first subtag in the tag, the overall match fails. Otherwise, move to the next subtag + // in both the range and the tag. + let first_range_subtag = range_subtags.next().unwrap(); + let first_tag_subtag = tag_subtags.next().unwrap(); + if !language_subtags_match(language_range, &first_range_subtag, language_tag, &first_tag_subtag) { + return false; + } + + let mut tag_subtag = tag_subtags.next(); + + // 3. While there are more subtags left in the language range's list: + for range_subtag in range_subtags { + // A. If the subtag currently being examined in the range is the wildcard ('*'), move to + // the next subtag in the range and continue with the loop. + if range_subtag.len() == 1 && language_range[range_subtag.start] == u16::from(b'*') { + continue; + } + + // B. Else, if there are no more subtags in the language tag's list, the match fails. + loop { + let Some(current_tag_subtag) = tag_subtag else { + return false; + }; + + // C. Else, if the current subtag in the range's list matches the current subtag in the + // language tag's list, move to the next subtag in both lists and continue with the + // loop. + if language_subtags_match(language_range, &range_subtag, language_tag, ¤t_tag_subtag) { + tag_subtag = tag_subtags.next(); + break; + } + + // D. Else, if the language tag's subtag is a "singleton" (a single letter or digit, + // which includes the private-use subtag 'x') the match fails. + if current_tag_subtag.len() == 1 + && is_ascii_alphanumeric(language_tag.code_unit_at(current_tag_subtag.start)) + { + return false; + } + + // E. Else, move to the next subtag in the language tag's list and continue with the + // loop. + tag_subtag = tag_subtags.next(); + } + } + + // 4. When the language range's list has no more subtags, the match succeeds. + true +} + +#[derive(Clone, Copy)] +enum StringCaseSensitivity { + Sensitive, + AsciiInsensitive, +} + +fn code_units_equal(first: u16, second: u16, case_sensitivity: StringCaseSensitivity) -> bool { + match case_sensitivity { + StringCaseSensitivity::Sensitive => first == second, + StringCaseSensitivity::AsciiInsensitive => ascii_lowercase(first) == ascii_lowercase(second), + } +} + +fn dom_string_matches_at( + value: DomStringView<'_>, + start: usize, + selector_value: &[u16], + case_sensitivity: StringCaseSensitivity, +) -> bool { + start + .checked_add(selector_value.len()) + .is_some_and(|end| end <= value.len()) + && selector_value + .iter() + .enumerate() + .all(|(index, &selector)| code_units_equal(value.code_unit_at(start + index), selector, case_sensitivity)) +} + +fn dom_string_equals( + value: DomStringView<'_>, + selector_value: &[u16], + case_sensitivity: StringCaseSensitivity, +) -> bool { + value.len() == selector_value.len() && dom_string_matches_at(value, 0, selector_value, case_sensitivity) +} + +fn matches_attribute_value( + match_type: AttributeMatchType, + selector_value: &[u16], + attribute_value: DomStringView<'_>, + case_sensitivity: StringCaseSensitivity, +) -> bool { + match match_type { + AttributeMatchType::HasAttribute => true, + AttributeMatchType::ExactValue => dom_string_equals(attribute_value, selector_value, case_sensitivity), + AttributeMatchType::ContainsWord => { + if selector_value.is_empty() + || selector_value.contains(&u16::from(b' ')) + || selector_value.len() > attribute_value.len() + { + return false; + } + (0..=attribute_value.len() - selector_value.len()).any(|start| { + (start == 0 || attribute_value.code_unit_at(start - 1) == u16::from(b' ')) + && (start + selector_value.len() == attribute_value.len() + || attribute_value.code_unit_at(start + selector_value.len()) == u16::from(b' ')) + && dom_string_matches_at(attribute_value, start, selector_value, case_sensitivity) + }) + } + AttributeMatchType::ContainsString => { + if selector_value.is_empty() || selector_value.len() > attribute_value.len() { + return false; + } + (0..=attribute_value.len() - selector_value.len()) + .any(|start| dom_string_matches_at(attribute_value, start, selector_value, case_sensitivity)) + } + AttributeMatchType::StartsWithSegment => { + if attribute_value.len() == 0 { + return selector_value.is_empty(); + } + if selector_value.is_empty() || selector_value.len() > attribute_value.len() { + return false; + } + dom_string_matches_at(attribute_value, 0, selector_value, case_sensitivity) + && (selector_value.len() == attribute_value.len() + || attribute_value.code_unit_at(selector_value.len()) == u16::from(b'-')) + } + AttributeMatchType::StartsWithString => { + !selector_value.is_empty() && dom_string_matches_at(attribute_value, 0, selector_value, case_sensitivity) + } + AttributeMatchType::EndsWithString => { + !selector_value.is_empty() + && selector_value.len() <= attribute_value.len() + && dom_string_matches_at( + attribute_value, + attribute_value.len() - selector_value.len(), + selector_value, + case_sensitivity, + ) + } + } +} + +fn utf16_equals_ascii(value: &[u16], ascii: &[u8]) -> bool { + value.len() == ascii.len() + && value + .iter() + .zip(ascii) + .all(|(&code_unit, &byte)| code_unit == u16::from(byte)) +} + +// https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors +// Attribute selectors on an HTML element in an HTML document must treat the values of attributes +// with the following names as ASCII case-insensitive: +fn is_ascii_case_insensitive_html_attribute(name: &[u16]) -> bool { + const NAMES: &[&[u8]] = &[ + b"accept", + b"accept-charset", + b"align", + b"alink", + b"axis", + b"bgcolor", + b"charset", + b"checked", + b"clear", + b"codetype", + b"color", + b"compact", + b"declare", + b"defer", + b"dir", + b"direction", + b"disabled", + b"enctype", + b"face", + b"frame", + b"hreflang", + b"http-equiv", + b"lang", + b"language", + b"link", + b"media", + b"method", + b"multiple", + b"nohref", + b"noresize", + b"noshade", + b"nowrap", + b"readonly", + b"rel", + b"rev", + b"rules", + b"scope", + b"scrolling", + b"selected", + b"shape", + b"target", + b"text", + b"type", + b"valign", + b"valuetype", + b"vlink", + ]; + NAMES.iter().any(|candidate| utf16_equals_ascii(name, candidate)) +} + struct FfiCallScope; struct FfiDom<'a> { @@ -1705,9 +2610,9 @@ impl<'a> FfiDom<'a> { }) } - unsafe fn element(&self, element: *const c_void) -> Option> { + unsafe fn element(&self, element: FfiElement) -> Option> { // SAFETY: The caller guarantees that a non-null pointer identifies a live DOM element. - unsafe { self.node(element, FfiNodeKind::Element) } + unsafe { self.node(element.pointer, FfiNodeKind::Element) } } unsafe fn scope(&self, scope: *const c_void) -> Option> { @@ -1725,21 +2630,47 @@ impl<'a> FfiDom<'a> { self.element(value.shadow_host) })) } + + fn default_namespace(&self) -> FfiResolvedNamespace { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The matching context remains live for this matching call. + unsafe { selector_ffi_default_namespace(self.context) } + } + + fn resolve_namespace(&self, prefix: &[u16]) -> FfiResolvedNamespace { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The matching context remains live, and the prefix view is borrowed for this + // accessor call only. + unsafe { selector_ffi_resolve_namespace(self.context, ffi_string_view(prefix)) } + } + + fn matches_resolved_namespace(&self, element: FfiNode<'a>, namespace: FfiResolvedNamespace) -> bool { + match namespace.namespace_type { + FfiResolvedNamespaceType::Missing => false, + FfiResolvedNamespaceType::Null => element.as_element().namespace_is_null(), + FfiResolvedNamespaceType::Named => { + element.as_element().qualified_name().namespace() == Some(namespace.namespace_) + } + } + } } impl<'a> SelectorDom for FfiDom<'a> { type Element = FfiNode<'a>; fn matches_universal_selector(&mut self, element: FfiNode<'a>, name: &QualifiedName) -> bool { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); - // SAFETY: `FfiDom` guarantees that the context, element, and retained simple selector - // remain valid for the duration of matching. - unsafe { - selector_ffi_matches_universal( - self.context, - element.as_element_pointer(), - name.cxx_simple_selector.as_ptr(), - ) + match name.namespace_type { + NamespaceType::Default => { + let namespace = self.default_namespace(); + namespace.namespace_type == FfiResolvedNamespaceType::Missing + || self.matches_resolved_namespace(element, namespace) + } + NamespaceType::None => element.as_element().namespace_is_null(), + NamespaceType::Any => true, + NamespaceType::Named => { + let namespace = self.resolve_namespace(&name.namespace); + self.matches_resolved_namespace(element, namespace) + } } } @@ -1749,104 +2680,214 @@ impl<'a> SelectorDom for FfiDom<'a> { name: &QualifiedName, mode: TagNameMatchingMode, ) -> bool { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); - // SAFETY: `FfiDom` guarantees that the context, element, and retained simple selector - // remain valid for the duration of matching. - unsafe { - selector_ffi_matches_tag_name( - self.context, - element.as_element_pointer(), - name.cxx_simple_selector.as_ptr(), - mode, - ) + // https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors + // The same selector when compared to other elements must be compared according to its + // original case. In both cases, to match, the values must be identical to each other (and + // therefore the comparison is case sensitive). + let ffi_element = element.as_element(); + let is_html_element_in_html_document = ffi_element.is_html_element_in_html_document(); + let name_matches = if is_html_element_in_html_document || mode == TagNameMatchingMode::Fast { + let interned_name = if is_html_element_in_html_document { + name.interned_lowercase_name + } else { + name.interned_name + }; + interned_name.is_some_and(|name| ffi_element.qualified_name().local_name() == Some(name)) + } else { + utf16_equals(element.local_name(), &name.name) + }; + if !name_matches { + return false; } + self.matches_universal_selector(element, name) } fn matches_id_selector(&mut self, element: FfiNode<'a>, id: &NameSelector) -> bool { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); - // SAFETY: `FfiDom` guarantees that the element and retained simple selector remain valid - // for the duration of matching. - unsafe { selector_ffi_matches_id(element.as_element_pointer(), id.cxx_simple_selector.as_ptr()) } + let ffi_element = element.as_element(); + if ffi_element.id_and_class_names_are_case_insensitive() { + return utf16_equals_ignoring_ascii_case(element.id_value(), &id.name); + } + id.interned_name.is_some_and(|id| ffi_element.id() == Some(id)) } fn matches_class_selector(&mut self, element: FfiNode<'a>, class_name: &NameSelector) -> bool { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); - // SAFETY: `FfiDom` guarantees that the element and retained simple selector remain valid - // for the duration of matching. - unsafe { selector_ffi_matches_class(element.as_element_pointer(), class_name.cxx_simple_selector.as_ptr()) } + let ffi_element = element.as_element(); + if ffi_element.id_and_class_names_are_case_insensitive() { + return (0..element.classes().len()) + .any(|index| utf16_equals_ignoring_ascii_case(element.class_name(index), &class_name.name)); + } + class_name + .interned_name + .is_some_and(|class_name| element.classes().contains(&class_name)) } fn matches_attribute_selector(&mut self, element: FfiNode<'a>, attribute: &AttributeSelector) -> bool { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); - // SAFETY: `FfiDom` guarantees that the context, element, and retained simple selector - // remain valid for the duration of matching. - unsafe { - selector_ffi_matches_attribute( - self.context, - element.as_element_pointer(), - attribute.qualified_name.cxx_simple_selector.as_ptr(), - ) - } - } + let qualified_name = &attribute.qualified_name; + let is_html_element_in_html_document = element.as_element().is_html_element_in_html_document(); + let use_lowercase_name = + is_html_element_in_html_document && qualified_name.namespace_type != NamespaceType::Named; + let name_to_match = if use_lowercase_name { + qualified_name.interned_lowercase_name + } else { + qualified_name.interned_name + }; + let Some(name_to_match) = name_to_match else { + return false; + }; - fn matches_pseudo_class_state(&mut self, element: FfiNode<'a>, pseudo_class: &PseudoClassSelector) -> bool { - match pseudo_class.pseudo_class { - PseudoClassType::Lang => pseudo_class.languages.iter().any(|language| { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); - // SAFETY: `FfiDom` guarantees that the element remains valid, and the string - // view is borrowed from `language` for this callback only. - unsafe { selector_ffi_matches_language(element.as_element_pointer(), ffi_string_view(language)) } - }), - PseudoClassType::Dir => match pseudo_class.direction { - Some(Direction::LeftToRight) => { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); - // SAFETY: `FfiDom` guarantees that the element remains valid for matching. - unsafe { selector_ffi_matches_direction(element.as_element_pointer(), FfiDirection::LeftToRight) } - } - Some(Direction::RightToLeft) => { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); - // SAFETY: `FfiDom` guarantees that the element remains valid for matching. - unsafe { selector_ffi_matches_direction(element.as_element_pointer(), FfiDirection::RightToLeft) } + let resolved_namespace = if qualified_name.namespace_type == NamespaceType::Named { + let namespace = self.resolve_namespace(&qualified_name.namespace); + if namespace.namespace_type == FfiResolvedNamespaceType::Missing { + return false; + } + Some(namespace) + } else { + None + }; + + let case_sensitivity = match attribute.case_type { + AttributeCaseType::Sensitive => StringCaseSensitivity::Sensitive, + AttributeCaseType::Insensitive => StringCaseSensitivity::AsciiInsensitive, + AttributeCaseType::Default => { + if is_html_element_in_html_document + && qualified_name.namespace_type == NamespaceType::Default + && is_ascii_case_insensitive_html_attribute(&qualified_name.name) + { + StringCaseSensitivity::AsciiInsensitive + } else { + StringCaseSensitivity::Sensitive } - _ => false, - }, - PseudoClassType::State => { - pseudo_class.identifier.is_some() && { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); - // SAFETY: `FfiDom` guarantees that the element and retained simple selector - // remain valid for the duration of matching. - unsafe { - selector_ffi_matches_state( - element.as_element_pointer(), - pseudo_class.cxx_simple_selector.as_ptr(), - ) + } + }; + + (0..element.attribute_count()).any(|index| { + let dom_attribute = element.attribute(index); + if dom_attribute.local_name() != name_to_match { + return false; + } + let namespace_matches = match qualified_name.namespace_type { + // https://www.w3.org/TR/selectors-4/#attrnmsp + // In keeping with the Namespaces in the XML recommendation, default namespaces do + // not apply to attributes, therefore attribute selectors without a namespace + // component apply only to attributes that have no namespace (equivalent to + // "|attr"). + NamespaceType::Default | NamespaceType::None => dom_attribute.namespace().is_none(), + NamespaceType::Any => true, + NamespaceType::Named => { + let resolved_namespace = resolved_namespace.unwrap(); + match resolved_namespace.namespace_type { + FfiResolvedNamespaceType::Missing => false, + FfiResolvedNamespaceType::Null => dom_attribute.namespace().is_none(), + FfiResolvedNamespaceType::Named => { + dom_attribute.namespace() == Some(resolved_namespace.namespace_) + } } } + }; + namespace_matches + && matches_attribute_value( + attribute.match_type, + &attribute.value, + dom_attribute.value(), + case_sensitivity, + ) + }) + } + + fn matches_pseudo_class_state(&mut self, element: FfiNode<'a>, pseudo_class: &PseudoClassSelector) -> bool { + match pseudo_class.pseudo_class { + PseudoClassType::AnyLink | PseudoClassType::Link => element.as_element().is_link(), + PseudoClassType::Active => element.as_element().is_active(), + PseudoClassType::Checked => element.as_element().is_checked(), + PseudoClassType::Defined => element.as_element().is_defined(), + PseudoClassType::Disabled => element.as_element().is_disabled(), + PseudoClassType::Enabled => element.as_element().is_enabled(), + PseudoClassType::LocalLink => element.as_element().is_local_link(), + PseudoClassType::PlaceholderShown => element.as_element().is_placeholder_shown(), + PseudoClassType::Target => element.as_element().is_target(), + PseudoClassType::Unchecked => element.as_element().is_unchecked(), + PseudoClassType::Buffering => element.as_element().media_is_blocked(), + PseudoClassType::Muted => element.as_element().media_is_muted(), + PseudoClassType::Paused => element.as_element().media_is_paused(), + PseudoClassType::Playing => { + let element = element.as_element(); + element.is_media_element() && !element.media_is_paused() } - PseudoClassType::Heading => { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); - // SAFETY: `FfiDom` guarantees that the element remains valid, and the levels array - // remains valid for this callback. - unsafe { - selector_ffi_matches_heading( - element.as_element_pointer(), - pseudo_class.levels.as_ptr(), - pseudo_class.levels.len(), - ) - } + PseudoClassType::Seeking => element.as_element().media_is_seeking(), + PseudoClassType::Stalled => element.as_element().media_is_stalled(), + PseudoClassType::Default => element.as_element().is_default(), + PseudoClassType::EvenLessGoodValue => { + element.as_element().meter_value_state() == FfiMeterValueState::EvenLessGood } - _ => { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); - // SAFETY: `FfiDom` guarantees that the element remains valid for matching. - unsafe { - selector_ffi_matches_pseudo_class(element.as_element_pointer(), pseudo_class.pseudo_class as u8) - } + PseudoClassType::HighValue => element.as_element().meter_value_is_high(), + PseudoClassType::Hover => element.as_element().is_hovered(), + PseudoClassType::Indeterminate => element.as_element().is_indeterminate(), + PseudoClassType::Invalid => element.as_element().validity_state() == FfiValidityState::Invalid, + PseudoClassType::LowValue => element.as_element().meter_value_is_low(), + PseudoClassType::Modal => element.as_element().is_modal(), + PseudoClassType::Open => element.as_element().is_open(), + PseudoClassType::OptimalValue => element.as_element().meter_value_state() == FfiMeterValueState::Optimal, + PseudoClassType::Optional => element.as_element().required_state() == FfiRequiredState::Optional, + PseudoClassType::ReadOnly => !element.as_element().is_read_write(), + PseudoClassType::ReadWrite => element.as_element().is_read_write(), + PseudoClassType::Required => element.as_element().required_state() == FfiRequiredState::Required, + PseudoClassType::SuboptimalValue => { + element.as_element().meter_value_state() == FfiMeterValueState::Suboptimal + } + PseudoClassType::UserInvalid => element.as_element().user_validity_state() == FfiValidityState::Invalid, + PseudoClassType::UserValid => element.as_element().user_validity_state() == FfiValidityState::Valid, + PseudoClassType::Valid => element.as_element().validity_state() == FfiValidityState::Valid, + PseudoClassType::Fullscreen => element.as_element().is_fullscreen(), + PseudoClassType::Focus => element.as_element().is_focused(), + PseudoClassType::FocusVisible => { + element.as_element().is_focused() && element.as_element().should_indicate_focus() + } + PseudoClassType::FocusWithin => element.as_element().has_focus_within(), + PseudoClassType::Autofill => { + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill + // FIXME: The :autofill and :-webkit-autofill pseudo-classes must match input + // elements which have been autofilled by user agent. These pseudo-classes + // must stop matching if the user edits the autofilled field. + // NB: We don't support autofilling inputs yet, so this is always false. + false } + PseudoClassType::Visited => { + // https://drafts.csswg.org/selectors/#visited-pseudo + // FIXME: For simplicity we currently have :visited never match. We may want to + // rethink this in the future. + false + } + PseudoClassType::VolumeLocked => { + // FIXME: Currently we don't allow the user to specify an override volume, so this + // is always false. Once we do, implement this! + false + } + PseudoClassType::Lang => element.language().is_some_and(|language_tag| { + pseudo_class + .languages + .iter() + .any(|language_range| language_range_matches_tag(language_range, language_tag)) + }), + PseudoClassType::Dir => pseudo_class + .direction + .is_some_and(|direction| direction == element.as_element().direction()), + PseudoClassType::State => pseudo_class + .identifier_identity + .is_some_and(|state| element.as_element().has_custom_state(state)), + PseudoClassType::Heading => element + .as_element() + .heading_level() + .is_some_and(|level| pseudo_class.levels.is_empty() || pseudo_class.levels.contains(&level)), + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open + PseudoClassType::PopoverOpen => { + element.as_element().has_popover_attribute() && element.as_element().popover_is_showing() + } + _ => unreachable!("structural pseudo-class reached state matching"), } } fn parent_element(&mut self, element: FfiNode<'a>, shadow_host: Option>) -> Option> { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that the input handles remain valid. The callback returns // either null or another live element borrowed for the same lifetime. unsafe { @@ -1858,42 +2899,42 @@ impl<'a> SelectorDom for FfiDom<'a> { } fn parent_element_in_light_tree(&mut self, element: FfiNode<'a>) -> Option> { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that the input remains valid. The callback returns either // null or another live element borrowed for the same lifetime. unsafe { self.element(selector_ffi_parent_element_in_light_tree(element.as_element_pointer())) } } fn previous_element_sibling(&mut self, element: FfiNode<'a>) -> Option> { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that the input remains valid. The callback returns either // null or another live element borrowed for the same lifetime. unsafe { self.element(selector_ffi_previous_element_sibling(element.as_element_pointer())) } } fn next_element_sibling(&mut self, element: FfiNode<'a>) -> Option> { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that the input remains valid. The callback returns either // null or another live element borrowed for the same lifetime. unsafe { self.element(selector_ffi_next_element_sibling(element.as_element_pointer())) } } fn first_element_child(&mut self, element: FfiNode<'a>) -> Option> { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that the input remains valid. The callback returns either // null or another live element borrowed for the same lifetime. unsafe { self.element(selector_ffi_first_element_child(element.as_element_pointer())) } } fn first_element_descendant(&mut self, element: FfiNode<'a>) -> Option> { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that the input remains valid. The callback returns either // null or another live element borrowed for the same lifetime. unsafe { self.element(selector_ffi_first_element_descendant(element.as_element_pointer())) } } fn next_element_descendant(&mut self, element: FfiNode<'a>, root: FfiNode<'a>) -> Option> { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that both element handles remain valid for this call. The // callback returns either null or another live element borrowed for the same lifetime. unsafe { @@ -1905,31 +2946,34 @@ impl<'a> SelectorDom for FfiDom<'a> { } fn has_no_element_or_nonempty_text_children(&mut self, element: FfiNode<'a>) -> bool { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that the element remains valid for matching. - unsafe { selector_ffi_has_no_element_or_nonempty_text_children(element.as_element_pointer()) } + if unsafe { selector_ffi_element_has_element_child(element.as_element_pointer()) } { + return false; + } + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: `FfiDom` guarantees that the element remains valid for matching. + !unsafe { selector_ffi_element_has_nonempty_text_child(element.as_element_pointer()) } } fn has_same_type(&mut self, first: FfiNode<'a>, second: FfiNode<'a>) -> bool { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); - // SAFETY: `FfiDom` guarantees that both elements remain valid for matching. - unsafe { selector_ffi_has_same_type(first.as_element_pointer(), second.as_element_pointer()) } + let first = first.as_element().qualified_name(); + let second = second.as_element().qualified_name(); + first.local_name() == second.local_name() && first.namespace() == second.namespace() } fn is_document_root(&mut self, element: FfiNode<'a>) -> bool { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); - // SAFETY: `FfiDom` guarantees that the element remains valid for matching. - unsafe { selector_ffi_is_document_root(element.as_element_pointer()) } + element.as_element().is_document_root() } fn is_shadow_tree_slot(&mut self, element: FfiNode<'a>) -> bool { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that the element remains valid for matching. unsafe { selector_ffi_is_shadow_tree_slot(element.as_element_pointer()) } } fn slotted_parent(&mut self, element: FfiNode<'a>) -> Option<(FfiNode<'a>, Option>)> { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that the context and element remain valid. The callback // returns null or live elements borrowed for the same lifetime. unsafe { self.element_and_shadow_host(selector_ffi_slotted_parent(self.context, element.as_element_pointer())) } @@ -1942,7 +2986,7 @@ impl<'a> SelectorDom for FfiDom<'a> { allow_same_shadow_root_scope: bool, shadow_host: Option>, ) -> Option<(FfiNode<'a>, Option>)> { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); let identifiers = identifiers .iter() .map(|identifier| ffi_string_view(identifier)) @@ -2100,6 +3144,12 @@ unsafe fn string_from_ffi(value: FfiStringView) -> SelectorString { unsafe { copy_ffi_slice(value.data, value.length) } } +unsafe fn interned_name_from_ffi(selector: &FfiSimpleSelector) -> Option { + // SAFETY: The caller guarantees that a non-null pointer identifies the one-word storage of a + // live C++ `Utf16FlyString` for the duration of selector compilation. + unsafe { selector.interned_name.as_ref().copied() } +} + unsafe fn qualified_name_from_ffi(selector: &FfiSimpleSelector) -> QualifiedName { QualifiedName { namespace_type: selector.namespace_type, @@ -2109,7 +3159,11 @@ unsafe fn qualified_name_from_ffi(selector: &FfiSimpleSelector) -> QualifiedName name: unsafe { string_from_ffi(selector.name) }, // SAFETY: The caller guarantees that every string view in `selector` is valid. lowercase_name: unsafe { string_from_ffi(selector.lowercase_name) }, - cxx_simple_selector: RetainedCxxPointer::new(selector.cxx_simple_selector), + // SAFETY: The caller guarantees that all retained C++ selector data is valid. + interned_name: unsafe { interned_name_from_ffi(selector) }, + // SAFETY: The caller guarantees that a non-null pointer identifies retained C++ selector + // data for the duration of selector compilation. + interned_lowercase_name: unsafe { selector.interned_lowercase_name.as_ref().copied() }, } } @@ -2132,12 +3186,14 @@ unsafe fn simple_selector_from_ffi(selector: &FfiSimpleSelector) -> SimpleSelect FfiSimpleSelectorType::Id => SimpleSelector::Id(NameSelector { // SAFETY: The caller guarantees that every string view in `selector` is valid. name: unsafe { string_from_ffi(selector.name) }, - cxx_simple_selector: RetainedCxxPointer::new(selector.cxx_simple_selector), + // SAFETY: The caller guarantees that all retained C++ selector data is valid. + interned_name: unsafe { interned_name_from_ffi(selector) }, }), FfiSimpleSelectorType::Class => SimpleSelector::Class(NameSelector { // SAFETY: The caller guarantees that every string view in `selector` is valid. name: unsafe { string_from_ffi(selector.name) }, - cxx_simple_selector: RetainedCxxPointer::new(selector.cxx_simple_selector), + // SAFETY: The caller guarantees that all retained C++ selector data is valid. + interned_name: unsafe { interned_name_from_ffi(selector) }, }), FfiSimpleSelectorType::Attribute => SimpleSelector::Attribute(AttributeSelector { match_type: selector.attribute_match_type, @@ -2176,6 +3232,9 @@ unsafe fn simple_selector_from_ffi(selector: &FfiSimpleSelector) -> SimpleSelect // SAFETY: The caller guarantees that every string view in `selector` is valid. unsafe { string_from_ffi(selector.identifier) } }); + // SAFETY: The caller guarantees that a non-null pointer identifies a live C++ + // `Utf16FlyString` for the duration of selector compilation. + let identifier_identity = unsafe { interned_name_from_ffi(selector) }; // SAFETY: The caller guarantees that the levels array is valid. let levels = unsafe { copy_ffi_slice(selector.levels, selector.level_count) }; @@ -2189,8 +3248,8 @@ unsafe fn simple_selector_from_ffi(selector: &FfiSimpleSelector) -> SimpleSelect languages, direction, identifier, + identifier_identity, levels, - cxx_simple_selector: RetainedCxxPointer::new(selector.cxx_simple_selector), }) } FfiSimpleSelectorType::PseudoElement => { @@ -2255,12 +3314,10 @@ unsafe fn compiled_selector_from_ffi(selector: &FfiSelector) -> Rc( - element: *const c_void, - shadow_host: *const c_void, - context: *mut c_void, + element: FfiElement, + shadow_host: FfiElement, scope: *const c_void, - collects_selector_involvement_metadata: bool, - inside_has_argument: bool, + configuration: FfiDomConfiguration, callback: impl for<'a> FnOnce(FfiNode<'a>, Option>, Option>, &mut FfiDom<'a>) -> R, ) -> R { let mut call_scope = FfiCallScope; @@ -2268,30 +3325,36 @@ unsafe fn with_ffi_dom( // callback. Borrowing `call_scope` prevents the DOM wrapper and its nodes from escaping it. let mut dom = unsafe { FfiDom::new( - context, + configuration.context, &mut call_scope, - collects_selector_involvement_metadata, - inside_has_argument, + configuration.collects_selector_involvement_metadata, + configuration.inside_has_argument, ) }; - // SAFETY: The caller guarantees that `element` points to a DOM element. + // SAFETY: The caller guarantees that `element` wraps a live DOM element. let element = unsafe { dom.element(element) }.unwrap(); - // SAFETY: The caller guarantees that a non-null `shadow_host` points to a DOM element. + // SAFETY: The caller guarantees that a non-null `shadow_host` wraps a live DOM element. let shadow_host = unsafe { dom.element(shadow_host) }; // SAFETY: The caller guarantees that a non-null `scope` points to a DOM parent node. let scope = unsafe { dom.scope(scope) }; callback(element, shadow_host, scope, &mut dom) } +#[derive(Clone, Copy)] +struct FfiDomConfiguration { + context: *mut c_void, + collects_selector_involvement_metadata: bool, + inside_has_argument: bool, +} + /// # Safety /// /// `selector`, every transitively referenced array, string, and `RustSelector` handle must be /// properly aligned and valid for reads for the duration of this call. Every enum field must /// contain a valid discriminant. /// -/// `FfiSelector::cxx_selector` and every `FfiSimpleSelector::cxx_simple_selector` must remain valid -/// until the returned handle is passed to `rust_selector_destroy`. The returned handle must be -/// destroyed exactly once. +/// `FfiSelector::cxx_selector` must remain valid until the returned handle is passed to +/// `rust_selector_destroy`. The returned handle must be destroyed exactly once. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_selector_create(selector: *const FfiSelector) -> *mut RustSelector { abort_on_panic(|| { @@ -2330,15 +3393,15 @@ pub unsafe extern "C" fn rust_selector_target_pseudo_element(selector: *const Ru /// # Safety /// The `selector` handle must have been returned by `rust_selector_create`. `element` and a -/// non-null `shadow_host` must point to C++ DOM elements, a non-null `scope` must point to a C++ +/// non-null `shadow_host` must wrap live C++ DOM elements, a non-null `scope` must point to a C++ /// DOM parent node, and `context` must point to a C++ Rust matching context. All referenced objects /// must remain valid for this call. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_selector_matches( selector: *const RustSelector, - element: *const c_void, + element: FfiElement, pseudo_element: u8, - shadow_host: *const c_void, + shadow_host: FfiElement, context: *mut c_void, scope: *const c_void, collects_selector_involvement_metadata: bool, @@ -2347,7 +3410,7 @@ pub unsafe extern "C" fn rust_selector_matches( crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMatchEntry); abort_on_panic(|| { assert!(!selector.is_null()); - assert!(!element.is_null()); + assert!(!element.pointer.is_null()); assert!(!context.is_null()); // SAFETY: The caller guarantees that the selector handle remains valid for this call. let selector = unsafe { &(*selector).selector }; @@ -2357,10 +3420,12 @@ pub unsafe extern "C" fn rust_selector_matches( with_ffi_dom( element, shadow_host, - context, scope, - collects_selector_involvement_metadata, - inside_has_argument, + FfiDomConfiguration { + context, + collects_selector_involvement_metadata, + inside_has_argument, + }, |element, shadow_host, scope, dom| { let target = MatchTarget { element, @@ -2378,15 +3443,15 @@ pub unsafe extern "C" fn rust_selector_matches( /// # Safety /// The `selector` handle must have been returned by `rust_selector_create`. `element` and a -/// non-null `shadow_host` must point to C++ DOM elements, a non-null `scope` must point to a C++ +/// non-null `shadow_host` must wrap live C++ DOM elements, a non-null `scope` must point to a C++ /// DOM parent node, and `context` must point to a C++ Rust matching context. All referenced objects /// must remain valid for this call. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_selector_matches_originating_element( selector: *const RustSelector, pseudo_element: u8, - element: *const c_void, - shadow_host: *const c_void, + element: FfiElement, + shadow_host: FfiElement, context: *mut c_void, scope: *const c_void, collects_selector_involvement_metadata: bool, @@ -2395,7 +3460,7 @@ pub unsafe extern "C" fn rust_selector_matches_originating_element( crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMatchEntry); abort_on_panic(|| { assert!(!selector.is_null()); - assert!(!element.is_null()); + assert!(!element.pointer.is_null()); assert!(!context.is_null()); // SAFETY: The caller guarantees that the selector handle remains valid for this call. let selector = unsafe { &(*selector).selector }; @@ -2405,10 +3470,12 @@ pub unsafe extern "C" fn rust_selector_matches_originating_element( with_ffi_dom( element, shadow_host, - context, scope, - collects_selector_involvement_metadata, - inside_has_argument, + FfiDomConfiguration { + context, + collects_selector_involvement_metadata, + inside_has_argument, + }, |element, shadow_host, scope, dom| { matches_originating_element_for_pseudo_element( selector, @@ -2658,7 +3725,7 @@ mod tests { fn class(name: &str) -> SimpleSelector { SimpleSelector::Class(NameSelector { name: name.encode_utf16().collect(), - cxx_simple_selector: RetainedCxxPointer::default(), + interned_name: None, }) } @@ -2712,7 +3779,7 @@ mod tests { combinator: *combinator, simple_selectors: vec![SimpleSelector::Id(NameSelector { name: Box::from([b'x' as u16]), - cxx_simple_selector: RetainedCxxPointer::default(), + interned_name: None, })] .into_boxed_slice(), }) @@ -2872,8 +3939,8 @@ mod tests { languages: Box::new([]), direction: None, identifier: None, + identifier_identity: None, levels: Box::new([]), - cxx_simple_selector: RetainedCxxPointer::default(), }); let selector = selector(vec![compound(Combinator::None, vec![class("anchor"), has])]); let mut dom = test_tree(); @@ -2914,8 +3981,8 @@ mod tests { languages: Box::new([]), direction: None, identifier: None, + identifier_identity: None, levels: Box::new([]), - cxx_simple_selector: RetainedCxxPointer::default(), }); let selector = selector(vec![compound(Combinator::None, vec![nth_child])]); let mut dom = test_tree(); diff --git a/Libraries/LibWeb/CSS/SelectorMatching.cpp b/Libraries/LibWeb/CSS/SelectorMatching.cpp index 3ffb444084ad1..a61640d202660 100644 --- a/Libraries/LibWeb/CSS/SelectorMatching.cpp +++ b/Libraries/LibWeb/CSS/SelectorMatching.cpp @@ -5,6 +5,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -37,17 +38,6 @@ namespace Web::SelectorMatching { -static bool fly_string_equals_utf16(Utf16FlyString const& fly_string, Utf16View utf16_string) -{ - return utf16_string == fly_string.view(); -} - -template -static bool fly_string_is_one_of_utf16(Utf16View utf16_string, Names const&... names) -{ - return (fly_string_equals_utf16(names, utf16_string) || ...); -} - static u32 salted_tag_name_hash(Utf16FlyString const& tag_name) { return CSS::ancestor_filter_hash_for_tag_name(tag_name.ascii_case_insensitive_hash()); @@ -245,65 +235,6 @@ static bool should_reject_with_has_fast_reject_filter(CSS::Selector const& selec return false; } -static bool language_range_matches_tag(Utf16View language_range, Utf16View language_tag) -{ - // 1. Split both the extended language range and the language tag being compared into a list of subtags by - // dividing on the hyphen (%x2D) character. - auto range_subtags = language_range.split_view('-', SplitBehavior::KeepEmpty); - auto tag_subtags = language_tag.split_view('-', SplitBehavior::KeepEmpty); - - // Two subtags match if either they are the same when compared case-insensitively or the language range's subtag - // is the wildcard '*'. - auto subtags_match = [](Utf16View language_range_subtag, Utf16View language_subtag) { - return language_range_subtag == u"*"sv - || language_range_subtag.equals_ignoring_ascii_case(language_subtag); - }; - - // 2. Begin with the first subtag in each list. If the first subtag in the range does not match the first - // subtag in the tag, the overall match fails. Otherwise, move to the next subtag in both the range and the - // tag. - auto tag_subtag = tag_subtags.begin(); - auto range_subtag = range_subtags.begin(); - if (!subtags_match(*range_subtag, *tag_subtag)) - return false; - ++tag_subtag; - ++range_subtag; - - // 3. While there are more subtags left in the language range's list: - while (!range_subtag.is_end()) { - // A. If the subtag currently being examined in the range is the wildcard ('*'), move to the next subtag in - // the range and continue with the loop. - if (*range_subtag == u"*"sv) { - ++range_subtag; - continue; - } - - // B. Else, if there are no more subtags in the language tag's list, the match fails. - if (tag_subtag.is_end()) - return false; - - // C. Else, if the current subtag in the range's list matches the current subtag in the language tag's - // list, move to the next subtag in both lists and continue with the loop. - if (subtags_match(*range_subtag, *tag_subtag)) { - ++range_subtag; - ++tag_subtag; - continue; - } - - // D. Else, if the language tag's subtag is a "singleton" (a single letter or digit, which includes the - // private-use subtag 'x') the match fails. - if (tag_subtag->length_in_code_units() == 1 && is_ascii_alphanumeric(tag_subtag->code_unit_at(0))) { - return false; - } - - // E. Else, move to the next subtag in the language tag's list and continue with the loop. - ++tag_subtag; - } - - // 4. When the language range's list has no more subtags, the match succeeds. - return true; -} - static bool matches_hover_pseudo_class(DOM::Element const& element) { auto* hovered_node = element.document().hovered_node(); @@ -354,7 +285,7 @@ static bool matches_read_write_pseudo_class(DOM::Element const& element) } // https://drafts.csswg.org/selectors-4/#open-state -static bool matches_open_state_pseudo_class(DOM::Element const& element, bool open) +static bool matches_open_state_pseudo_class(DOM::Element const& element) { // The :open pseudo-class represents an element that has both “open” and “closed” states, // and which is currently in the “open” state. @@ -364,293 +295,25 @@ static bool matches_open_state_pseudo_class(DOM::Element const& element, bool op // - details elements that have an open attribute // - dialog elements that have an open attribute if (is(element) || is(element)) - return open == element.has_attribute(HTML::AttributeNames::open); + return element.has_attribute(HTML::AttributeNames::open); // - select elements that are a drop-down box and whose drop-down boxes are open if (auto const* select = as_if(element)) - return open == select->is_open(); + return select->is_open(); // - input elements that support a picker and whose pickers are open if (auto const* input = as_if(element)) - return open == (input->supports_a_picker() && input->is_open()); + return input->supports_a_picker() && input->is_open(); return false; } -static bool matches_optimal_value_pseudo_class(DOM::Element const& element, HTML::HTMLMeterElement::ValueState desired_state) +static CSS::SelectorFFI::Element element_to_ffi(DOM::Element const* element) { - if (auto* meter = as_if(element)) - return meter->value_state() == desired_state; - return false; -} - -static bool matches_pseudo_class_state(CSS::PseudoClass pseudo_class, DOM::Element const& element) -{ - switch (pseudo_class) { - case CSS::PseudoClass::Active: - return element.is_being_activated(); - case CSS::PseudoClass::AnyLink: - case CSS::PseudoClass::Link: - // NOTE: AnyLink should match whether the link is visited or not, so if we ever start matching - // :visited, we'll need to handle these differently. - return element.matches_link_pseudo_class(); - case CSS::PseudoClass::Autofill: - // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill - // FIXME: The :autofill and :-webkit-autofill pseudo-classes must match input elements which have been autofilled by - // user agent. These pseudo-classes must stop matching if the user edits the autofilled field. - // NB: We don't support autofilling inputs yet, so this is always false. - return false; - case CSS::PseudoClass::Buffering: - if (auto const* media_element = as_if(element)) - return media_element->blocked(); - return false; - case CSS::PseudoClass::Checked: - return element.matches_checked_pseudo_class(); - case CSS::PseudoClass::Default: { - // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-default - if (auto const* form_associated_element = as_if(element)) { - if (form_associated_element->is_submit_button() && form_associated_element->form() && form_associated_element->form()->default_button() == form_associated_element) - return true; - if (auto const* input_element = as_if(form_associated_element)) { - if (input_element->checked_applies() && input_element->has_attribute(HTML::AttributeNames::checked)) - return true; - } - if (auto const* option_element = as_if(form_associated_element)) { - if (option_element->has_attribute(HTML::AttributeNames::selected)) - return true; - } - } - return false; - } - case CSS::PseudoClass::Defined: - return element.is_defined(); - case CSS::PseudoClass::Disabled: - return element.matches_disabled_pseudo_class(); - case CSS::PseudoClass::Enabled: - return element.matches_enabled_pseudo_class(); - case CSS::PseudoClass::EvenLessGoodValue: - return matches_optimal_value_pseudo_class(element, HTML::HTMLMeterElement::ValueState::EvenLessGood); - case CSS::PseudoClass::Focus: - return element.is_focused(); - case CSS::PseudoClass::FocusVisible: - return element.is_focused() && element.should_indicate_focus(); - case CSS::PseudoClass::FocusWithin: - return element.matches_focus_within_pseudo_class(); - case CSS::PseudoClass::Fullscreen: - return element.is_fullscreen_element(); - case CSS::PseudoClass::HighValue: - if (auto const* meter = as_if(element)) - return meter->value() > meter->high(); - return false; - case CSS::PseudoClass::Hover: - return matches_hover_pseudo_class(element); - case CSS::PseudoClass::Indeterminate: - return matches_indeterminate_pseudo_class(element); - case CSS::PseudoClass::Invalid: { - // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-invalid - if (auto form_associated_element = as_if(element)) { - if (form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) - return true; - } - - if (auto form_element = as_if(element)) { - bool has_invalid_elements = false; - element.for_each_in_subtree([&](auto& node) { - if (auto form_associated_element = as_if(&node)) { - if (form_associated_element->form() == form_element && form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) { - has_invalid_elements = true; - return TraversalDecision::Break; - } - } - return TraversalDecision::Continue; - }); - if (has_invalid_elements) - return true; - } - - if (is(element)) { - bool has_invalid_children = false; - element.for_each_in_subtree([&](auto& node) { - if (auto form_associated_element = as_if(&node)) { - if (form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) { - has_invalid_children = true; - return TraversalDecision::Break; - } - } - return TraversalDecision::Continue; - }); - if (has_invalid_children) - return true; - } - return false; - } - case CSS::PseudoClass::LocalLink: - return element.matches_local_link_pseudo_class(); - case CSS::PseudoClass::LowValue: - if (auto const* meter = as_if(element)) - return meter->value() < meter->low(); - return false; - case CSS::PseudoClass::Modal: - // https://drafts.csswg.org/selectors/#modal-state - if (auto const* dialog_element = as_if(element)) - return dialog_element->is_modal(); - // FIXME: fullscreen elements are also modal. - return false; - case CSS::PseudoClass::Muted: - if (auto const* media_element = as_if(element)) - return media_element->muted(); - return false; - case CSS::PseudoClass::Open: - return matches_open_state_pseudo_class(element, true); - case CSS::PseudoClass::OptimalValue: - return matches_optimal_value_pseudo_class(element, HTML::HTMLMeterElement::ValueState::Optimal); - case CSS::PseudoClass::Optional: - // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-optional - if (auto const* input_element = as_if(element)) { - if (input_element->required_applies() && !input_element->has_attribute(HTML::AttributeNames::required)) - return true; - // AD-HOC: Chromium and Webkit also match for hidden inputs (and WPT expects this) - // See: https://github.com/whatwg/html/issues/11273 - return input_element->type_state() == HTML::HTMLInputElement::TypeAttributeState::Hidden; - } - if (auto const* select_element = as_if(element)) - return !select_element->has_attribute(HTML::AttributeNames::required); - if (auto const* textarea_element = as_if(element)) - return !textarea_element->has_attribute(HTML::AttributeNames::required); - return false; - case CSS::PseudoClass::Paused: - if (auto const* media_element = as_if(element)) - return media_element->paused(); - return false; - case CSS::PseudoClass::PlaceholderShown: - return element.matches_placeholder_shown_pseudo_class(); - case CSS::PseudoClass::Playing: - if (auto const* media_element = as_if(element)) - return !media_element->paused(); - return false; - case CSS::PseudoClass::PopoverOpen: - // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open - if (auto const* html_element = as_if(element); - html_element && html_element->has_attribute(HTML::AttributeNames::popover)) { - return html_element->popover_visibility_state() == HTML::HTMLElement::PopoverVisibilityState::Showing; - } - return false; - case CSS::PseudoClass::ReadOnly: - return !matches_read_write_pseudo_class(element); - case CSS::PseudoClass::ReadWrite: - return matches_read_write_pseudo_class(element); - case CSS::PseudoClass::Required: - // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-required - if (auto const* input_element = as_if(element)) - return input_element->required_applies() && input_element->has_attribute(HTML::AttributeNames::required); - if (auto const* select_element = as_if(element)) - return select_element->has_attribute(HTML::AttributeNames::required); - if (auto const* textarea_element = as_if(element)) - return textarea_element->has_attribute(HTML::AttributeNames::required); - return false; - case CSS::PseudoClass::Seeking: - if (auto const* media_element = as_if(element)) - return media_element->seeking(); - return false; - case CSS::PseudoClass::Stalled: - if (auto const* media_element = as_if(element)) - return media_element->stalled(); - return false; - case CSS::PseudoClass::SuboptimalValue: - return matches_optimal_value_pseudo_class(element, HTML::HTMLMeterElement::ValueState::Suboptimal); - case CSS::PseudoClass::Target: - return element.is_target(); - case CSS::PseudoClass::Unchecked: - return element.matches_unchecked_pseudo_class(); - case CSS::PseudoClass::UserInvalid: - case CSS::PseudoClass::UserValid: { - // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-user-valid - // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-user-invalid - bool user_validity = false; - if (auto const* input_element = as_if(element)) - user_validity = input_element->user_validity(); - else if (auto const* select_element = as_if(element)) - user_validity = select_element->user_validity(); - else if (auto const* text_area_element = as_if(element)) - user_validity = text_area_element->user_validity(); - if (!user_validity) - return false; - - auto const& form_associated_element = as(element); - if (!form_associated_element.is_candidate_for_constraint_validation()) - return false; - return pseudo_class == CSS::PseudoClass::UserValid - ? form_associated_element.satisfies_its_constraints() - : !form_associated_element.satisfies_its_constraints(); - } - case CSS::PseudoClass::Valid: { - // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-valid - if (auto form_associated_element = as_if(element)) { - if (form_associated_element->is_candidate_for_constraint_validation() && form_associated_element->satisfies_its_constraints()) - return true; - } - - if (auto form_element = as_if(element)) { - bool has_invalid_elements = false; - element.for_each_in_subtree([&](auto& node) { - if (auto form_associated_element = as_if(&node)) { - if (form_associated_element->form() == form_element && form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) { - has_invalid_elements = true; - return TraversalDecision::Break; - } - } - return TraversalDecision::Continue; - }); - if (!has_invalid_elements) - return true; - } + if (!element) + return {}; - if (is(element)) { - bool has_invalid_children = false; - element.for_each_in_subtree([&](auto& node) { - if (auto form_associated_element = as_if(&node)) { - if (form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) { - has_invalid_children = true; - return TraversalDecision::Break; - } - } - return TraversalDecision::Continue; - }); - if (!has_invalid_children) - return true; - } - return false; - } - case CSS::PseudoClass::Visited: - return element.matches_visited_pseudo_class(); - case CSS::PseudoClass::VolumeLocked: - // FIXME: Currently we don't allow the user to specify an override volume, so this is always false. - // Once we do, implement this! - return false; - case CSS::PseudoClass::__Count: - case CSS::PseudoClass::Dir: - case CSS::PseudoClass::Empty: - case CSS::PseudoClass::FirstChild: - case CSS::PseudoClass::FirstOfType: - case CSS::PseudoClass::Has: - case CSS::PseudoClass::Heading: - case CSS::PseudoClass::Host: - case CSS::PseudoClass::Is: - case CSS::PseudoClass::Lang: - case CSS::PseudoClass::LastChild: - case CSS::PseudoClass::LastOfType: - case CSS::PseudoClass::Not: - case CSS::PseudoClass::NthChild: - case CSS::PseudoClass::NthLastChild: - case CSS::PseudoClass::NthLastOfType: - case CSS::PseudoClass::NthOfType: - case CSS::PseudoClass::OnlyChild: - case CSS::PseudoClass::OnlyOfType: - case CSS::PseudoClass::Root: - case CSS::PseudoClass::Scope: - case CSS::PseudoClass::State: - case CSS::PseudoClass::Where: - VERIFY_NOT_REACHED(); - } - VERIFY_NOT_REACHED(); + return { + .pointer = element, + }; } bool matches(CSS::Selector const& selector, DOM::AbstractElement const& target, GC::Ptr shadow_host, @@ -658,9 +321,9 @@ bool matches(CSS::Selector const& selector, DOM::AbstractElement const& target, { return CSS::SelectorFFI::rust_selector_matches( &selector.rust_selector(), - &target.element(), + element_to_ffi(&target.element()), CSS::pseudo_element_to_ffi(target.pseudo_element()), - shadow_host.ptr(), + element_to_ffi(shadow_host.ptr()), &context, scope.ptr(), context.collect_per_element_selector_involvement_metadata, @@ -674,8 +337,8 @@ bool matches_originating_element_for_pseudo_element(CSS::Selector const& selecto return CSS::SelectorFFI::rust_selector_matches_originating_element( &selector.rust_selector(), CSS::pseudo_element_to_ffi(pseudo_element), - &target.element(), - shadow_host.ptr(), + element_to_ffi(&target.element()), + element_to_ffi(shadow_host.ptr()), &context, scope.ptr(), context.collect_per_element_selector_involvement_metadata, @@ -703,79 +366,72 @@ static Utf16View ffi_string_view(CSS::SelectorFFI::StringView string) return { reinterpret_cast(string.data), string.length }; } -static CSS::Selector::SimpleSelector const& ffi_simple_selector(void const* simple_selector) -{ - VERIFY(simple_selector); - return *static_cast(simple_selector); -} - static bool is_in_null_namespace(DOM::Element const& element) { return !element.namespace_uri().has_value() || element.namespace_uri()->is_empty(); } -static bool matches_namespace(CSS::Selector::SimpleSelector::QualifiedName const& qualified_name, DOM::Element const& element, GC::Ptr style_sheet_for_rule) -{ - switch (qualified_name.namespace_type) { - case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Default: - // "if no default namespace has been declared for selectors, this is equivalent to *|E." - if (!style_sheet_for_rule || !style_sheet_for_rule->default_namespace_rule()) - return true; - // "Otherwise it is equivalent to ns|E where ns is the default namespace." - if (style_sheet_for_rule->default_namespace_rule()->namespace_uri().is_empty()) - return is_in_null_namespace(element); - - return element.namespace_uri().has_value() - && style_sheet_for_rule->default_namespace_rule()->namespace_uri() == element.namespace_uri()->view(); - case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::None: - // "elements with name E without a namespace" - return is_in_null_namespace(element); - case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Any: - // "elements with name E in any namespace, including those without a namespace" - return true; - case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Named: { - // "elements with name E in namespace ns" - // Unrecognized namespace prefixes are invalid, so don't match. - // (We can't detect this at parse time, since a namespace rule may be inserted later.) - // So, if we don't have a context to look up namespaces from, we fail to match. - if (!style_sheet_for_rule) - return false; - auto selector_namespace = style_sheet_for_rule->namespace_uri(qualified_name.namespace_); - // https://www.w3.org/TR/css-namespaces-3/#terminology - // In CSS Namespaces a namespace name consisting of the empty string is taken to represent the null namespace - // or lack of a namespace. - if (selector_namespace.has_value() && selector_namespace->is_empty()) - return is_in_null_namespace(element); - return selector_namespace.has_value() - && element.namespace_uri().has_value() - && *selector_namespace == element.namespace_uri()->view(); - } - } - VERIFY_NOT_REACHED(); -} - -using CSS::SelectorFFI::AttributeCaseType; -using CSS::SelectorFFI::AttributeMatchType; using CSS::SelectorFFI::Combinator; using CSS::SelectorFFI::Direction; using CSS::SelectorFFI::HasCacheResult; using CSS::SelectorFFI::NamespaceType; using CSS::SelectorFFI::StringView; -using CSS::SelectorFFI::TagNameMatchingMode; #define DECLARE_SELECTOR_FFI_CALLBACK(function) \ extern "C" decltype(CSS::SelectorFFI::function) function -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_universal); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_tag_name); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_id); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_class); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_attribute); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_pseudo_class); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_language); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_direction); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_state); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_heading); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_qualified_name); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_id); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_id_value); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_classes); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_id_and_class_names_are_case_insensitive); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_namespace_is_null); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_html_element_in_html_document); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_document_root); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_link); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_fullscreen); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_heading_level); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_has_popover_attribute); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_popover_is_showing); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_direction); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_has_custom_state); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_language); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_focused); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_should_indicate_focus); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_has_focus_within); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_active); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_checked); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_defined); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_disabled); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_enabled); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_local_link); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_placeholder_shown); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_target); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_unchecked); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_media_element); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_media_is_blocked); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_media_is_muted); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_media_is_paused); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_media_is_seeking); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_media_is_stalled); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_default); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_meter_value_state); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_meter_value_is_high); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_meter_value_is_low); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_hovered); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_indeterminate); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_validity_state); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_modal); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_open); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_required_state); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_read_write); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_user_validity_state); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_local_name); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_class_name); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_attribute_count); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_attribute); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_default_namespace); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_resolve_namespace); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_parent_element); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_parent_element_in_light_tree); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_previous_element_sibling); @@ -783,9 +439,8 @@ DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_next_element_sibling); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_first_element_child); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_first_element_descendant); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_next_element_descendant); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_has_no_element_or_nonempty_text_children); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_has_same_type); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_is_document_root); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_has_element_child); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_has_nonempty_text_child); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_is_shadow_tree_slot); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_slotted_parent); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_part_parent); @@ -802,326 +457,543 @@ DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_should_reject_has_argument); #undef DECLARE_SELECTOR_FFI_CALLBACK -extern "C" bool selector_ffi_matches_universal(void* context, void const* element, void const* cxx_simple_selector) +// `Utf16FlyString` is its interned one-word representation. The accessors below expose pointers +// to live words only for the duration of the synchronous selector-matching call. +static_assert(sizeof(Utf16FlyString) == sizeof(uintptr_t)); +static_assert(alignof(Utf16FlyString) == alignof(uintptr_t)); + +static uintptr_t interned_string_identity(Utf16FlyString const& string) { - auto& match_context = rust_match_context(context); - auto const& qualified_name = ffi_simple_selector(cxx_simple_selector).qualified_name(); - return matches_namespace(qualified_name, ffi_element(element), match_context.style_sheet_for_rule); + uintptr_t identity; + __builtin_memcpy(&identity, &string, sizeof(identity)); + return identity; } -extern "C" bool selector_ffi_matches_tag_name(void* context, void const* element, void const* cxx_simple_selector, TagNameMatchingMode matching_mode) +static CSS::SelectorFFI::DomStringView dom_string_view(Utf16View string) +{ + if (string.has_ascii_storage()) { + auto storage = string.ascii_span(); + return { + .data = storage.data(), + .length = storage.size(), + .is_ascii = true, + }; + } + auto storage = string.utf16_span(); + return { + .data = storage.data(), + .length = storage.size(), + .is_ascii = false, + }; +} + +extern "C" CSS::SelectorFFI::ElementQualifiedName selector_ffi_element_qualified_name(void const* element) { - auto& match_context = rust_match_context(context); auto const& target = ffi_element(element); - auto const& qualified_name = ffi_simple_selector(cxx_simple_selector).qualified_name(); - bool const is_html_element_in_html_document = target.namespace_uri() == Namespace::HTML - && target.document().document_type() == DOM::Document::Type::HTML; - auto const& name_to_match = is_html_element_in_html_document ? qualified_name.name.lowercase_name : qualified_name.name.name; - bool name_matches; - if (is_html_element_in_html_document || matching_mode == TagNameMatchingMode::Fast) - name_matches = target.local_name() == name_to_match; - else - name_matches = target.local_name().equals_ignoring_ascii_case(name_to_match); - return name_matches - && matches_namespace(qualified_name, target, match_context.style_sheet_for_rule); + return { + .local_name = reinterpret_cast(&target.local_name()), + .namespace_ = target.namespace_uri().has_value() ? reinterpret_cast(&target.namespace_uri().value()) : nullptr, + }; } -extern "C" bool selector_ffi_matches_id(void const* element, void const* cxx_simple_selector) +extern "C" uintptr_t const* selector_ffi_element_id(void const* element) { - return ffi_element(element).id() == ffi_simple_selector(cxx_simple_selector).id_name(); + auto const& id = ffi_element(element).id(); + return id.has_value() ? reinterpret_cast(&id.value()) : nullptr; } -extern "C" bool selector_ffi_matches_class(void const* element, void const* cxx_simple_selector) +extern "C" CSS::SelectorFFI::DomStringView selector_ffi_element_id_value(void const* element) { - auto const& target = ffi_element(element); - auto case_sensitivity = target.document().in_quirks_mode() ? CaseSensitivity::CaseInsensitive : CaseSensitivity::CaseSensitive; - return target.has_class(ffi_simple_selector(cxx_simple_selector).class_name(), case_sensitivity); + auto const& id = ffi_element(element).id(); + return id.has_value() ? dom_string_view(id.value()) : CSS::SelectorFFI::DomStringView {}; } -static bool matches_attribute_value(CSS::Selector::SimpleSelector::Attribute::MatchType match_type, Utf16View selector_value, Utf16View element_value, CaseSensitivity case_sensitivity) +extern "C" CSS::SelectorFFI::InternedStringList selector_ffi_element_classes(void const* element) { - bool const case_insensitive = case_sensitivity == CaseSensitivity::CaseInsensitive; - auto values_equal = [&](Utf16View first, Utf16View second) { - return case_insensitive ? first.equals_ignoring_ascii_case(second) : first == second; + auto const& classes = ffi_element(element).class_names(); + return { + .data = reinterpret_cast(classes.data()), + .count = classes.size(), }; +} - switch (match_type) { - case CSS::Selector::SimpleSelector::Attribute::MatchType::HasAttribute: - return true; - case CSS::Selector::SimpleSelector::Attribute::MatchType::ExactValueMatch: - return values_equal(element_value, selector_value); - case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsWord: - if (selector_value.is_empty()) - return false; - return element_value.split_view(' ', SplitBehavior::Nothing).contains([&](auto value) { return values_equal(value, selector_value); }); - case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsString: - return !selector_value.is_empty() - && (case_insensitive ? element_value.find_code_unit_offset_ignoring_case(selector_value).has_value() : element_value.contains(selector_value)); - case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithSegment: - if (element_value.is_empty()) - return selector_value.is_empty(); - if (selector_value.is_empty() || element_value.length_in_code_units() < selector_value.length_in_code_units()) - return false; - if (element_value.length_in_code_units() == selector_value.length_in_code_units()) - return values_equal(element_value, selector_value); - return values_equal(element_value.substring_view(0, selector_value.length_in_code_units()), selector_value) - && element_value.code_unit_at(selector_value.length_in_code_units()) == '-'; - case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithString: - return !selector_value.is_empty() - && selector_value.length_in_code_units() <= element_value.length_in_code_units() - && values_equal(element_value.substring_view(0, selector_value.length_in_code_units()), selector_value); - case CSS::Selector::SimpleSelector::Attribute::MatchType::EndsWithString: - return !selector_value.is_empty() - && selector_value.length_in_code_units() <= element_value.length_in_code_units() - && values_equal(element_value.substring_view(element_value.length_in_code_units() - selector_value.length_in_code_units()), selector_value); - } - VERIFY_NOT_REACHED(); +extern "C" bool selector_ffi_element_id_and_class_names_are_case_insensitive(void const* element) +{ + return ffi_element(element).document().in_quirks_mode(); } -extern "C" bool selector_ffi_matches_attribute(void* context, void const* element, void const* cxx_simple_selector) +extern "C" bool selector_ffi_element_namespace_is_null(void const* element) +{ + return is_in_null_namespace(ffi_element(element)); +} + +extern "C" bool selector_ffi_element_is_html_element_in_html_document(void const* element) { - auto& match_context = rust_match_context(context); auto const& target = ffi_element(element); - auto const& attribute_selector = ffi_simple_selector(cxx_simple_selector).attribute(); - auto const& qualified_name = attribute_selector.qualified_name; - auto const& attribute_name = qualified_name.name.name; - auto const& lowercase_attribute_name = qualified_name.name.lowercase_name; - auto const selector_value = attribute_selector.value.utf16_view(); - auto const match_type = attribute_selector.match_type; - - CaseSensitivity case_sensitivity; - switch (attribute_selector.case_type) { - case CSS::Selector::SimpleSelector::Attribute::CaseType::CaseSensitiveMatch: - case_sensitivity = CaseSensitivity::CaseSensitive; - break; - case CSS::Selector::SimpleSelector::Attribute::CaseType::CaseInsensitiveMatch: - case_sensitivity = CaseSensitivity::CaseInsensitive; - break; - case CSS::Selector::SimpleSelector::Attribute::CaseType::DefaultMatch: - case_sensitivity = target.document().is_html_document() - && target.namespace_uri() == Namespace::HTML - && qualified_name.namespace_type == CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Default - && fly_string_is_one_of_utf16( - attribute_name, - HTML::AttributeNames::accept, HTML::AttributeNames::accept_charset, HTML::AttributeNames::align, - HTML::AttributeNames::alink, HTML::AttributeNames::axis, HTML::AttributeNames::bgcolor, HTML::AttributeNames::charset, - HTML::AttributeNames::checked, HTML::AttributeNames::clear, HTML::AttributeNames::codetype, HTML::AttributeNames::color, - HTML::AttributeNames::compact, HTML::AttributeNames::declare, HTML::AttributeNames::defer, HTML::AttributeNames::dir, - HTML::AttributeNames::direction, HTML::AttributeNames::disabled, HTML::AttributeNames::enctype, HTML::AttributeNames::face, - HTML::AttributeNames::frame, HTML::AttributeNames::hreflang, HTML::AttributeNames::http_equiv, HTML::AttributeNames::lang, - HTML::AttributeNames::language, HTML::AttributeNames::link, HTML::AttributeNames::media, HTML::AttributeNames::method, - HTML::AttributeNames::multiple, HTML::AttributeNames::nohref, HTML::AttributeNames::noresize, HTML::AttributeNames::noshade, - HTML::AttributeNames::nowrap, HTML::AttributeNames::readonly, HTML::AttributeNames::rel, HTML::AttributeNames::rev, - HTML::AttributeNames::rules, HTML::AttributeNames::scope, HTML::AttributeNames::scrolling, HTML::AttributeNames::selected, - HTML::AttributeNames::shape, HTML::AttributeNames::target, HTML::AttributeNames::text, HTML::AttributeNames::type, - HTML::AttributeNames::valign, HTML::AttributeNames::valuetype, HTML::AttributeNames::vlink) - ? CaseSensitivity::CaseInsensitive - : CaseSensitivity::CaseSensitive; - break; - } + return target.namespace_uri() == Namespace::HTML + && target.document().document_type() == DOM::Document::Type::HTML; +} - auto attribute_matches = [&](DOM::Attr const& attribute) { - return matches_attribute_value(match_type, selector_value, attribute.value().utf16_view(), case_sensitivity); - }; +extern "C" bool selector_ffi_element_is_document_root(void const* element) +{ + return is(ffi_element(element)); +} - switch (qualified_name.namespace_type) { - // "In keeping with the Namespaces in the XML recommendation, default namespaces do not apply to attributes, - // therefore attribute selectors without a namespace component apply only to attributes that have no namespace (equivalent to "|attr")" - case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Default: - case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::None: { - auto const& name_to_match = target.document().is_html_document() && target.namespace_uri() == Namespace::HTML - ? lowercase_attribute_name - : attribute_name; - for (u32 i = 0; i < target.attributes()->length(); ++i) { - auto const* attribute = target.attributes()->item(i); - if (!attribute->namespace_uri().has_value() && attribute->local_name() == name_to_match) - return attribute_matches(*attribute); - } - return false; - } - case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Any: { - bool const use_lowercase_name = target.document().is_html_document() && target.namespace_uri() == Namespace::HTML; - auto const& name_to_match = use_lowercase_name ? lowercase_attribute_name : attribute_name; - for (u32 i = 0; i < target.attributes()->length(); ++i) { - auto const* attribute = target.attributes()->item(i); - if (attribute->local_name() == name_to_match && attribute_matches(*attribute)) - return true; - } - return false; - } - case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Named: { - if (!match_context.style_sheet_for_rule) - return false; - auto selector_namespace = match_context.style_sheet_for_rule->namespace_uri(qualified_name.namespace_); - if (!selector_namespace.has_value()) - return false; - for (u32 i = 0; i < target.attributes()->length(); ++i) { - auto const* attribute = target.attributes()->item(i); - if (attribute->namespace_uri().has_value() - && *selector_namespace == attribute->namespace_uri()->view() - && attribute->local_name() == attribute_name) - return attribute_matches(*attribute); - } - return false; - } - } - VERIFY_NOT_REACHED(); +extern "C" bool selector_ffi_element_is_link(void const* element) +{ + return ffi_element(element).matches_link_pseudo_class(); } -static bool is_rust_structural_or_functional_pseudo_class(CSS::PseudoClass pseudo_class) -{ - return first_is_one_of( - pseudo_class, - CSS::PseudoClass::Empty, - CSS::PseudoClass::FirstChild, - CSS::PseudoClass::FirstOfType, - CSS::PseudoClass::Has, - CSS::PseudoClass::Host, - CSS::PseudoClass::Is, - CSS::PseudoClass::LastChild, - CSS::PseudoClass::LastOfType, - CSS::PseudoClass::Not, - CSS::PseudoClass::NthChild, - CSS::PseudoClass::NthLastChild, - CSS::PseudoClass::NthLastOfType, - CSS::PseudoClass::NthOfType, - CSS::PseudoClass::OnlyChild, - CSS::PseudoClass::OnlyOfType, - CSS::PseudoClass::Root, - CSS::PseudoClass::Scope, - CSS::PseudoClass::Where, - CSS::PseudoClass::Dir, - CSS::PseudoClass::Heading, - CSS::PseudoClass::Lang, - CSS::PseudoClass::State); -} - -extern "C" bool selector_ffi_matches_pseudo_class(void const* element, u8 pseudo_class_value) +extern "C" bool selector_ffi_element_is_fullscreen(void const* element) { - auto pseudo_class = static_cast(pseudo_class_value); - VERIFY(pseudo_class < CSS::PseudoClass::__Count); - VERIFY(!is_rust_structural_or_functional_pseudo_class(pseudo_class)); - return matches_pseudo_class_state(pseudo_class, ffi_element(element)); + return ffi_element(element).is_fullscreen_element(); +} + +extern "C" i64 selector_ffi_element_heading_level(void const* element) +{ + auto const* heading = as_if(ffi_element(element)); + return heading ? heading->heading_level() : 0; +} + +extern "C" bool selector_ffi_element_has_popover_attribute(void const* element) +{ + return ffi_element(element).has_attribute(HTML::AttributeNames::popover); } -extern "C" bool selector_ffi_matches_language(void const* element, StringView language) +extern "C" bool selector_ffi_element_popover_is_showing(void const* element) { - auto element_language = ffi_element(element).lang(); - return element_language.has_value() - && language_range_matches_tag(ffi_string_view(language), *element_language); + auto const* html_element = as_if(ffi_element(element)); + return html_element + && html_element->popover_visibility_state() == HTML::HTMLElement::PopoverVisibilityState::Showing; } -extern "C" bool selector_ffi_matches_direction(void const* element, Direction direction) +extern "C" Direction selector_ffi_element_direction(void const* element) { switch (ffi_element(element).directionality()) { case DOM::Element::Directionality::Ltr: - return direction == Direction::LeftToRight; + return Direction::LeftToRight; case DOM::Element::Directionality::Rtl: - return direction == Direction::RightToLeft; + return Direction::RightToLeft; } VERIFY_NOT_REACHED(); } -extern "C" bool selector_ffi_matches_state(void const* element, void const* cxx_simple_selector) +extern "C" bool selector_ffi_element_has_custom_state(void const* element, uintptr_t state) { auto const& target = ffi_element(element); if (!target.is_custom()) return false; if (auto custom_state_set = target.custom_state_set()) - return custom_state_set->has_state(ffi_simple_selector(cxx_simple_selector).pseudo_class().ident->string_value); + return custom_state_set->has_state(*reinterpret_cast(&state)); return false; } -extern "C" bool selector_ffi_matches_heading(void const* element, i64 const* levels, size_t level_count) +extern "C" CSS::SelectorFFI::DomStringView selector_ffi_element_language(void const* element) { - auto const* heading = as_if(ffi_element(element)); - if (!heading) + auto language = ffi_element(element).lang_view(); + return language.has_value() ? dom_string_view(*language) : CSS::SelectorFFI::DomStringView {}; +} + +extern "C" bool selector_ffi_element_is_focused(void const* element) +{ + return ffi_element(element).is_focused(); +} + +extern "C" bool selector_ffi_element_should_indicate_focus(void const* element) +{ + return ffi_element(element).should_indicate_focus(); +} + +extern "C" bool selector_ffi_element_has_focus_within(void const* element) +{ + return ffi_element(element).matches_focus_within_pseudo_class(); +} + +extern "C" bool selector_ffi_element_is_active(void const* element) +{ + return ffi_element(element).is_being_activated(); +} + +extern "C" bool selector_ffi_element_is_checked(void const* element) +{ + return ffi_element(element).matches_checked_pseudo_class(); +} + +extern "C" bool selector_ffi_element_is_defined(void const* element) +{ + return ffi_element(element).is_defined(); +} + +extern "C" bool selector_ffi_element_is_disabled(void const* element) +{ + return ffi_element(element).matches_disabled_pseudo_class(); +} + +extern "C" bool selector_ffi_element_is_enabled(void const* element) +{ + return ffi_element(element).matches_enabled_pseudo_class(); +} + +extern "C" bool selector_ffi_element_is_local_link(void const* element) +{ + return ffi_element(element).matches_local_link_pseudo_class(); +} + +extern "C" bool selector_ffi_element_is_placeholder_shown(void const* element) +{ + return ffi_element(element).matches_placeholder_shown_pseudo_class(); +} + +extern "C" bool selector_ffi_element_is_target(void const* element) +{ + return ffi_element(element).is_target(); +} + +extern "C" bool selector_ffi_element_is_unchecked(void const* element) +{ + return ffi_element(element).matches_unchecked_pseudo_class(); +} + +extern "C" bool selector_ffi_element_is_media_element(void const* element) +{ + return is(ffi_element(element)); +} + +extern "C" bool selector_ffi_element_media_is_blocked(void const* element) +{ + auto const* media_element = as_if(ffi_element(element)); + return media_element && media_element->blocked(); +} + +extern "C" bool selector_ffi_element_media_is_muted(void const* element) +{ + auto const* media_element = as_if(ffi_element(element)); + return media_element && media_element->muted(); +} + +extern "C" bool selector_ffi_element_media_is_paused(void const* element) +{ + auto const* media_element = as_if(ffi_element(element)); + return media_element && media_element->paused(); +} + +extern "C" bool selector_ffi_element_media_is_seeking(void const* element) +{ + auto const* media_element = as_if(ffi_element(element)); + return media_element && media_element->seeking(); +} + +extern "C" bool selector_ffi_element_media_is_stalled(void const* element) +{ + auto const* media_element = as_if(ffi_element(element)); + return media_element && media_element->stalled(); +} + +// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-default +extern "C" bool selector_ffi_element_is_default(void const* element) +{ + auto const& target = ffi_element(element); + auto const* form_associated_element = as_if(target); + if (!form_associated_element) return false; - if (level_count == 0) + if (form_associated_element->is_submit_button() && form_associated_element->form() && form_associated_element->form()->default_button() == form_associated_element) return true; - VERIFY(levels); - return ReadonlySpan { levels, level_count }.contains_slow(heading->heading_level()); + if (auto const* input_element = as_if(form_associated_element)) + return input_element->checked_applies() && input_element->has_attribute(HTML::AttributeNames::checked); + if (auto const* option_element = as_if(form_associated_element)) + return option_element->has_attribute(HTML::AttributeNames::selected); + return false; +} + +extern "C" CSS::SelectorFFI::FfiMeterValueState selector_ffi_element_meter_value_state(void const* element) +{ + auto const* meter = as_if(ffi_element(element)); + if (!meter) + return CSS::SelectorFFI::FfiMeterValueState::NotMeter; + switch (meter->value_state()) { + case HTML::HTMLMeterElement::ValueState::EvenLessGood: + return CSS::SelectorFFI::FfiMeterValueState::EvenLessGood; + case HTML::HTMLMeterElement::ValueState::Suboptimal: + return CSS::SelectorFFI::FfiMeterValueState::Suboptimal; + case HTML::HTMLMeterElement::ValueState::Optimal: + return CSS::SelectorFFI::FfiMeterValueState::Optimal; + } + VERIFY_NOT_REACHED(); +} + +extern "C" bool selector_ffi_element_meter_value_is_high(void const* element) +{ + auto const* meter = as_if(ffi_element(element)); + return meter && meter->value() > meter->high(); +} + +extern "C" bool selector_ffi_element_meter_value_is_low(void const* element) +{ + auto const* meter = as_if(ffi_element(element)); + return meter && meter->value() < meter->low(); +} + +extern "C" bool selector_ffi_element_is_hovered(void const* element) +{ + return matches_hover_pseudo_class(ffi_element(element)); +} + +extern "C" bool selector_ffi_element_is_indeterminate(void const* element) +{ + return matches_indeterminate_pseudo_class(ffi_element(element)); +} + +// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-invalid +// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-valid +extern "C" CSS::SelectorFFI::FfiValidityState selector_ffi_element_validity_state(void const* element) +{ + auto const& target = ffi_element(element); + if (auto const* form_associated_element = as_if(target)) { + if (form_associated_element->is_candidate_for_constraint_validation()) { + return form_associated_element->satisfies_its_constraints() + ? CSS::SelectorFFI::FfiValidityState::Valid + : CSS::SelectorFFI::FfiValidityState::Invalid; + } + } + + auto const* form_element = as_if(target); + if (!form_element && !is(target)) + return CSS::SelectorFFI::FfiValidityState::NotApplicable; + + bool has_invalid_elements = false; + target.for_each_in_subtree([&](auto& node) { + auto const* form_associated_element = as_if(&node); + if (!form_associated_element) + return TraversalDecision::Continue; + if (form_element && form_associated_element->form() != form_element) + return TraversalDecision::Continue; + if (form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) { + has_invalid_elements = true; + return TraversalDecision::Break; + } + return TraversalDecision::Continue; + }); + return has_invalid_elements + ? CSS::SelectorFFI::FfiValidityState::Invalid + : CSS::SelectorFFI::FfiValidityState::Valid; +} + +// https://drafts.csswg.org/selectors/#modal-state +extern "C" bool selector_ffi_element_is_modal(void const* element) +{ + auto const* dialog_element = as_if(ffi_element(element)); + // FIXME: Fullscreen elements are also modal. + return dialog_element && dialog_element->is_modal(); +} + +extern "C" bool selector_ffi_element_is_open(void const* element) +{ + return matches_open_state_pseudo_class(ffi_element(element)); +} + +// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-optional +// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-required +extern "C" CSS::SelectorFFI::FfiRequiredState selector_ffi_element_required_state(void const* element) +{ + auto const& target = ffi_element(element); + if (auto const* input_element = as_if(target)) { + if (input_element->required_applies()) + return input_element->has_attribute(HTML::AttributeNames::required) + ? CSS::SelectorFFI::FfiRequiredState::Required + : CSS::SelectorFFI::FfiRequiredState::Optional; + // AD-HOC: Chromium and WebKit also match :optional for hidden inputs. + return input_element->type_state() == HTML::HTMLInputElement::TypeAttributeState::Hidden + ? CSS::SelectorFFI::FfiRequiredState::Optional + : CSS::SelectorFFI::FfiRequiredState::NotApplicable; + } + if (is(target) || is(target)) + return target.has_attribute(HTML::AttributeNames::required) + ? CSS::SelectorFFI::FfiRequiredState::Required + : CSS::SelectorFFI::FfiRequiredState::Optional; + return CSS::SelectorFFI::FfiRequiredState::NotApplicable; +} + +extern "C" bool selector_ffi_element_is_read_write(void const* element) +{ + return matches_read_write_pseudo_class(ffi_element(element)); +} + +// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-user-valid +// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-user-invalid +extern "C" CSS::SelectorFFI::FfiValidityState selector_ffi_element_user_validity_state(void const* element) +{ + auto const& target = ffi_element(element); + bool user_validity = false; + if (auto const* input_element = as_if(target)) + user_validity = input_element->user_validity(); + else if (auto const* select_element = as_if(target)) + user_validity = select_element->user_validity(); + else if (auto const* text_area_element = as_if(target)) + user_validity = text_area_element->user_validity(); + else + return CSS::SelectorFFI::FfiValidityState::NotApplicable; + if (!user_validity) + return CSS::SelectorFFI::FfiValidityState::NotApplicable; + + auto const& form_associated_element = as(target); + if (!form_associated_element.is_candidate_for_constraint_validation()) + return CSS::SelectorFFI::FfiValidityState::NotApplicable; + return form_associated_element.satisfies_its_constraints() + ? CSS::SelectorFFI::FfiValidityState::Valid + : CSS::SelectorFFI::FfiValidityState::Invalid; +} + +extern "C" CSS::SelectorFFI::DomStringView selector_ffi_element_local_name(void const* element) +{ + return dom_string_view(ffi_element(element).local_name()); +} + +extern "C" CSS::SelectorFFI::DomStringView selector_ffi_element_class_name(void const* element, size_t index) +{ + auto const& classes = ffi_element(element).class_names(); + VERIFY(index < classes.size()); + return dom_string_view(classes[index]); +} + +extern "C" size_t selector_ffi_element_attribute_count(void const* element) +{ + auto count = ffi_element(element).attribute_list_size(); + VERIFY(count <= NumericLimits::max()); + return count; +} + +extern "C" CSS::SelectorFFI::DomAttribute selector_ffi_element_attribute(void const* element, size_t index) +{ + auto const& target = ffi_element(element); + VERIFY(index < target.attribute_list_size()); + auto const* attribute = target.attributes()->item(static_cast(index)); + VERIFY(attribute); + return { + .local_name = interned_string_identity(attribute->local_name()), + .namespace_ = attribute->namespace_uri().has_value() ? interned_string_identity(*attribute->namespace_uri()) : 0, + .has_namespace = attribute->namespace_uri().has_value(), + .value = dom_string_view(attribute->value()), + }; +} + +extern "C" CSS::SelectorFFI::ResolvedNamespace selector_ffi_default_namespace(void* context) +{ + auto& match_context = rust_match_context(context); + if (!match_context.style_sheet_for_rule || !match_context.style_sheet_for_rule->default_namespace_rule()) { + return { + .namespace_type = CSS::SelectorFFI::ResolvedNamespaceType::Missing, + .namespace_ = 0, + }; + } + + auto const& namespace_ = match_context.style_sheet_for_rule->default_namespace_rule()->namespace_uri(); + if (namespace_.is_empty()) { + return { + .namespace_type = CSS::SelectorFFI::ResolvedNamespaceType::Null, + .namespace_ = 0, + }; + } + return { + .namespace_type = CSS::SelectorFFI::ResolvedNamespaceType::Named, + .namespace_ = interned_string_identity(namespace_), + }; +} + +extern "C" CSS::SelectorFFI::ResolvedNamespace selector_ffi_resolve_namespace(void* context, StringView prefix) +{ + auto& match_context = rust_match_context(context); + if (!match_context.style_sheet_for_rule) { + return { + .namespace_type = CSS::SelectorFFI::ResolvedNamespaceType::Missing, + .namespace_ = 0, + }; + } + + auto namespace_ = match_context.style_sheet_for_rule->namespace_uri(ffi_string_view(prefix)); + if (!namespace_.has_value()) { + return { + .namespace_type = CSS::SelectorFFI::ResolvedNamespaceType::Missing, + .namespace_ = 0, + }; + } + if (namespace_->is_empty()) { + return { + .namespace_type = CSS::SelectorFFI::ResolvedNamespaceType::Null, + .namespace_ = 0, + }; + } + return { + .namespace_type = CSS::SelectorFFI::ResolvedNamespaceType::Named, + .namespace_ = interned_string_identity(*namespace_), + }; } -extern "C" void const* selector_ffi_parent_element(void const* element, void const* shadow_host) +extern "C" CSS::SelectorFFI::Element selector_ffi_parent_element(void const* element, void const* shadow_host) { auto const& target = ffi_element(element); if (!shadow_host) - return target.parent_element(); + return element_to_ffi(target.parent_element()); if (element == shadow_host) - return nullptr; - return target.parent_or_shadow_host_element(); + return {}; + return element_to_ffi(target.parent_or_shadow_host_element()); } -extern "C" void const* selector_ffi_parent_element_in_light_tree(void const* element) +extern "C" CSS::SelectorFFI::Element selector_ffi_parent_element_in_light_tree(void const* element) { - return ffi_element(element).parent_element(); + return element_to_ffi(ffi_element(element).parent_element()); } -extern "C" void const* selector_ffi_previous_element_sibling(void const* element) +extern "C" CSS::SelectorFFI::Element selector_ffi_previous_element_sibling(void const* element) { - return ffi_element(element).previous_element_sibling(); + return element_to_ffi(ffi_element(element).previous_element_sibling()); } -extern "C" void const* selector_ffi_next_element_sibling(void const* element) +extern "C" CSS::SelectorFFI::Element selector_ffi_next_element_sibling(void const* element) { - return ffi_element(element).next_element_sibling(); + return element_to_ffi(ffi_element(element).next_element_sibling()); } -extern "C" void const* selector_ffi_first_element_child(void const* element) +extern "C" CSS::SelectorFFI::Element selector_ffi_first_element_child(void const* element) { - return ffi_element(element).first_child_of_type(); + return element_to_ffi(ffi_element(element).first_child_of_type()); } -extern "C" void const* selector_ffi_first_element_descendant(void const* element) +extern "C" CSS::SelectorFFI::Element selector_ffi_first_element_descendant(void const* element) { auto const& root = ffi_element(element); for (auto const* node = root.first_child(); node; node = node->next_in_pre_order(&root)) { if (node->is_element()) - return static_cast(node); + return element_to_ffi(static_cast(node)); } - return nullptr; + return {}; } -extern "C" void const* selector_ffi_next_element_descendant(void const* element, void const* root) +extern "C" CSS::SelectorFFI::Element selector_ffi_next_element_descendant(void const* element, void const* root) { auto const& root_element = ffi_element(root); for (auto const* node = static_cast(&ffi_element(element))->next_in_pre_order(&root_element); node; node = node->next_in_pre_order(&root_element)) { if (node->is_element()) - return static_cast(node); + return element_to_ffi(static_cast(node)); } - return nullptr; + return {}; } -extern "C" bool selector_ffi_has_no_element_or_nonempty_text_children(void const* element) +extern "C" bool selector_ffi_element_has_element_child(void const* element) +{ + return ffi_element(element).first_child_of_type(); +} + +extern "C" bool selector_ffi_element_has_nonempty_text_child(void const* element) { - auto const& target = ffi_element(element); - if (!target.has_children()) - return true; - if (target.first_child_of_type()) - return false; bool has_nonempty_text_child = false; - target.for_each_child_of_type([&](auto const& text) { + ffi_element(element).for_each_child_of_type([&](auto const& text) { if (!text.data().is_empty()) { has_nonempty_text_child = true; return IterationDecision::Break; } return IterationDecision::Continue; }); - return !has_nonempty_text_child; -} - -extern "C" bool selector_ffi_has_same_type(void const* first, void const* second) -{ - auto const& first_element = ffi_element(first); - auto const& second_element = ffi_element(second); - return first_element.local_name() == second_element.local_name() - && first_element.namespace_uri() == second_element.namespace_uri(); -} - -extern "C" bool selector_ffi_is_document_root(void const* element) -{ - return is(ffi_element(element)); + return has_nonempty_text_child; } extern "C" bool selector_ffi_is_shadow_tree_slot(void const* element) @@ -1139,8 +1011,8 @@ extern "C" CSS::SelectorFFI::ElementAndShadowHost selector_ffi_slotted_parent(vo if (slot_shadow_root != match_context.rule_shadow_root) continue; return { - .element = slot, - .shadow_host = slot_shadow_root ? slot_shadow_root->host() : nullptr, + .element = element_to_ffi(slot), + .shadow_host = element_to_ffi(slot_shadow_root ? slot_shadow_root->host() : nullptr), }; } return {}; @@ -1182,7 +1054,7 @@ extern "C" CSS::SelectorFFI::ElementAndShadowHost selector_ffi_part_parent(void* else next_shadow_host = nullptr; } - return { .element = &host, .shadow_host = next_shadow_host }; + return { .element = element_to_ffi(&host), .shadow_host = element_to_ffi(next_shadow_host) }; } return {}; } diff --git a/Libraries/LibWeb/CSS/SelectorRustBridge.cpp b/Libraries/LibWeb/CSS/SelectorRustBridge.cpp index f0bee1fdb3c44..f64c45b2529e9 100644 --- a/Libraries/LibWeb/CSS/SelectorRustBridge.cpp +++ b/Libraries/LibWeb/CSS/SelectorRustBridge.cpp @@ -152,14 +152,13 @@ class SelectorCompiler { output.namespace_ = store_string(qualified_name.namespace_); output.name = store_string(qualified_name.name.name); output.lowercase_name = store_string(qualified_name.name.lowercase_name); + output.interned_name = reinterpret_cast(&qualified_name.name.name); + output.interned_lowercase_name = reinterpret_cast(&qualified_name.name.lowercase_name); } SelectorFFI::SimpleSelector compile_simple_selector(Selector::SimpleSelector const& simple_selector) { SelectorFFI::SimpleSelector output {}; - // NB: The C++ simple selector outlives the compiled Rust selector, so matching callbacks - // can use this pointer to compare interned strings without copying them. - output.cxx_simple_selector = &simple_selector; switch (simple_selector.type) { case Selector::SimpleSelector::Type::Universal: @@ -173,10 +172,12 @@ class SelectorCompiler { case Selector::SimpleSelector::Type::Id: output.selector_type = SelectorFFI::SimpleSelectorType::Id; output.name = store_string(simple_selector.id_name()); + output.interned_name = reinterpret_cast(&simple_selector.id_name()); break; case Selector::SimpleSelector::Type::Class: output.selector_type = SelectorFFI::SimpleSelectorType::Class; output.name = store_string(simple_selector.class_name()); + output.interned_name = reinterpret_cast(&simple_selector.class_name()); break; case Selector::SimpleSelector::Type::Attribute: { output.selector_type = SelectorFFI::SimpleSelectorType::Attribute; @@ -230,6 +231,7 @@ class SelectorCompiler { if (pseudo_class.ident.has_value()) { output.identifier = store_string(pseudo_class.ident->string_value); + output.interned_name = reinterpret_cast(&pseudo_class.ident->string_value); if (pseudo_class.ident->keyword == Keyword::Ltr) output.direction = SelectorFFI::Direction::LeftToRight; else if (pseudo_class.ident->keyword == Keyword::Rtl) diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index fd07f5f2424f6..c7c3ab35ec250 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -5123,6 +5123,15 @@ Optional Element::lang() const return m_lang_value; } +Optional Element::lang_view() const +{ + if (!m_lang_value.has_value()) + (void)lang(); + if (m_lang_value->is_empty()) + return {}; + return m_lang_value->utf16_view(); +} + void Element::invalidate_lang_value() { if (m_lang_value.has_value()) { diff --git a/Libraries/LibWeb/DOM/Element.h b/Libraries/LibWeb/DOM/Element.h index 5afaa2e7a83e1..096ad8c297c60 100644 --- a/Libraries/LibWeb/DOM/Element.h +++ b/Libraries/LibWeb/DOM/Element.h @@ -146,6 +146,7 @@ class WEB_API Element void follow_the_hyperlink(Optional hyperlink_suffix, HTML::UserNavigationInvolvement = HTML::UserNavigationInvolvement::None); Optional lang() const; + Optional lang_view() const; void invalidate_lang_value(); WebIDL::ExceptionOr set_attribute_for_bindings(Utf16FlyString qualified_name, Variant, GC::Ref, GC::Ref, Utf16String> const& value); diff --git a/Tests/LibWeb/Text/expected/css/live-media-state-selectors.txt b/Tests/LibWeb/Text/expected/css/live-media-state-selectors.txt new file mode 100644 index 0000000000000..487ded822147b --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/live-media-state-selectors.txt @@ -0,0 +1,13 @@ +video:buffering: true +video:muted: false +video:paused: true +video:playing: false +video:seeking: false +video:stalled: false +video:muted after mutation: true +div:buffering: false +div:muted: false +div:paused: false +div:playing: false +div:seeking: false +div:stalled: false diff --git a/Tests/LibWeb/Text/expected/css/live-selector-identifiers.txt b/Tests/LibWeb/Text/expected/css/live-selector-identifiers.txt new file mode 100644 index 0000000000000..864c42a1d4efe --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/live-selector-identifiers.txt @@ -0,0 +1,10 @@ +initial match: true +initial color: rgb(1, 2, 3) +mutated match: true +stale match: false +mutated color: rgb(4, 5, 6) +disconnected match: true +shadow initial match: true +shadow mutated match: true +document root: true +disconnected html root: true diff --git a/Tests/LibWeb/Text/expected/css/selector-engine-characterization.txt b/Tests/LibWeb/Text/expected/css/selector-engine-characterization.txt index 9838cf8887d82..1308d20c07531 100644 --- a/Tests/LibWeb/Text/expected/css/selector-engine-characterization.txt +++ b/Tests/LibWeb/Text/expected/css/selector-engine-characterization.txt @@ -4,6 +4,7 @@ adjacent sibling: true subsequent sibling: true attribute insensitive: true attribute sensitive: false +attribute after mutation: true/false logical: true positional: true relative descendant: true @@ -12,6 +13,7 @@ relative sibling: true closest: true query selector all: 3 XML tag case: true/false +XML slow tag case: false XML namespace wildcard: true XML attribute namespace wildcard: true host: yes diff --git a/Tests/LibWeb/Text/expected/non-html-mixed-case-element-name-selector-matching.txt b/Tests/LibWeb/Text/expected/non-html-mixed-case-element-name-selector-matching.txt index ff6e3f2858fa9..74bca0e144853 100644 --- a/Tests/LibWeb/Text/expected/non-html-mixed-case-element-name-selector-matching.txt +++ b/Tests/LibWeb/Text/expected/non-html-mixed-case-element-name-selector-matching.txt @@ -2,3 +2,5 @@ ✅ Pass: Selector match for SVG element clipPath. ✅ Pass: Selector match for SVG element foreignObject. ✅ Pass: Selector match for SVG element radialGradient. +✅ Pass: Exact-case selector match for SVG element linearGradient. +✅ Pass: Selector matching for SVG element linearGradient is case-sensitive. diff --git a/Tests/LibWeb/Text/expected/quirks-mode-case-insensitive-id-selector.txt b/Tests/LibWeb/Text/expected/quirks-mode-case-insensitive-id-selector.txt new file mode 100644 index 0000000000000..7ab628d28f76a --- /dev/null +++ b/Tests/LibWeb/Text/expected/quirks-mode-case-insensitive-id-selector.txt @@ -0,0 +1 @@ +ParentNode.querySelector matches ID selectors case-insensitively in quirks mode: true diff --git a/Tests/LibWeb/Text/input/css/live-media-state-selectors.html b/Tests/LibWeb/Text/input/css/live-media-state-selectors.html new file mode 100644 index 0000000000000..9b1034bb45bc9 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/live-media-state-selectors.html @@ -0,0 +1,19 @@ + + + +
+ diff --git a/Tests/LibWeb/Text/input/css/live-selector-identifiers.html b/Tests/LibWeb/Text/input/css/live-selector-identifiers.html new file mode 100644 index 0000000000000..51a90710a85c0 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/live-selector-identifiers.html @@ -0,0 +1,51 @@ + + + +
+ +
+
+ diff --git a/Tests/LibWeb/Text/input/css/selector-engine-characterization.html b/Tests/LibWeb/Text/input/css/selector-engine-characterization.html index abcd5298cbe2c..8a003008493ad 100644 --- a/Tests/LibWeb/Text/input/css/selector-engine-characterization.html +++ b/Tests/LibWeb/Text/input/css/selector-engine-characterization.html @@ -19,6 +19,8 @@ println(`subsequent sibling: ${document.querySelector(".third").matches(".first ~ .third")}`); println(`attribute insensitive: ${outer.matches('[data-mode="mixed" i]')}`); println(`attribute sensitive: ${outer.matches('[data-mode="mixed" s]')}`); + outer.setAttribute("data-mode", "changed"); + println(`attribute after mutation: ${outer.matches('[data-mode="changed"]')}/${outer.matches('[data-mode="mixed" i]')}`); println(`logical: ${second.matches(":is(.first, .second):not(.third)")}`); println(`positional: ${second.matches(":nth-child(2 of .item)")}`); println(`relative descendant: ${outer.matches(":has(.second .needle)")}`); @@ -28,10 +30,12 @@ println(`query selector all: ${outer.querySelectorAll(":scope > .item").length}`); const xml = document.implementation.createDocument("urn:example", "ex:root"); const child = xml.createElementNS("urn:example", "ex:Child"); + child.setAttribute("other", "no"); child.setAttributeNS("urn:attributes", "attr:value", "yes"); xml.documentElement.appendChild(child); println(`XML tag case: ${child.matches("Child")}/${child.matches("child")}`); + println(`XML slow tag case: ${child.matches("child:not(.missing)")}`); println(`XML namespace wildcard: ${child.matches("*|Child")}`); println(`XML attribute namespace wildcard: ${child.matches("[*|value]")}`); const host = document.createElement("div"); diff --git a/Tests/LibWeb/Text/input/non-html-mixed-case-element-name-selector-matching.html b/Tests/LibWeb/Text/input/non-html-mixed-case-element-name-selector-matching.html index f22888e8d0eaa..3dc4db2305c39 100644 --- a/Tests/LibWeb/Text/input/non-html-mixed-case-element-name-selector-matching.html +++ b/Tests/LibWeb/Text/input/non-html-mixed-case-element-name-selector-matching.html @@ -26,5 +26,13 @@ println("✅ Pass: Selector match for SVG element radialGradient."); else println("❌ Fail: No selector match for SVG element radialGradient."); + if (document.querySelector("linearGradient:not(.missing)")) + println("✅ Pass: Exact-case selector match for SVG element linearGradient."); + else + println("❌ Fail: No exact-case selector match for SVG element linearGradient."); + if (!document.querySelector("lineargradient:not(.missing)")) + println("✅ Pass: Selector matching for SVG element linearGradient is case-sensitive."); + else + println("❌ Fail: Selector matching for SVG element linearGradient is not case-sensitive."); }); diff --git a/Tests/LibWeb/Text/input/quirks-mode-case-insensitive-class-selector.html b/Tests/LibWeb/Text/input/quirks-mode-case-insensitive-class-selector.html index 810a266601453..9d195c3601775 100644 --- a/Tests/LibWeb/Text/input/quirks-mode-case-insensitive-class-selector.html +++ b/Tests/LibWeb/Text/input/quirks-mode-case-insensitive-class-selector.html @@ -1,6 +1,6 @@ -
+
+
+