From 53f99e51f8382a20cec7752480d505957a2680b1 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Thu, 17 Oct 2024 12:26:37 +0100 Subject: [PATCH] LibWeb/CSS: Parse and use nested style rules For example, this: ```css .foo { color: red; &:hover { color: green; } } ``` now has the same effect as this: ```css .foo { color: red; } .foo:hover { color: green; } ``` CSSStyleRule now has "absolutized selectors", which are its selectors with any `&`s resolved. We use these instead of the "real" selectors when matching them, meaning the style computer doesn't have to know or care about where the selector appears in the CSS document. --- Tests/LibWeb/Ref/css-nesting.html | 90 ++++++++++++++ .../LibWeb/Ref/reference/css-nesting-ref.html | 18 +++ .../Libraries/LibWeb/CSS/CSSStyleRule.cpp | 68 +++++++++++ Userland/Libraries/LibWeb/CSS/CSSStyleRule.h | 3 + .../Libraries/LibWeb/CSS/Parser/Parser.cpp | 46 ++++++- Userland/Libraries/LibWeb/CSS/Selector.cpp | 112 ++++++++++++++++++ Userland/Libraries/LibWeb/CSS/Selector.h | 8 ++ .../Libraries/LibWeb/CSS/StyleComputer.cpp | 10 +- 8 files changed, 345 insertions(+), 10 deletions(-) create mode 100644 Tests/LibWeb/Ref/css-nesting.html create mode 100644 Tests/LibWeb/Ref/reference/css-nesting-ref.html 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,