From f63a945ba0300551417c740e450957f29c9b73d1 Mon Sep 17 00:00:00 2001 From: Diego Frias Date: Sat, 13 Jul 2024 09:19:30 -0700 Subject: [PATCH] LibWeb: Implement the `:has()` pseudo-class See https://drafts.csswg.org/selectors-4/#relational. --- .../LibWeb/GenerateCSSPseudoClass.cpp | 3 + Tests/LibWeb/Ref/css-has-compound.html | 11 +++ Tests/LibWeb/Ref/css-has-descendant.html | 9 ++ Tests/LibWeb/Ref/css-has-direct-child.html | 9 ++ Tests/LibWeb/Ref/css-has-next-sibling.html | 11 +++ .../Ref/css-has-subsequent-sibling.html | 19 ++++ Tests/LibWeb/Ref/css-nested-has.html | 8 ++ .../LibWeb/Ref/css-pseudo-element-in-has.html | 12 +++ .../Ref/reference/css-has-compound.html | 4 + .../Ref/reference/css-has-descendant.html | 2 + .../Ref/reference/css-has-direct-child.html | 2 + .../Ref/reference/css-has-next-sibling.html | 4 + .../reference/css-has-subsequent-sibling.html | 12 +++ .../LibWeb/Ref/reference/css-nested-has.html | 1 + .../reference/css-pseudo-element-in-has.html | 1 + .../LibWeb/CSS/Parser/SelectorParsing.cpp | 6 +- .../Libraries/LibWeb/CSS/PseudoClasses.json | 3 + Userland/Libraries/LibWeb/CSS/Selector.cpp | 1 + .../Libraries/LibWeb/CSS/SelectorEngine.cpp | 94 ++++++++++++++++--- .../Libraries/LibWeb/CSS/SelectorEngine.h | 7 +- Userland/Libraries/LibWeb/Dump.cpp | 1 + 21 files changed, 203 insertions(+), 17 deletions(-) create mode 100644 Tests/LibWeb/Ref/css-has-compound.html create mode 100644 Tests/LibWeb/Ref/css-has-descendant.html create mode 100644 Tests/LibWeb/Ref/css-has-direct-child.html create mode 100644 Tests/LibWeb/Ref/css-has-next-sibling.html create mode 100644 Tests/LibWeb/Ref/css-has-subsequent-sibling.html create mode 100644 Tests/LibWeb/Ref/css-nested-has.html create mode 100644 Tests/LibWeb/Ref/css-pseudo-element-in-has.html create mode 100644 Tests/LibWeb/Ref/reference/css-has-compound.html create mode 100644 Tests/LibWeb/Ref/reference/css-has-descendant.html create mode 100644 Tests/LibWeb/Ref/reference/css-has-direct-child.html create mode 100644 Tests/LibWeb/Ref/reference/css-has-next-sibling.html create mode 100644 Tests/LibWeb/Ref/reference/css-has-subsequent-sibling.html create mode 100644 Tests/LibWeb/Ref/reference/css-nested-has.html create mode 100644 Tests/LibWeb/Ref/reference/css-pseudo-element-in-has.html diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoClass.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoClass.cpp index 8c1efb07dde..a35a852e90d 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoClass.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoClass.cpp @@ -72,6 +72,7 @@ struct PseudoClassMetadata { ANPlusBOf, CompoundSelector, ForgivingSelectorList, + ForgivingRelativeSelectorList, Ident, LanguageRanges, SelectorList, @@ -167,6 +168,8 @@ PseudoClassMetadata pseudo_class_metadata(PseudoClass pseudo_class) parameter_type = "CompoundSelector"_string; } else if (argument_string == ""sv) { parameter_type = "ForgivingSelectorList"_string; + } else if (argument_string == ""sv) { + parameter_type = "ForgivingRelativeSelectorList"_string; } else if (argument_string == ""sv) { parameter_type = "Ident"_string; } else if (argument_string == ""sv) { diff --git a/Tests/LibWeb/Ref/css-has-compound.html b/Tests/LibWeb/Ref/css-has-compound.html new file mode 100644 index 00000000000..0ebd135407b --- /dev/null +++ b/Tests/LibWeb/Ref/css-has-compound.html @@ -0,0 +1,11 @@ + + + +Link +Link +Link +Link diff --git a/Tests/LibWeb/Ref/css-has-descendant.html b/Tests/LibWeb/Ref/css-has-descendant.html new file mode 100644 index 00000000000..ebce7534684 --- /dev/null +++ b/Tests/LibWeb/Ref/css-has-descendant.html @@ -0,0 +1,9 @@ + + + +Link +Link diff --git a/Tests/LibWeb/Ref/css-has-direct-child.html b/Tests/LibWeb/Ref/css-has-direct-child.html new file mode 100644 index 00000000000..a847f3ecbf0 --- /dev/null +++ b/Tests/LibWeb/Ref/css-has-direct-child.html @@ -0,0 +1,9 @@ + + + +Link +Link diff --git a/Tests/LibWeb/Ref/css-has-next-sibling.html b/Tests/LibWeb/Ref/css-has-next-sibling.html new file mode 100644 index 00000000000..9416d677dc3 --- /dev/null +++ b/Tests/LibWeb/Ref/css-has-next-sibling.html @@ -0,0 +1,11 @@ + + + +LinkHello! +LinkHello! +LinkHelloworld! +Link diff --git a/Tests/LibWeb/Ref/css-has-subsequent-sibling.html b/Tests/LibWeb/Ref/css-has-subsequent-sibling.html new file mode 100644 index 00000000000..dc87bf6d074 --- /dev/null +++ b/Tests/LibWeb/Ref/css-has-subsequent-sibling.html @@ -0,0 +1,19 @@ + + + +
+ LinkHello! +
+
+ LinkHello! +
+
+ LinkHelloworld! +
+
+ Link +
diff --git a/Tests/LibWeb/Ref/css-nested-has.html b/Tests/LibWeb/Ref/css-nested-has.html new file mode 100644 index 00000000000..3717c0fae10 --- /dev/null +++ b/Tests/LibWeb/Ref/css-nested-has.html @@ -0,0 +1,8 @@ + + + +Link diff --git a/Tests/LibWeb/Ref/css-pseudo-element-in-has.html b/Tests/LibWeb/Ref/css-pseudo-element-in-has.html new file mode 100644 index 00000000000..a97f2901dff --- /dev/null +++ b/Tests/LibWeb/Ref/css-pseudo-element-in-has.html @@ -0,0 +1,12 @@ + + + +foo diff --git a/Tests/LibWeb/Ref/reference/css-has-compound.html b/Tests/LibWeb/Ref/reference/css-has-compound.html new file mode 100644 index 00000000000..3cd0106af38 --- /dev/null +++ b/Tests/LibWeb/Ref/reference/css-has-compound.html @@ -0,0 +1,4 @@ +Link +Link +Link +Link diff --git a/Tests/LibWeb/Ref/reference/css-has-descendant.html b/Tests/LibWeb/Ref/reference/css-has-descendant.html new file mode 100644 index 00000000000..1a4ec55270c --- /dev/null +++ b/Tests/LibWeb/Ref/reference/css-has-descendant.html @@ -0,0 +1,2 @@ +Link +Link diff --git a/Tests/LibWeb/Ref/reference/css-has-direct-child.html b/Tests/LibWeb/Ref/reference/css-has-direct-child.html new file mode 100644 index 00000000000..1cf696d2f2c --- /dev/null +++ b/Tests/LibWeb/Ref/reference/css-has-direct-child.html @@ -0,0 +1,2 @@ +Link +Link diff --git a/Tests/LibWeb/Ref/reference/css-has-next-sibling.html b/Tests/LibWeb/Ref/reference/css-has-next-sibling.html new file mode 100644 index 00000000000..85ee3493a20 --- /dev/null +++ b/Tests/LibWeb/Ref/reference/css-has-next-sibling.html @@ -0,0 +1,4 @@ +LinkHello! +LinkHello! +LinkHelloworld! +Link diff --git a/Tests/LibWeb/Ref/reference/css-has-subsequent-sibling.html b/Tests/LibWeb/Ref/reference/css-has-subsequent-sibling.html new file mode 100644 index 00000000000..744d14a8115 --- /dev/null +++ b/Tests/LibWeb/Ref/reference/css-has-subsequent-sibling.html @@ -0,0 +1,12 @@ +
+ LinkHello! +
+
+ LinkHello! +
+
+ LinkHelloworld! +
+
+ Link +
diff --git a/Tests/LibWeb/Ref/reference/css-nested-has.html b/Tests/LibWeb/Ref/reference/css-nested-has.html new file mode 100644 index 00000000000..48f04cc05fa --- /dev/null +++ b/Tests/LibWeb/Ref/reference/css-nested-has.html @@ -0,0 +1 @@ +Link diff --git a/Tests/LibWeb/Ref/reference/css-pseudo-element-in-has.html b/Tests/LibWeb/Ref/reference/css-pseudo-element-in-has.html new file mode 100644 index 00000000000..7eee20c6ad3 --- /dev/null +++ b/Tests/LibWeb/Ref/reference/css-pseudo-element-in-has.html @@ -0,0 +1 @@ +foobar diff --git a/Userland/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp b/Userland/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp index 5fe7a1cbfec..3a52edb7bd5 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp +++ b/Userland/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp @@ -516,10 +516,14 @@ Parser::ParseErrorOr Parser::parse_pseudo_simple_selec .argument_selector_list = { move(selector) } } }; } + case PseudoClassMetadata::ParameterType::ForgivingRelativeSelectorList: case PseudoClassMetadata::ParameterType::ForgivingSelectorList: { auto function_token_stream = TokenStream(pseudo_function.values()); + auto selector_type = metadata.parameter_type == PseudoClassMetadata::ParameterType::ForgivingSelectorList + ? SelectorType::Standalone + : SelectorType::Relative; // NOTE: Because it's forgiving, even complete garbage will parse OK as an empty selector-list. - auto argument_selector_list = MUST(parse_a_selector_list(function_token_stream, SelectorType::Standalone, SelectorParsingMode::Forgiving)); + auto argument_selector_list = MUST(parse_a_selector_list(function_token_stream, selector_type, SelectorParsingMode::Forgiving)); return Selector::SimpleSelector { .type = Selector::SimpleSelector::Type::PseudoClass, diff --git a/Userland/Libraries/LibWeb/CSS/PseudoClasses.json b/Userland/Libraries/LibWeb/CSS/PseudoClasses.json index 22b54008756..6a199cc2b99 100644 --- a/Userland/Libraries/LibWeb/CSS/PseudoClasses.json +++ b/Userland/Libraries/LibWeb/CSS/PseudoClasses.json @@ -44,6 +44,9 @@ "focus-within": { "argument": "" }, + "has": { + "argument": "" + }, "host": { "argument": "?" }, diff --git a/Userland/Libraries/LibWeb/CSS/Selector.cpp b/Userland/Libraries/LibWeb/CSS/Selector.cpp index e55e98500da..54e5fa0df10 100644 --- a/Userland/Libraries/LibWeb/CSS/Selector.cpp +++ b/Userland/Libraries/LibWeb/CSS/Selector.cpp @@ -119,6 +119,7 @@ u32 Selector::specificity() const case SimpleSelector::Type::PseudoClass: { auto& pseudo_class = simple_selector.pseudo_class(); switch (pseudo_class.type) { + case PseudoClass::Has: case PseudoClass::Is: case PseudoClass::Not: { // The specificity of an :is(), :not(), or :has() pseudo-class is replaced by the diff --git a/Userland/Libraries/LibWeb/CSS/SelectorEngine.cpp b/Userland/Libraries/LibWeb/CSS/SelectorEngine.cpp index 535dd536692..5afed33922a 100644 --- a/Userland/Libraries/LibWeb/CSS/SelectorEngine.cpp +++ b/Userland/Libraries/LibWeb/CSS/SelectorEngine.cpp @@ -63,6 +63,56 @@ static inline bool matches_lang_pseudo_class(DOM::Element const& element, Vector return false; } +// https://drafts.csswg.org/selectors-4/#relational +static inline bool matches_has_pseudo_class(CSS::Selector const& selector, Optional style_sheet_for_rule, DOM::Element const& anchor) +{ + switch (selector.compound_selectors()[0].combinator) { + // Shouldn't be possible because we've parsed relative selectors, which always have a combinator, implicitly or explicitly. + case CSS::Selector::Combinator::None: + VERIFY_NOT_REACHED(); + case CSS::Selector::Combinator::Descendant: { + bool has = false; + anchor.for_each_in_subtree([&](auto const& descendant) { + if (!descendant.is_element()) + return TraversalDecision::Continue; + auto const& descendant_element = static_cast(descendant); + if (matches(selector, style_sheet_for_rule, descendant_element, {}, {}, SelectorKind::Relative)) { + has = true; + return TraversalDecision::Break; + } + return TraversalDecision::Continue; + }); + return has; + } + case CSS::Selector::Combinator::ImmediateChild: { + bool has = false; + anchor.for_each_child([&](DOM::Node const& child) { + if (!child.is_element()) + return IterationDecision::Continue; + auto const& child_element = static_cast(child); + if (matches(selector, style_sheet_for_rule, child_element, {}, {}, SelectorKind::Relative)) { + has = true; + return IterationDecision::Break; + } + return IterationDecision::Continue; + }); + return has; + } + case CSS::Selector::Combinator::NextSibling: + return anchor.next_element_sibling() != nullptr && matches(selector, style_sheet_for_rule, *anchor.next_element_sibling(), {}, {}, SelectorKind::Relative); + case CSS::Selector::Combinator::SubsequentSibling: { + for (auto* sibling = anchor.next_element_sibling(); sibling; sibling = sibling->next_element_sibling()) { + if (matches(selector, style_sheet_for_rule, *sibling, {}, {}, SelectorKind::Relative)) + return true; + } + return false; + } + case CSS::Selector::Combinator::Column: + TODO(); + } + return false; +} + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-link static inline bool matches_link_pseudo_class(DOM::Element const& element) { @@ -268,7 +318,7 @@ static bool matches_open_state_pseudo_class(DOM::Element const& element, bool op return false; } -static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoClassSelector const& pseudo_class, Optional style_sheet_for_rule, DOM::Element const& element, JS::GCPtr scope) +static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoClassSelector const& pseudo_class, Optional style_sheet_for_rule, DOM::Element const& element, JS::GCPtr scope, SelectorKind selector_kind) { switch (pseudo_class.type) { case CSS::PseudoClass::Link: @@ -359,6 +409,16 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla return matches_indeterminate_pseudo_class(element); case CSS::PseudoClass::Defined: return element.is_defined(); + case CSS::PseudoClass::Has: + // :has() cannot be nested in a :has() + if (selector_kind == SelectorKind::Relative) + return false; + // These selectors should be relative selectors (https://drafts.csswg.org/selectors-4/#relative-selector) + for (auto& selector : pseudo_class.argument_selector_list) { + if (matches_has_pseudo_class(selector, style_sheet_for_rule, element)) + return true; + } + return false; case CSS::PseudoClass::Is: case CSS::PseudoClass::Where: for (auto& selector : pseudo_class.argument_selector_list) { @@ -588,7 +648,7 @@ static ALWAYS_INLINE bool matches_namespace( VERIFY_NOT_REACHED(); } -static inline bool matches(CSS::Selector::SimpleSelector const& component, Optional style_sheet_for_rule, DOM::Element const& element, JS::GCPtr scope) +static inline bool matches(CSS::Selector::SimpleSelector const& component, Optional style_sheet_for_rule, DOM::Element const& element, JS::GCPtr scope, SelectorKind selector_kind) { switch (component.type) { case CSS::Selector::SimpleSelector::Type::Universal: @@ -615,7 +675,7 @@ static inline bool matches(CSS::Selector::SimpleSelector const& component, Optio case CSS::Selector::SimpleSelector::Type::Attribute: return matches_attribute(component.attribute(), style_sheet_for_rule, element); case CSS::Selector::SimpleSelector::Type::PseudoClass: - return matches_pseudo_class(component.pseudo_class(), style_sheet_for_rule, element, scope); + return matches_pseudo_class(component.pseudo_class(), style_sheet_for_rule, element, scope, selector_kind); case CSS::Selector::SimpleSelector::Type::PseudoElement: // Pseudo-element matching/not-matching is handled in the top level matches(). return true; @@ -624,22 +684,26 @@ static inline bool matches(CSS::Selector::SimpleSelector const& component, Optio } } -static inline bool matches(CSS::Selector const& selector, Optional style_sheet_for_rule, int component_list_index, DOM::Element const& element, JS::GCPtr scope) +static inline bool matches(CSS::Selector const& selector, Optional style_sheet_for_rule, int component_list_index, DOM::Element const& element, JS::GCPtr scope, SelectorKind selector_kind) { - auto& relative_selector = selector.compound_selectors()[component_list_index]; - for (auto& simple_selector : relative_selector.simple_selectors) { - if (!matches(simple_selector, style_sheet_for_rule, element, scope)) + auto& compound_selector = selector.compound_selectors()[component_list_index]; + for (auto& simple_selector : compound_selector.simple_selectors) { + if (!matches(simple_selector, style_sheet_for_rule, element, scope, selector_kind)) return false; } - switch (relative_selector.combinator) { + // Always matches because we assume that element is already relative to its anchor + if (selector_kind == SelectorKind::Relative && component_list_index == 0) + return true; + switch (compound_selector.combinator) { case CSS::Selector::Combinator::None: + VERIFY(selector_kind != SelectorKind::Relative); return true; case CSS::Selector::Combinator::Descendant: VERIFY(component_list_index != 0); for (auto* ancestor = element.parent(); ancestor; ancestor = ancestor->parent()) { if (!is(*ancestor)) continue; - if (matches(selector, style_sheet_for_rule, component_list_index - 1, static_cast(*ancestor), scope)) + if (matches(selector, style_sheet_for_rule, component_list_index - 1, static_cast(*ancestor), scope, selector_kind)) return true; } return false; @@ -647,16 +711,16 @@ static inline bool matches(CSS::Selector const& selector, Optional(*element.parent())) return false; - return matches(selector, style_sheet_for_rule, component_list_index - 1, static_cast(*element.parent()), scope); + return matches(selector, style_sheet_for_rule, component_list_index - 1, static_cast(*element.parent()), scope, selector_kind); case CSS::Selector::Combinator::NextSibling: VERIFY(component_list_index != 0); if (auto* sibling = element.previous_element_sibling()) - return matches(selector, style_sheet_for_rule, component_list_index - 1, *sibling, scope); + return matches(selector, style_sheet_for_rule, component_list_index - 1, *sibling, scope, selector_kind); return false; case CSS::Selector::Combinator::SubsequentSibling: VERIFY(component_list_index != 0); for (auto* sibling = element.previous_element_sibling(); sibling; sibling = sibling->previous_element_sibling()) { - if (matches(selector, style_sheet_for_rule, component_list_index - 1, *sibling, scope)) + if (matches(selector, style_sheet_for_rule, component_list_index - 1, *sibling, scope, selector_kind)) return true; } return false; @@ -666,14 +730,14 @@ static inline bool matches(CSS::Selector const& selector, Optional style_sheet_for_rule, DOM::Element const& element, Optional pseudo_element, JS::GCPtr scope) +bool matches(CSS::Selector const& selector, Optional style_sheet_for_rule, DOM::Element const& element, Optional pseudo_element, JS::GCPtr scope, SelectorKind selector_kind) { VERIFY(!selector.compound_selectors().is_empty()); if (pseudo_element.has_value() && selector.pseudo_element().has_value() && selector.pseudo_element().value().type() != pseudo_element) return false; if (!pseudo_element.has_value() && selector.pseudo_element().has_value()) return false; - return matches(selector, style_sheet_for_rule, selector.compound_selectors().size() - 1, element, scope); + return matches(selector, style_sheet_for_rule, selector.compound_selectors().size() - 1, element, scope, selector_kind); } static bool fast_matches_simple_selector(CSS::Selector::SimpleSelector const& simple_selector, Optional style_sheet_for_rule, DOM::Element const& element) @@ -696,7 +760,7 @@ static bool fast_matches_simple_selector(CSS::Selector::SimpleSelector const& si case CSS::Selector::SimpleSelector::Type::Attribute: return matches_attribute(simple_selector.attribute(), style_sheet_for_rule, element); case CSS::Selector::SimpleSelector::Type::PseudoClass: - return matches_pseudo_class(simple_selector.pseudo_class(), style_sheet_for_rule, element, nullptr); + return matches_pseudo_class(simple_selector.pseudo_class(), style_sheet_for_rule, element, nullptr, SelectorKind::Normal); default: VERIFY_NOT_REACHED(); } diff --git a/Userland/Libraries/LibWeb/CSS/SelectorEngine.h b/Userland/Libraries/LibWeb/CSS/SelectorEngine.h index 6b5c31ec44e..0235b4b2c60 100644 --- a/Userland/Libraries/LibWeb/CSS/SelectorEngine.h +++ b/Userland/Libraries/LibWeb/CSS/SelectorEngine.h @@ -11,7 +11,12 @@ namespace Web::SelectorEngine { -bool matches(CSS::Selector const&, Optional style_sheet_for_rule, DOM::Element const&, Optional = {}, JS::GCPtr scope = {}); +enum class SelectorKind { + Normal, + Relative, +}; + +bool matches(CSS::Selector const&, Optional style_sheet_for_rule, DOM::Element const&, Optional = {}, JS::GCPtr scope = {}, SelectorKind selector_kind = SelectorKind::Normal); [[nodiscard]] bool fast_matches(CSS::Selector const&, Optional style_sheet_for_rule, DOM::Element const&); [[nodiscard]] bool can_use_fast_matches(CSS::Selector const&); diff --git a/Userland/Libraries/LibWeb/Dump.cpp b/Userland/Libraries/LibWeb/Dump.cpp index 37499f5150c..e74c1c457d5 100644 --- a/Userland/Libraries/LibWeb/Dump.cpp +++ b/Userland/Libraries/LibWeb/Dump.cpp @@ -527,6 +527,7 @@ void dump_selector(StringBuilder& builder, CSS::Selector const& selector) } case CSS::PseudoClassMetadata::ParameterType::CompoundSelector: case CSS::PseudoClassMetadata::ParameterType::ForgivingSelectorList: + case CSS::PseudoClassMetadata::ParameterType::ForgivingRelativeSelectorList: case CSS::PseudoClassMetadata::ParameterType::SelectorList: { builder.append("(["sv); for (auto& selector : pseudo_class.argument_selector_list)