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.
This commit is contained in:
Sam Atkins 2024-10-17 12:26:37 +01:00 committed by Andreas Kling
commit 53f99e51f8
Notes: github-actions[bot] 2024-10-17 18:57:13 +00:00
8 changed files with 345 additions and 10 deletions

View file

@ -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)
{
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<CSSStyleRule> 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 <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);
if (!declaration) {
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));
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)