فهرست منبع

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.
Sam Atkins 9 ماه پیش
والد
کامیت
53f99e51f8

+ 90 - 0
Tests/LibWeb/Ref/css-nesting.html

@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<link rel="match" href="reference/css-nesting-ref.html" />
+<!-- Adapted from http://wpt.live/css/css-nesting/nesting-basic.html -->
+<style>
+    .test {
+        background-color: red;
+        width: 30px;
+        height: 30px;
+        margin-bottom: 10px;
+        display: grid;
+    }
+
+    .test-1 {
+        & > div {
+            background-color: green;
+        }
+    }
+
+    .test-2 {
+        & > div {
+            background-color: green;
+        }
+    }
+
+    .test-3 {
+        & .test-3-child {
+            background-color: green;
+        }
+    }
+
+    span > b {
+        .test-4 section & {
+            display: inline-block;
+            background-color: green;
+            width: 100%;
+            height: 100%;
+        }
+
+        .test-4 section > & {
+            background-color: red;
+        }
+    }
+
+    .test-6 {
+        &.test {
+            background-color: green;
+        }
+    }
+
+    .test-7, .t7- {
+        & + .test-7-child, &.t7-- {
+            background-color: green;
+        }
+    }
+
+    .test-8 {
+        & {
+            background-color: green;
+        }
+    }
+
+    .test-9 {
+        &:is(.t9-, &.t9--) {
+            background-color: green;
+        }
+    }
+
+    /* & at top level counts as :scope, i.e. the root element here */
+    & .test-12 {
+        background-color: green;
+    }
+    & > .test-12 {
+        background-color: red !important;
+    }
+</style>
+<body>
+<div class="test test-1"><div></div></div>
+<div class="test test-2"><div></div></div>
+<div class="test test-3"><div class="test-3-child"></div></div>
+<div class="test test-4">
+    <section>
+        <span><b></b></span>
+    </section>
+</div>
+<div class="test test-6"><div></div></div>
+<div class="test t7- t7--"><div class="test-7-child"></div></div>
+<div class="test test-8"><div></div></div>
+<div class="test test-9 t9-- t9-"><div></div></div>
+<div class="test test-12"></div>
+</body>

+ 18 - 0
Tests/LibWeb/Ref/reference/css-nesting-ref.html

@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<style>
+    .test {
+        background-color: green;
+        width: 30px;
+        height: 30px;
+        margin-bottom: 10px;
+    }
+</style>
+<div class="test"></div>
+<div class="test"></div>
+<div class="test"></div>
+<div class="test"></div>
+<div class="test"></div>
+<div class="test"></div>
+<div class="test"></div>
+<div class="test"></div>
+<div class="test"></div>

+ 68 - 0
Userland/Libraries/LibWeb/CSS/CSSStyleRule.cpp

@@ -122,6 +122,8 @@ String CSSStyleRule::selector_text() const
 // https://drafts.csswg.org/cssom-1/#dom-cssstylerule-selectortext
 // https://drafts.csswg.org/cssom-1/#dom-cssstylerule-selectortext
 void CSSStyleRule::set_selector_text(StringView selector_text)
 void CSSStyleRule::set_selector_text(StringView selector_text)
 {
 {
+    clear_caches();
+
     // 1. Run the parse a group of selectors algorithm on the given value.
     // 1. Run the parse a group of selectors algorithm on the given value.
     auto parsed_selectors = parse_selector(Parser::ParsingContext { realm() }, selector_text);
     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.
     // 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<CSSStyleRule const*>(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();
+}
+
 }
 }

+ 3 - 0
Userland/Libraries/LibWeb/CSS/CSSStyleRule.h

@@ -24,6 +24,7 @@ public:
     virtual ~CSSStyleRule() override = default;
     virtual ~CSSStyleRule() override = default;
 
 
     SelectorList const& selectors() const { return m_selectors; }
     SelectorList const& selectors() const { return m_selectors; }
+    SelectorList const& absolutized_selectors() const;
     PropertyOwningCSSStyleDeclaration const& declaration() const { return m_declaration; }
     PropertyOwningCSSStyleDeclaration const& declaration() const { return m_declaration; }
 
 
     virtual Type type() const override { return Type::Style; }
     virtual Type type() const override { return Type::Style; }
@@ -40,9 +41,11 @@ private:
 
 
     virtual void initialize(JS::Realm&) override;
     virtual void initialize(JS::Realm&) override;
     virtual void visit_edges(Cell::Visitor&) override;
     virtual void visit_edges(Cell::Visitor&) override;
+    virtual void clear_caches() override;
     virtual String serialized() const override;
     virtual String serialized() const override;
 
 
     SelectorList m_selectors;
     SelectorList m_selectors;
+    mutable Optional<SelectorList> m_cached_absolutized_selectors;
     JS::NonnullGCPtr<PropertyOwningCSSStyleDeclaration> m_declaration;
     JS::NonnullGCPtr<PropertyOwningCSSStyleDeclaration> m_declaration;
 };
 };
 
 

+ 41 - 5
Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp

@@ -1374,11 +1374,12 @@ JS::GCPtr<CSSRule> Parser::convert_to_rule(Rule const& rule, Nested nested)
 JS::GCPtr<CSSStyleRule> Parser::convert_to_style_rule(QualifiedRule const& qualified_rule, Nested nested)
 JS::GCPtr<CSSStyleRule> Parser::convert_to_style_rule(QualifiedRule const& qualified_rule, Nested nested)
 {
 {
     TokenStream prelude_stream { qualified_rule.prelude };
     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);
         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.");
             dbgln_if(CSS_PARSER_DEBUG, "CSSParser: style rule selectors invalid; discarding.");
             if constexpr (CSS_PARSER_DEBUG) {
             if constexpr (CSS_PARSER_DEBUG) {
                 prelude_stream.dump_all_tokens();
                 prelude_stream.dump_all_tokens();
@@ -1387,11 +1388,46 @@ JS::GCPtr<CSSStyleRule> Parser::convert_to_style_rule(QualifiedRule const& quali
         return {};
         return {};
     }
     }
 
 
-    if (selectors.value().is_empty()) {
+    if (maybe_selectors.value().is_empty()) {
         dbgln_if(CSS_PARSER_DEBUG, "CSSParser: empty selector; discarding.");
         dbgln_if(CSS_PARSER_DEBUG, "CSSParser: empty selector; discarding.");
         return {};
         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 <relative-selector-list> as its prelude (rather than just a <selector-list>).
+        //   Any relative selectors are relative to the elements represented by the nesting selector.
+        // - If a selector in the <relative-selector-list> 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 <relative-selector-list>
+
+        // 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);
     auto* declaration = convert_to_style_declaration(qualified_rule.declarations);
     if (!declaration) {
     if (!declaration) {
         dbgln_if(CSS_PARSER_DEBUG, "CSSParser: style rule declaration invalid; discarding.");
         dbgln_if(CSS_PARSER_DEBUG, "CSSParser: style rule declaration invalid; discarding.");
@@ -1423,7 +1459,7 @@ JS::GCPtr<CSSStyleRule> Parser::convert_to_style_rule(QualifiedRule const& quali
             });
             });
     }
     }
     auto nested_rules = CSSRuleList::create(m_context.realm(), move(child_rules));
     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<CSSImportRule> Parser::convert_to_import_rule(AtRule const& rule)
 JS::GCPtr<CSSImportRule> Parser::convert_to_import_rule(AtRule const& rule)

+ 112 - 0
Userland/Libraries/LibWeb/CSS/Selector.cpp

@@ -6,6 +6,7 @@
  */
  */
 
 
 #include "Selector.h"
 #include "Selector.h"
+#include <AK/GenericShorthands.h>
 #include <LibWeb/CSS/Serialize.h>
 #include <LibWeb/CSS/Serialize.h>
 
 
 namespace Web::CSS {
 namespace Web::CSS {
@@ -24,6 +25,30 @@ Selector::Selector(Vector<CompoundSelector>&& 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 <delim-token> 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();
     collect_ancestor_hashes();
 }
 }
 
 
@@ -494,4 +519,91 @@ Optional<Selector::PseudoElement> Selector::PseudoElement::from_string(FlyString
     return {};
     return {};
 }
 }
 
 
+NonnullRefPtr<Selector> 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<CompoundSelector> 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> Selector::absolutized(Selector::SimpleSelector const& selector_for_nesting) const
+{
+    if (!contains_the_nesting_selector())
+        return *this;
+
+    Vector<CompoundSelector> 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<SimpleSelector> 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();
+}
+
 }
 }

+ 8 - 0
Userland/Libraries/LibWeb/CSS/Selector.h

@@ -213,6 +213,8 @@ public:
         QualifiedName& qualified_name() { return value.get<QualifiedName>(); }
         QualifiedName& qualified_name() { return value.get<QualifiedName>(); }
 
 
         String serialize() const;
         String serialize() const;
+
+        SimpleSelector absolutized(SimpleSelector const& selector_for_nesting) const;
     };
     };
 
 
     enum class Combinator {
     enum class Combinator {
@@ -229,6 +231,8 @@ public:
         // but it is more understandable to put them together.
         // but it is more understandable to put them together.
         Combinator combinator { Combinator::None };
         Combinator combinator { Combinator::None };
         Vector<SimpleSelector> simple_selectors;
         Vector<SimpleSelector> simple_selectors;
+
+        CompoundSelector absolutized(SimpleSelector const& selector_for_nesting) const;
     };
     };
 
 
     static NonnullRefPtr<Selector> create(Vector<CompoundSelector>&& compound_selectors)
     static NonnullRefPtr<Selector> create(Vector<CompoundSelector>&& compound_selectors)
@@ -240,6 +244,9 @@ public:
 
 
     Vector<CompoundSelector> const& compound_selectors() const { return m_compound_selectors; }
     Vector<CompoundSelector> const& compound_selectors() const { return m_compound_selectors; }
     Optional<PseudoElement> pseudo_element() const { return m_pseudo_element; }
     Optional<PseudoElement> pseudo_element() const { return m_pseudo_element; }
+    NonnullRefPtr<Selector> relative_to(SimpleSelector const&) const;
+    bool contains_the_nesting_selector() const { return m_contains_the_nesting_selector; }
+    NonnullRefPtr<Selector> absolutized(SimpleSelector const& selector_for_nesting) const;
     u32 specificity() const;
     u32 specificity() const;
     String serialize() const;
     String serialize() const;
 
 
@@ -251,6 +258,7 @@ private:
     Vector<CompoundSelector> m_compound_selectors;
     Vector<CompoundSelector> m_compound_selectors;
     mutable Optional<u32> m_specificity;
     mutable Optional<u32> m_specificity;
     Optional<Selector::PseudoElement> m_pseudo_element;
     Optional<Selector::PseudoElement> m_pseudo_element;
+    bool m_contains_the_nesting_selector { false };
 
 
     void collect_ancestor_hashes();
     void collect_ancestor_hashes();
 
 

+ 5 - 5
Userland/Libraries/LibWeb/CSS/StyleComputer.cpp

@@ -434,7 +434,7 @@ Vector<MatchingRule> StyleComputer::collect_matching_rules(DOM::Element const& e
             continue;
             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)) {
         if (should_reject_with_ancestor_filter(*selector)) {
             rule_to_run.skip = true;
             rule_to_run.skip = true;
             continue;
             continue;
@@ -461,7 +461,7 @@ Vector<MatchingRule> StyleComputer::collect_matching_rules(DOM::Element const& e
         if (element.is_shadow_host() && rule_root != element.shadow_root())
         if (element.is_shadow_host() && rule_root != element.shadow_root())
             shadow_host_to_use = nullptr;
             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 (rule_to_run.can_use_fast_matches) {
             if (!SelectorEngine::fast_matches(selector, *rule_to_run.sheet, element, shadow_host_to_use))
             if (!SelectorEngine::fast_matches(selector, *rule_to_run.sheet, element, shadow_host_to_use))
@@ -478,8 +478,8 @@ Vector<MatchingRule> StyleComputer::collect_matching_rules(DOM::Element const& e
 static void sort_matching_rules(Vector<MatchingRule>& matching_rules)
 static void sort_matching_rules(Vector<MatchingRule>& matching_rules)
 {
 {
     quick_sort(matching_rules, [&](MatchingRule& a, MatchingRule& b) {
     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 a_specificity = a_selector->specificity();
         auto b_specificity = b_selector->specificity();
         auto b_specificity = b_selector->specificity();
         if (a_specificity == b_specificity) {
         if (a_specificity == b_specificity) {
@@ -2418,7 +2418,7 @@ NonnullOwnPtr<StyleComputer::RuleCache> StyleComputer::make_rule_cache_for_casca
         size_t rule_index = 0;
         size_t rule_index = 0;
         sheet.for_each_effective_style_rule([&](auto const& rule) {
         sheet.for_each_effective_style_rule([&](auto const& rule) {
             size_t selector_index = 0;
             size_t selector_index = 0;
-            for (CSS::Selector const& selector : rule.selectors()) {
+            for (CSS::Selector const& selector : rule.absolutized_selectors()) {
                 MatchingRule matching_rule {
                 MatchingRule matching_rule {
                     shadow_root,
                     shadow_root,
                     &rule,
                     &rule,