Skip to content

Commit

Permalink
LibWeb: Implement the :has() pseudo-class
Browse files Browse the repository at this point in the history
  • Loading branch information
dzfrias committed Jul 13, 2024
1 parent 13a8c2a commit 0d60977
Show file tree
Hide file tree
Showing 16 changed files with 182 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ struct PseudoClassMetadata {
ANPlusBOf,
CompoundSelector,
ForgivingSelectorList,
ForgivingRelativeSelectorList,
Ident,
LanguageRanges,
SelectorList,
Expand Down Expand Up @@ -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) {
Expand Down
11 changes: 11 additions & 0 deletions Tests/LibWeb/Ref/css-has-compound.html
Original file line number Diff line number Diff line change
@@ -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>
9 changes: 9 additions & 0 deletions Tests/LibWeb/Ref/css-has-descendant.html
Original file line number Diff line number Diff line change
@@ -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>
9 changes: 9 additions & 0 deletions Tests/LibWeb/Ref/css-has-direct-child.html
Original file line number Diff line number Diff line change
@@ -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>
11 changes: 11 additions & 0 deletions Tests/LibWeb/Ref/css-has-next-sibling.html
Original file line number Diff line number Diff line change
@@ -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>
19 changes: 19 additions & 0 deletions Tests/LibWeb/Ref/css-has-subsequent-sibling.html
Original file line number Diff line number Diff line change
@@ -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>
4 changes: 4 additions & 0 deletions Tests/LibWeb/Ref/reference/css-has-compound.html
Original file line number Diff line number Diff line change
@@ -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>
2 changes: 2 additions & 0 deletions Tests/LibWeb/Ref/reference/css-has-descendant.html
Original file line number Diff line number Diff line change
@@ -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>
2 changes: 2 additions & 0 deletions Tests/LibWeb/Ref/reference/css-has-direct-child.html
Original file line number Diff line number Diff line change
@@ -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>
4 changes: 4 additions & 0 deletions Tests/LibWeb/Ref/reference/css-has-next-sibling.html
Original file line number Diff line number Diff line change
@@ -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>
12 changes: 12 additions & 0 deletions Tests/LibWeb/Ref/reference/css-has-subsequent-sibling.html
Original file line number Diff line number Diff line change
@@ -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>
4 changes: 3 additions & 1 deletion Userland/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -516,10 +516,12 @@ 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,
Expand Down
3 changes: 3 additions & 0 deletions Userland/Libraries/LibWeb/CSS/PseudoClasses.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
"focus-within": {
"argument": ""
},
"has": {
"argument": "<forgiving-relative-selector-list>"
},
"host": {
"argument": "<compound-selector>?"
},
Expand Down
1 change: 1 addition & 0 deletions Userland/Libraries/LibWeb/CSS/Selector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
88 changes: 88 additions & 0 deletions Userland/Libraries/LibWeb/CSS/SelectorEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,57 @@ 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& element)
{
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;
element.for_each_in_subtree([&](auto const& descendant) {
if (!descendant.is_element())
return TraversalDecision::Continue;
auto const& descendant_element = static_cast<DOM::Element const&>(descendant);
// Provide the element as our "scope" (which is referred to as the "anchor element" in the spec: https://drafts.csswg.org/selectors-4/#relative-selector)
if (matches(selector, style_sheet_for_rule, descendant_element, {}, element)) {
has = true;
return TraversalDecision::Break;
}
return TraversalDecision::Continue;
});
return has;
}
case CSS::Selector::Combinator::ImmediateChild: {
bool has = false;
element.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, {}, element)) {
has = true;
return IterationDecision::Break;
}
return IterationDecision::Continue;
});
return has;
}
case CSS::Selector::Combinator::NextSibling:
return element.next_element_sibling() != nullptr && matches(selector, style_sheet_for_rule, *element.next_element_sibling(), {}, element);
case CSS::Selector::Combinator::SubsequentSibling: {
for (auto* sibling = element.next_element_sibling(); sibling; sibling = sibling->next_element_sibling()) {
if (matches(selector, style_sheet_for_rule, *sibling, {}, element))
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)
{
Expand Down Expand Up @@ -359,6 +410,13 @@ 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:
// 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) {
Expand Down Expand Up @@ -635,6 +693,13 @@ static inline bool matches(CSS::Selector const& selector, Optional<CSS::CSSStyle
case CSS::Selector::Combinator::None:
return true;
case CSS::Selector::Combinator::Descendant:
if (component_list_index == 0 && scope) {
for (auto* ancestor = element.parent(); ancestor; ancestor = ancestor->parent()) {
if (ancestor == scope)
return true;
}
return false;
}
VERIFY(component_list_index != 0);
for (auto* ancestor = element.parent(); ancestor; ancestor = ancestor->parent()) {
if (!is<DOM::Element>(*ancestor))
Expand All @@ -644,16 +709,39 @@ static inline bool matches(CSS::Selector const& selector, Optional<CSS::CSSStyle
}
return false;
case CSS::Selector::Combinator::ImmediateChild:
if (component_list_index == 0 && scope) {
bool found = false;
scope->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 (&child_element == &element) {
found = true;
return IterationDecision::Break;
}
return IterationDecision::Continue;
});
return found;
}
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);
case CSS::Selector::Combinator::NextSibling:
if (component_list_index == 0 && scope)
return element.previous_element_sibling() == scope;
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 false;
case CSS::Selector::Combinator::SubsequentSibling:
if (component_list_index == 0 && scope) {
for (auto* sibling = element.previous_element_sibling(); sibling; sibling = sibling->previous_element_sibling()) {
if (sibling == scope)
return true;
}
return false;
}
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))
Expand Down
1 change: 1 addition & 0 deletions Userland/Libraries/LibWeb/Dump.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 0d60977

Please sign in to comment.