diff --git a/Tests/LibWeb/Ref/css-nesting.html b/Tests/LibWeb/Ref/css-nesting.html
new file mode 100644
index 00000000000..628b999282f
--- /dev/null
+++ b/Tests/LibWeb/Ref/css-nesting.html
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/LibWeb/Ref/reference/css-nesting-ref.html b/Tests/LibWeb/Ref/reference/css-nesting-ref.html
new file mode 100644
index 00000000000..da38c508422
--- /dev/null
+++ b/Tests/LibWeb/Ref/reference/css-nesting-ref.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Userland/Libraries/LibWeb/CSS/CSSStyleRule.cpp b/Userland/Libraries/LibWeb/CSS/CSSStyleRule.cpp
index fc48410c9aa..78bdc9155f9 100644
--- a/Userland/Libraries/LibWeb/CSS/CSSStyleRule.cpp
+++ b/Userland/Libraries/LibWeb/CSS/CSSStyleRule.cpp
@@ -122,6 +122,8 @@ String CSSStyleRule::selector_text() const
// https://drafts.csswg.org/cssom-1/#dom-cssstylerule-selectortext
void CSSStyleRule::set_selector_text(StringView selector_text)
{
+ clear_caches();
+
// 1. Run the parse a group of selectors algorithm on the given value.
auto parsed_selectors = parse_selector(Parser::ParsingContext { realm() }, selector_text);
@@ -139,4 +141,70 @@ void CSSStyleRule::set_selector_text(StringView selector_text)
// 3. Otherwise, if the algorithm returns a null value, do nothing.
}
+SelectorList const& CSSStyleRule::absolutized_selectors() const
+{
+ if (m_cached_absolutized_selectors.has_value())
+ return m_cached_absolutized_selectors.value();
+
+ // Replace all occurrences of `&` with the nearest ancestor style rule's selector list wrapped in `:is(...)`,
+ // or if we have no such ancestor, with `:scope`.
+
+ // If we don't have any nesting selectors, we can just use our selectors as they are.
+ bool has_any_nesting = false;
+ for (auto const& selector : selectors()) {
+ if (selector->contains_the_nesting_selector()) {
+ has_any_nesting = true;
+ break;
+ }
+ }
+
+ if (!has_any_nesting) {
+ m_cached_absolutized_selectors = m_selectors;
+ return m_cached_absolutized_selectors.value();
+ }
+
+ // Otherwise, build up a new list of selectors with the `&` replaced.
+
+ // First, figure out what we should replace `&` with.
+ // "When used in the selector of a nested style rule, the nesting selector represents the elements matched by the parent rule.
+ // When used in any other context, it represents the same elements as :scope in that context (unless otherwise defined)."
+ // https://drafts.csswg.org/css-nesting-1/#nest-selector
+ CSSStyleRule const* parent_style_rule = nullptr;
+ for (auto* parent = parent_rule(); parent; parent = parent->parent_rule()) {
+ if (parent->type() == CSSStyleRule::Type::Style) {
+ parent_style_rule = static_cast(parent);
+ break;
+ }
+ }
+ Selector::SimpleSelector parent_selector;
+ if (parent_style_rule) {
+ // TODO: If there's only 1, we don't have to use `:is()` for it
+ parent_selector = {
+ .type = Selector::SimpleSelector::Type::PseudoClass,
+ .value = Selector::SimpleSelector::PseudoClassSelector {
+ .type = PseudoClass::Is,
+ .argument_selector_list = parent_style_rule->absolutized_selectors(),
+ },
+ };
+ } else {
+ parent_selector = {
+ .type = Selector::SimpleSelector::Type::PseudoClass,
+ .value = Selector::SimpleSelector::PseudoClassSelector { .type = PseudoClass::Scope },
+ };
+ }
+
+ SelectorList absolutized_selectors;
+ for (auto const& selector : selectors())
+ absolutized_selectors.append(selector->absolutized(parent_selector));
+
+ m_cached_absolutized_selectors = move(absolutized_selectors);
+ return m_cached_absolutized_selectors.value();
+}
+
+void CSSStyleRule::clear_caches()
+{
+ Base::clear_caches();
+ m_cached_absolutized_selectors.clear();
+}
+
}
diff --git a/Userland/Libraries/LibWeb/CSS/CSSStyleRule.h b/Userland/Libraries/LibWeb/CSS/CSSStyleRule.h
index 2c0244499c8..a60d84b47be 100644
--- a/Userland/Libraries/LibWeb/CSS/CSSStyleRule.h
+++ b/Userland/Libraries/LibWeb/CSS/CSSStyleRule.h
@@ -24,6 +24,7 @@ public:
virtual ~CSSStyleRule() override = default;
SelectorList const& selectors() const { return m_selectors; }
+ SelectorList const& absolutized_selectors() const;
PropertyOwningCSSStyleDeclaration const& declaration() const { return m_declaration; }
virtual Type type() const override { return Type::Style; }
@@ -40,9 +41,11 @@ private:
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
+ virtual void clear_caches() override;
virtual String serialized() const override;
SelectorList m_selectors;
+ mutable Optional m_cached_absolutized_selectors;
JS::NonnullGCPtr m_declaration;
};
diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp
index 9c192e408d4..eee1958cf92 100644
--- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp
+++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp
@@ -1374,11 +1374,12 @@ JS::GCPtr Parser::convert_to_rule(Rule const& rule, Nested nested)
JS::GCPtr Parser::convert_to_style_rule(QualifiedRule const& qualified_rule, Nested nested)
{
TokenStream prelude_stream { qualified_rule.prelude };
- auto selectors = parse_a_selector_list(prelude_stream,
+
+ auto maybe_selectors = parse_a_selector_list(prelude_stream,
nested == Nested::Yes ? SelectorType::Relative : SelectorType::Standalone);
- if (selectors.is_error()) {
- if (selectors.error() == ParseError::SyntaxError) {
+ if (maybe_selectors.is_error()) {
+ if (maybe_selectors.error() == ParseError::SyntaxError) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: style rule selectors invalid; discarding.");
if constexpr (CSS_PARSER_DEBUG) {
prelude_stream.dump_all_tokens();
@@ -1387,11 +1388,46 @@ JS::GCPtr Parser::convert_to_style_rule(QualifiedRule const& quali
return {};
}
- if (selectors.value().is_empty()) {
+ if (maybe_selectors.value().is_empty()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: empty selector; discarding.");
return {};
}
+ SelectorList selectors = maybe_selectors.release_value();
+ if (nested == Nested::Yes) {
+ // "Nested style rules differ from non-nested rules in the following ways:
+ // - A nested style rule accepts a as its prelude (rather than just a ).
+ // Any relative selectors are relative to the elements represented by the nesting selector.
+ // - If a selector in the does not start with a combinator but does contain the nesting
+ // selector, it is interpreted as a non-relative selector."
+ // https://drafts.csswg.org/css-nesting-1/#syntax
+ // NOTE: We already parsed the selectors as a
+
+ // Nested relative selectors get a `&` inserted at the beginning.
+ // This is, handily, how the spec wants them serialized:
+ // "When serializing a relative selector in a nested style rule, the selector must be absolutized,
+ // with the implied nesting selector inserted."
+ // - https://drafts.csswg.org/css-nesting-1/#cssom
+
+ SelectorList new_list;
+ new_list.ensure_capacity(selectors.size());
+ for (auto const& selector : selectors) {
+ auto first_combinator = selector->compound_selectors().first().combinator;
+ if (!first_is_one_of(first_combinator, Selector::Combinator::None, Selector::Combinator::Descendant)
+ || !selector->contains_the_nesting_selector()) {
+ new_list.append(selector->relative_to(Selector::SimpleSelector { .type = Selector::SimpleSelector::Type::Nesting }));
+ } else if (first_combinator == Selector::Combinator::Descendant) {
+ // Replace leading descendant combinator (whitespace) with none, because we're not actually relative.
+ auto copied_compound_selectors = selector->compound_selectors();
+ copied_compound_selectors.first().combinator = Selector::Combinator::None;
+ new_list.append(Selector::create(move(copied_compound_selectors)));
+ } else {
+ new_list.append(selector);
+ }
+ }
+ selectors = move(new_list);
+ }
+
auto* declaration = convert_to_style_declaration(qualified_rule.declarations);
if (!declaration) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: style rule declaration invalid; discarding.");
@@ -1423,7 +1459,7 @@ JS::GCPtr Parser::convert_to_style_rule(QualifiedRule const& quali
});
}
auto nested_rules = CSSRuleList::create(m_context.realm(), move(child_rules));
- return CSSStyleRule::create(m_context.realm(), move(selectors.value()), *declaration, *nested_rules);
+ return CSSStyleRule::create(m_context.realm(), move(selectors), *declaration, *nested_rules);
}
JS::GCPtr Parser::convert_to_import_rule(AtRule const& rule)
diff --git a/Userland/Libraries/LibWeb/CSS/Selector.cpp b/Userland/Libraries/LibWeb/CSS/Selector.cpp
index 7e5a8910b3b..e9ccd2c2dd8 100644
--- a/Userland/Libraries/LibWeb/CSS/Selector.cpp
+++ b/Userland/Libraries/LibWeb/CSS/Selector.cpp
@@ -6,6 +6,7 @@
*/
#include "Selector.h"
+#include
#include
namespace Web::CSS {
@@ -24,6 +25,30 @@ Selector::Selector(Vector&& compound_selectors)
}
}
+ // https://drafts.csswg.org/css-nesting-1/#contain-the-nesting-selector
+ // "A selector is said to contain the nesting selector if, when it was parsed as any type of selector,
+ // a with the value "&" (U+0026 AMPERSAND) was encountered."
+ for (auto const& compound_selector : m_compound_selectors) {
+ for (auto const& simple_selector : compound_selector.simple_selectors) {
+ if (simple_selector.type == SimpleSelector::Type::Nesting) {
+ m_contains_the_nesting_selector = true;
+ break;
+ }
+ if (simple_selector.type == SimpleSelector::Type::PseudoClass) {
+ for (auto const& child_selector : simple_selector.pseudo_class().argument_selector_list) {
+ if (child_selector->contains_the_nesting_selector()) {
+ m_contains_the_nesting_selector = true;
+ break;
+ }
+ }
+ if (m_contains_the_nesting_selector)
+ break;
+ }
+ }
+ if (m_contains_the_nesting_selector)
+ break;
+ }
+
collect_ancestor_hashes();
}
@@ -494,4 +519,91 @@ Optional Selector::PseudoElement::from_string(FlyString
return {};
}
+NonnullRefPtr Selector::relative_to(SimpleSelector const& parent) const
+{
+ // To make us relative to the parent, prepend it to the list of compound selectors,
+ // and ensure the next compound selector starts with a combinator.
+ Vector copied_compound_selectors;
+ copied_compound_selectors.ensure_capacity(compound_selectors().size() + 1);
+ copied_compound_selectors.empend(CompoundSelector { .simple_selectors = { parent } });
+
+ bool first = true;
+ for (auto compound_selector : compound_selectors()) {
+ if (first) {
+ if (compound_selector.combinator == Combinator::None)
+ compound_selector.combinator = Combinator::Descendant;
+ first = false;
+ }
+
+ copied_compound_selectors.append(move(compound_selector));
+ }
+
+ return Selector::create(move(copied_compound_selectors));
+}
+
+NonnullRefPtr Selector::absolutized(Selector::SimpleSelector const& selector_for_nesting) const
+{
+ if (!contains_the_nesting_selector())
+ return *this;
+
+ Vector absolutized_compound_selectors;
+ absolutized_compound_selectors.ensure_capacity(m_compound_selectors.size());
+ for (auto const& compound_selector : m_compound_selectors)
+ absolutized_compound_selectors.append(compound_selector.absolutized(selector_for_nesting));
+
+ return Selector::create(move(absolutized_compound_selectors));
+}
+
+Selector::CompoundSelector Selector::CompoundSelector::absolutized(Selector::SimpleSelector const& selector_for_nesting) const
+{
+ // TODO: Cache if it contains the nesting selector?
+
+ Vector absolutized_simple_selectors;
+ absolutized_simple_selectors.ensure_capacity(simple_selectors.size());
+ for (auto const& simple_selector : simple_selectors)
+ absolutized_simple_selectors.append(simple_selector.absolutized(selector_for_nesting));
+
+ return CompoundSelector {
+ .combinator = this->combinator,
+ .simple_selectors = absolutized_simple_selectors,
+ };
+}
+
+Selector::SimpleSelector Selector::SimpleSelector::absolutized(Selector::SimpleSelector const& selector_for_nesting) const
+{
+ switch (type) {
+ case Type::Nesting:
+ // Nesting selectors get replaced directly.
+ return selector_for_nesting;
+
+ case Type::PseudoClass: {
+ // Pseudo-classes may contain other selectors, so we need to absolutize them.
+ // Copy the PseudoClassSelector, and then replace its argument selector list.
+ auto pseudo_class = this->pseudo_class();
+ if (!pseudo_class.argument_selector_list.is_empty()) {
+ SelectorList new_selector_list;
+ new_selector_list.ensure_capacity(pseudo_class.argument_selector_list.size());
+ for (auto const& argument_selector : pseudo_class.argument_selector_list)
+ new_selector_list.append(argument_selector->absolutized(selector_for_nesting));
+ pseudo_class.argument_selector_list = move(new_selector_list);
+ }
+ return SimpleSelector {
+ .type = Type::PseudoClass,
+ .value = move(pseudo_class),
+ };
+ }
+
+ case Type::Universal:
+ case Type::TagName:
+ case Type::Id:
+ case Type::Class:
+ case Type::Attribute:
+ case Type::PseudoElement:
+ // Everything else isn't affected
+ return *this;
+ }
+
+ VERIFY_NOT_REACHED();
+}
+
}
diff --git a/Userland/Libraries/LibWeb/CSS/Selector.h b/Userland/Libraries/LibWeb/CSS/Selector.h
index fc40b045d0b..6a5736a6e5e 100644
--- a/Userland/Libraries/LibWeb/CSS/Selector.h
+++ b/Userland/Libraries/LibWeb/CSS/Selector.h
@@ -213,6 +213,8 @@ public:
QualifiedName& qualified_name() { return value.get(); }
String serialize() const;
+
+ SimpleSelector absolutized(SimpleSelector const& selector_for_nesting) const;
};
enum class Combinator {
@@ -229,6 +231,8 @@ public:
// but it is more understandable to put them together.
Combinator combinator { Combinator::None };
Vector simple_selectors;
+
+ CompoundSelector absolutized(SimpleSelector const& selector_for_nesting) const;
};
static NonnullRefPtr create(Vector&& compound_selectors)
@@ -240,6 +244,9 @@ public:
Vector const& compound_selectors() const { return m_compound_selectors; }
Optional pseudo_element() const { return m_pseudo_element; }
+ NonnullRefPtr relative_to(SimpleSelector const&) const;
+ bool contains_the_nesting_selector() const { return m_contains_the_nesting_selector; }
+ NonnullRefPtr absolutized(SimpleSelector const& selector_for_nesting) const;
u32 specificity() const;
String serialize() const;
@@ -251,6 +258,7 @@ private:
Vector m_compound_selectors;
mutable Optional m_specificity;
Optional m_pseudo_element;
+ bool m_contains_the_nesting_selector { false };
void collect_ancestor_hashes();
diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp
index 85ec4990bf4..b94d7e85691 100644
--- a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp
+++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp
@@ -434,7 +434,7 @@ Vector StyleComputer::collect_matching_rules(DOM::Element const& e
continue;
}
- auto const& selector = rule_to_run.rule->selectors()[rule_to_run.selector_index];
+ auto const& selector = rule_to_run.rule->absolutized_selectors()[rule_to_run.selector_index];
if (should_reject_with_ancestor_filter(*selector)) {
rule_to_run.skip = true;
continue;
@@ -461,7 +461,7 @@ Vector StyleComputer::collect_matching_rules(DOM::Element const& e
if (element.is_shadow_host() && rule_root != element.shadow_root())
shadow_host_to_use = nullptr;
- auto const& selector = rule_to_run.rule->selectors()[rule_to_run.selector_index];
+ auto const& selector = rule_to_run.rule->absolutized_selectors()[rule_to_run.selector_index];
if (rule_to_run.can_use_fast_matches) {
if (!SelectorEngine::fast_matches(selector, *rule_to_run.sheet, element, shadow_host_to_use))
@@ -478,8 +478,8 @@ Vector StyleComputer::collect_matching_rules(DOM::Element const& e
static void sort_matching_rules(Vector& matching_rules)
{
quick_sort(matching_rules, [&](MatchingRule& a, MatchingRule& b) {
- auto const& a_selector = a.rule->selectors()[a.selector_index];
- auto const& b_selector = b.rule->selectors()[b.selector_index];
+ auto const& a_selector = a.rule->absolutized_selectors()[a.selector_index];
+ auto const& b_selector = b.rule->absolutized_selectors()[b.selector_index];
auto a_specificity = a_selector->specificity();
auto b_specificity = b_selector->specificity();
if (a_specificity == b_specificity) {
@@ -2418,7 +2418,7 @@ NonnullOwnPtr StyleComputer::make_rule_cache_for_casca
size_t rule_index = 0;
sheet.for_each_effective_style_rule([&](auto const& rule) {
size_t selector_index = 0;
- for (CSS::Selector const& selector : rule.selectors()) {
+ for (CSS::Selector const& selector : rule.absolutized_selectors()) {
MatchingRule matching_rule {
shadow_root,
&rule,