LibWeb: Implement the :has() pseudo-class

See https://drafts.csswg.org/selectors-4/#relational.
This commit is contained in:
Diego Frias 2024-07-13 09:19:30 -07:00 committed by Sam Atkins
parent be1075e673
commit f63a945ba0
Notes: sideshowbarker 2024-07-17 02:22:23 +09:00
21 changed files with 203 additions and 17 deletions

View file

@ -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 == "<forgiving-selector-list>"sv) {
parameter_type = "ForgivingSelectorList"_string;
} else if (argument_string == "<forgiving-relative-selector-list>"sv) {
parameter_type = "ForgivingRelativeSelectorList"_string;
} else if (argument_string == "<ident>"sv) {
parameter_type = "Ident"_string;
} else if (argument_string == "<language-ranges>"sv) {

View file

@ -0,0 +1,11 @@
<!doctype html>
<link rel="match" href="reference/css-has-compound.html" />
<style>
a:has(span.nice > em) {
color: orange;
}
</style>
<a href="https://example.com"><em><span class="nice"><em>Link</em></span></em></a>
<a href="https://example.com"><em>Link</em></a>
<a href="https://example.com"><em><span class="hello"><em>Link</em></span></em></a>
<a href="https://example.com"><em><span class="nice">Link</span></em></a>

View file

@ -0,0 +1,9 @@
<!doctype html>
<link rel="match" href="reference/css-has-descendant.html" />
<style>
a:has(span) {
color: orange;
}
</style>
<a href="https://example.com"><em><span>Link</span></em></a>
<a href="https://example.com"><em>Link</em></a>

View file

@ -0,0 +1,9 @@
<!doctype html>
<link rel="match" href="reference/css-has-direct-child.html" />
<style>
a:has(> span) {
color: orange;
}
</style>
<a href="https://example.com"><span>Link</span></a>
<a href="https://example.com"><em><span>Link</span></em></a>

View file

@ -0,0 +1,11 @@
<!doctype html>
<link rel="match" href="reference/css-has-next-sibling.html" />
<style>
a:has(+ span) {
color: orange;
}
</style>
<a href="https://example.com">Link</a><span>Hello!</span>
<a href="https://example.com">Link</a><em>Hello!</em>
<a href="https://example.com">Link</a><em>Hello</em><span>world!</span>
<a href="https://example.com">Link</a>

View file

@ -0,0 +1,19 @@
<!doctype html>
<link rel="match" href="reference/css-has-subsequent-sibling.html" />
<style>
a:has(~ span) {
color: orange;
}
</style>
<div>
<a href="https://example.com">Link</a><span>Hello!</span>
</div>
<div>
<a href="https://example.com">Link</a><em>Hello!</em>
</div>
<div>
<a href="https://example.com">Link</a><em>Hello</em><span>world!</span>
</div>
<div>
<a href="https://example.com">Link</a>
</div>

View file

@ -0,0 +1,8 @@
<!doctype html>
<link rel="match" href="reference/css-nested-has.html" />
<style>
a:has(span:has(strong)) {
color: orange;
}
</style>
<a href="https://example.com"><em><span><strong>Link</strong></span></em></a>

View file

@ -0,0 +1,12 @@
<!doctype html>
<link rel="match" href="reference/css-pseudo-element-in-has.html" />
<style>
span::after {
content: "bar";
color: red;
}
a:has(::after) {
color: orange;
}
</style>
<a href="https://example.com"><span>foo</span></a>

View file

@ -0,0 +1,4 @@
<a href="https://example.com" style="color: orange"><em><span class="nice"><em>Link</em></span></em></a>
<a href="https://example.com"><em>Link</em></a>
<a href="https://example.com"><em><span class="hello"><em>Link</em></span></em></a>
<a href="https://example.com"><em><span class="nice">Link</span></em></a>

View file

@ -0,0 +1,2 @@
<a href="https://example.com" style="color: orange"><em><span class="nice"><em>Link</em></span></em></a>
<a href="https://example.com"><em>Link</em></a>

View file

@ -0,0 +1,2 @@
<a href="https://example.com" style="color: orange"><span>Link</span></a>
<a href="https://example.com"><em><span>Link</span></em></a>

View file

@ -0,0 +1,4 @@
<a href="https://example.com" style="color: orange">Link</a><span>Hello!</span>
<a href="https://example.com">Link</a><em>Hello!</em>
<a href="https://example.com">Link</a><em>Hello</em><span>world!</span>
<a href="https://example.com">Link</a>

View file

@ -0,0 +1,12 @@
<div>
<a href="https://example.com" style="color: orange">Link</a><span>Hello!</span>
</div>
<div>
<a href="https://example.com">Link</a><em>Hello!</em>
</div>
<div>
<a href="https://example.com" style="color: orange">Link</a><em>Hello</em><span>world!</span>
</div>
<div>
<a href="https://example.com">Link</a>
</div>

View file

@ -0,0 +1 @@
<a href="https://example.com"><em><span><strong>Link</strong></span></em></a>

View file

@ -0,0 +1 @@
<a href="https://example.com"><span>foo<span style="color: red">bar</span></span></a>

View file

@ -516,10 +516,14 @@ Parser::ParseErrorOr<Selector::SimpleSelector> 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,

View file

@ -44,6 +44,9 @@
"focus-within": {
"argument": ""
},
"has": {
"argument": "<forgiving-relative-selector-list>"
},
"host": {
"argument": "<compound-selector>?"
},

View file

@ -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

View file

@ -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<CSS::CSSStyleSheet const&> 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<DOM::Element const&>(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<DOM::Element const&>(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<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element, JS::GCPtr<DOM::ParentNode const> scope)
static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoClassSelector const& pseudo_class, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element, JS::GCPtr<DOM::ParentNode const> 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<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element, JS::GCPtr<DOM::ParentNode const> scope)
static inline bool matches(CSS::Selector::SimpleSelector const& component, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element, JS::GCPtr<DOM::ParentNode const> 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<CSS::CSSStyleSheet const&> style_sheet_for_rule, int component_list_index, DOM::Element const& element, JS::GCPtr<DOM::ParentNode const> scope)
static inline bool matches(CSS::Selector const& selector, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, int component_list_index, DOM::Element const& element, JS::GCPtr<DOM::ParentNode const> 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<DOM::Element>(*ancestor))
continue;
if (matches(selector, style_sheet_for_rule, component_list_index - 1, static_cast<DOM::Element const&>(*ancestor), scope))
if (matches(selector, style_sheet_for_rule, component_list_index - 1, static_cast<DOM::Element const&>(*ancestor), scope, selector_kind))
return true;
}
return false;
@ -647,16 +711,16 @@ static inline bool matches(CSS::Selector const& selector, Optional<CSS::CSSStyle
VERIFY(component_list_index != 0);
if (!element.parent() || !is<DOM::Element>(*element.parent()))
return false;
return matches(selector, style_sheet_for_rule, component_list_index - 1, static_cast<DOM::Element const&>(*element.parent()), scope);
return matches(selector, style_sheet_for_rule, component_list_index - 1, static_cast<DOM::Element const&>(*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<CSS::CSSStyle
VERIFY_NOT_REACHED();
}
bool matches(CSS::Selector const& selector, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element, Optional<CSS::Selector::PseudoElement::Type> pseudo_element, JS::GCPtr<DOM::ParentNode const> scope)
bool matches(CSS::Selector const& selector, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element, Optional<CSS::Selector::PseudoElement::Type> pseudo_element, JS::GCPtr<DOM::ParentNode const> 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<CSS::CSSStyleSheet const&> 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();
}

View file

@ -11,7 +11,12 @@
namespace Web::SelectorEngine {
bool matches(CSS::Selector const&, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const&, Optional<CSS::Selector::PseudoElement::Type> = {}, JS::GCPtr<DOM::ParentNode const> scope = {});
enum class SelectorKind {
Normal,
Relative,
};
bool matches(CSS::Selector const&, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const&, Optional<CSS::Selector::PseudoElement::Type> = {}, JS::GCPtr<DOM::ParentNode const> scope = {}, SelectorKind selector_kind = SelectorKind::Normal);
[[nodiscard]] bool fast_matches(CSS::Selector const&, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const&);
[[nodiscard]] bool can_use_fast_matches(CSS::Selector const&);

View file

@ -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)