LibWeb: Add & when setting nested style rule's selector text

When we first parse a nested CSSStyleRule's selectors, we treat them as
relative selectors and then patch them up with an `&` as needed.
However, we weren't doing this when assigning the `cssText` attribute.
So, let's do that!

This gives us a couple of subtest passes. :^)
This commit is contained in:
Sam Atkins 2024-11-08 17:50:38 +00:00 committed by Andreas Kling
commit a0403ac427
Notes: github-actions[bot] 2024-11-09 13:30:38 +00:00
9 changed files with 77 additions and 46 deletions

View file

@ -6,7 +6,8 @@ Rerun
Found 11 tests Found 11 tests
11 Fail 1 Pass
10 Fail
Details Details
Result Test Name MessageFail Trailing declarations apply after any preceding rules Result Test Name MessageFail Trailing declarations apply after any preceding rules
Fail Trailing declarations apply after any preceding rules (no leading) Fail Trailing declarations apply after any preceding rules (no leading)
@ -18,4 +19,4 @@ Fail Bare declartaion in nested grouping rule can match pseudo-element
Fail Nested group rules have top-level specificity behavior Fail Nested group rules have top-level specificity behavior
Fail Nested @scope rules behave like :where(:scope) Fail Nested @scope rules behave like :where(:scope)
Fail Nested @scope rules behave like :where(:scope) (trailing) Fail Nested @scope rules behave like :where(:scope) (trailing)
Fail Nested declarations rule responds to parent selector text change Pass Nested declarations rule responds to parent selector text change

View file

@ -6,6 +6,6 @@ Rerun
Found 1 tests Found 1 tests
1 Fail 1 Pass
Details Details
Result Test Name MessageFail Nested rule responds to parent selector text change Result Test Name MessagePass Nested rule responds to parent selector text change

View file

@ -125,10 +125,18 @@ void CSSStyleRule::set_selector_text(StringView selector_text)
clear_caches(); 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); Optional<SelectorList> parsed_selectors;
if (parent_style_rule()) {
// AD-HOC: If we're a nested style rule, then we need to parse the selector as relative and then adapt it with implicit &s.
parsed_selectors = parse_selector_for_nested_style_rule(Parser::ParsingContext { realm() }, selector_text);
} else {
parsed_selectors = parse_selector(Parser::ParsingContext { realm() }, selector_text);
}
// 2. If the algorithm returns a non-null value replace the associated group of selectors with the returned value. // 2. If the algorithm returns a non-null value replace the associated group of selectors with the returned value.
if (parsed_selectors.has_value()) { if (parsed_selectors.has_value()) {
// NOTE: If we have a parent style rule, we need to update the selectors to add any implicit `&`s
m_selectors = parsed_selectors.release_value(); m_selectors = parsed_selectors.release_value();
if (auto* sheet = parent_style_sheet()) { if (auto* sheet = parent_style_sheet()) {
if (auto style_sheet_list = sheet->style_sheet_list()) { if (auto style_sheet_list = sheet->style_sheet_list()) {
@ -169,15 +177,8 @@ SelectorList const& CSSStyleRule::absolutized_selectors() const
// "When used in the selector of a nested style rule, the nesting selector represents the elements matched by the parent rule. // "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)." // 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 // 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; Selector::SimpleSelector parent_selector;
if (parent_style_rule) { if (auto const* parent_style_rule = this->parent_style_rule()) {
// TODO: If there's only 1, we don't have to use `:is()` for it // TODO: If there's only 1, we don't have to use `:is()` for it
parent_selector = { parent_selector = {
.type = Selector::SimpleSelector::Type::PseudoClass, .type = Selector::SimpleSelector::Type::PseudoClass,
@ -207,4 +208,13 @@ void CSSStyleRule::clear_caches()
m_cached_absolutized_selectors.clear(); m_cached_absolutized_selectors.clear();
} }
CSSStyleRule const* CSSStyleRule::parent_style_rule() const
{
for (auto* parent = parent_rule(); parent; parent = parent->parent_rule()) {
if (parent->type() == CSSStyleRule::Type::Style)
return static_cast<CSSStyleRule const*>(parent);
}
return nullptr;
}
} }

View file

@ -42,6 +42,8 @@ private:
virtual void clear_caches() override; virtual void clear_caches() override;
virtual String serialized() const override; virtual String serialized() const override;
CSSStyleRule const* parent_style_rule() const;
SelectorList m_selectors; SelectorList m_selectors;
mutable Optional<SelectorList> m_cached_absolutized_selectors; mutable Optional<SelectorList> m_cached_absolutized_selectors;
JS::NonnullGCPtr<PropertyOwningCSSStyleDeclaration> m_declaration; JS::NonnullGCPtr<PropertyOwningCSSStyleDeclaration> m_declaration;

View file

@ -54,6 +54,17 @@ Optional<CSS::SelectorList> parse_selector(CSS::Parser::ParsingContext const& co
return CSS::Parser::Parser::create(context, selector_text).parse_as_selector(); return CSS::Parser::Parser::create(context, selector_text).parse_as_selector();
} }
Optional<CSS::SelectorList> parse_selector_for_nested_style_rule(CSS::Parser::ParsingContext const& context, StringView selector_text)
{
auto parser = CSS::Parser::Parser::create(context, selector_text);
auto maybe_selectors = parser.parse_as_relative_selector(CSS::Parser::Parser::SelectorParsingMode::Standard);
if (!maybe_selectors.has_value())
return {};
return adapt_nested_relative_selector_list(*maybe_selectors);
}
Optional<CSS::Selector::PseudoElement> parse_pseudo_element_selector(CSS::Parser::ParsingContext const& context, StringView selector_text) Optional<CSS::Selector::PseudoElement> parse_pseudo_element_selector(CSS::Parser::ParsingContext const& context, StringView selector_text)
{ {
return CSS::Parser::Parser::create(context, selector_text).parse_as_pseudo_element_selector(); return CSS::Parser::Parser::create(context, selector_text).parse_as_pseudo_element_selector();

View file

@ -423,6 +423,7 @@ CSS::CSSStyleSheet* parse_css_stylesheet(CSS::Parser::ParsingContext const&, Str
CSS::ElementInlineCSSStyleDeclaration* parse_css_style_attribute(CSS::Parser::ParsingContext const&, StringView, DOM::Element&); CSS::ElementInlineCSSStyleDeclaration* parse_css_style_attribute(CSS::Parser::ParsingContext const&, StringView, DOM::Element&);
RefPtr<CSS::CSSStyleValue> parse_css_value(CSS::Parser::ParsingContext const&, StringView, CSS::PropertyID property_id = CSS::PropertyID::Invalid); RefPtr<CSS::CSSStyleValue> parse_css_value(CSS::Parser::ParsingContext const&, StringView, CSS::PropertyID property_id = CSS::PropertyID::Invalid);
Optional<CSS::SelectorList> parse_selector(CSS::Parser::ParsingContext const&, StringView); Optional<CSS::SelectorList> parse_selector(CSS::Parser::ParsingContext const&, StringView);
Optional<CSS::SelectorList> parse_selector_for_nested_style_rule(CSS::Parser::ParsingContext const&, StringView);
Optional<CSS::Selector::PseudoElement> parse_pseudo_element_selector(CSS::Parser::ParsingContext const&, StringView); Optional<CSS::Selector::PseudoElement> parse_pseudo_element_selector(CSS::Parser::ParsingContext const&, StringView);
CSS::CSSRule* parse_css_rule(CSS::Parser::ParsingContext const&, StringView); CSS::CSSRule* parse_css_rule(CSS::Parser::ParsingContext const&, StringView);
RefPtr<CSS::MediaQuery> parse_media_query(CSS::Parser::ParsingContext const&, StringView); RefPtr<CSS::MediaQuery> parse_media_query(CSS::Parser::ParsingContext const&, StringView);

View file

@ -98,39 +98,8 @@ JS::GCPtr<CSSStyleRule> Parser::convert_to_style_rule(QualifiedRule const& quali
} }
SelectorList selectors = maybe_selectors.release_value(); SelectorList selectors = maybe_selectors.release_value();
if (nested == Nested::Yes) { if (nested == Nested::Yes)
// "Nested style rules differ from non-nested rules in the following ways: selectors = adapt_nested_relative_selector_list(selectors);
// - 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) {

View file

@ -606,4 +606,39 @@ Selector::SimpleSelector Selector::SimpleSelector::absolutized(Selector::SimpleS
VERIFY_NOT_REACHED(); VERIFY_NOT_REACHED();
} }
SelectorList adapt_nested_relative_selector_list(SelectorList const& selectors)
{
// "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
CSS::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, CSS::Selector::Combinator::None, CSS::Selector::Combinator::Descendant)
|| !selector->contains_the_nesting_selector()) {
new_list.append(selector->relative_to(CSS::Selector::SimpleSelector { .type = CSS::Selector::SimpleSelector::Type::Nesting }));
} else if (first_combinator == CSS::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 = CSS::Selector::Combinator::None;
new_list.append(CSS::Selector::create(move(copied_compound_selectors)));
} else {
new_list.append(selector);
}
}
return new_list;
}
} }

View file

@ -267,6 +267,8 @@ private:
String serialize_a_group_of_selectors(SelectorList const& selectors); String serialize_a_group_of_selectors(SelectorList const& selectors);
SelectorList adapt_nested_relative_selector_list(SelectorList const&);
} }
namespace AK { namespace AK {