Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LibWeb: Fix some CSS nesting bugs #2240

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ Rerun

Found 11 tests

11 Fail
1 Pass
10 Fail
Details
Result Test Name MessageFail Trailing declarations apply after any preceding rules
Fail Trailing declarations apply after any preceding rules (no leading)
Expand All @@ -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 @scope rules behave like :where(:scope)
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ Rerun

Found 1 tests

1 Fail
1 Pass
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ Rerun

Found 32 tests

31 Pass
1 Fail
32 Pass
Details
Result Test Name MessagePass .foo { & { color: green; }}
Pass .foo { &.bar { color: green; }}
Expand All @@ -26,7 +25,7 @@ Pass .foo { &:is(.bar, .baz) { color: green; }}
Pass .foo { :is(.bar, &.baz) { color: green; }}
Pass .foo { &:is(.bar, &.baz) { color: green; }}
Pass .foo { div& { color: green; }}
Fail INVALID: .foo { &div { color: green; }}
Pass INVALID: .foo { &div { color: green; }}
Pass .foo { .class& { color: green; }}
Pass .foo { &.class { color: green; }}
Pass .foo { [attr]& { color: green; }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ Rerun

Found 1 tests

1 Fail
1 Pass
Details
Result Test Name MessageFail CSS Nesting: Specificity of top-level '&'
Result Test Name MessagePass CSS Nesting: Specificity of top-level '&'
47 changes: 27 additions & 20 deletions Userland/Libraries/LibWeb/CSS/CSSStyleRule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,18 @@ 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);
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.
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();
if (auto* sheet = parent_style_sheet()) {
if (auto style_sheet_list = sheet->style_sheet_list()) {
Expand Down Expand Up @@ -169,35 +177,25 @@ 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 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) {
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
parent_selector = {
Selector::SimpleSelector parent_selector = {
.type = Selector::SimpleSelector::Type::PseudoClass,
.value = Selector::SimpleSelector::PseudoClassSelector {
.type = PseudoClass::Is,
.argument_selector_list = parent_style_rule->absolutized_selectors(),
},
};
SelectorList absolutized_selectors;
for (auto const& selector : selectors())
absolutized_selectors.append(selector->absolutized(parent_selector));
m_cached_absolutized_selectors = move(absolutized_selectors);
} else {
parent_selector = {
.type = Selector::SimpleSelector::Type::PseudoClass,
.value = Selector::SimpleSelector::PseudoClassSelector { .type = PseudoClass::Scope },
};
// NOTE: We can't actually replace & with :scope, because & has to have 0 specificity.
// So we leave it, and treat & like :scope during matching.
m_cached_absolutized_selectors = m_selectors;
}

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();
}

Expand All @@ -207,4 +205,13 @@ void CSSStyleRule::clear_caches()
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;
}

}
2 changes: 2 additions & 0 deletions Userland/Libraries/LibWeb/CSS/CSSStyleRule.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class CSSStyleRule final : public CSSGroupingRule {
virtual void clear_caches() override;
virtual String serialized() const override;

CSSStyleRule const* parent_style_rule() const;

SelectorList m_selectors;
mutable Optional<SelectorList> m_cached_absolutized_selectors;
JS::NonnullGCPtr<PropertyOwningCSSStyleDeclaration> m_declaration;
Expand Down
11 changes: 11 additions & 0 deletions Userland/Libraries/LibWeb/CSS/Parser/Helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

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)
{
return CSS::Parser::Parser::create(context, selector_text).parse_as_pseudo_element_selector();
Expand Down
1 change: 1 addition & 0 deletions Userland/Libraries/LibWeb/CSS/Parser/Parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -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&);
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_for_nested_style_rule(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);
RefPtr<CSS::MediaQuery> parse_media_query(CSS::Parser::ParsingContext const&, StringView);
Expand Down
35 changes: 2 additions & 33 deletions Userland/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,39 +98,8 @@ JS::GCPtr<CSSStyleRule> Parser::convert_to_style_rule(QualifiedRule const& quali
}

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);
}
if (nested == Nested::Yes)
selectors = adapt_nested_relative_selector_list(selectors);

auto* declaration = convert_to_style_declaration(qualified_rule.declarations);
if (!declaration) {
Expand Down
4 changes: 4 additions & 0 deletions Userland/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ Parser::ParseErrorOr<Optional<Selector::CompoundSelector>> Parser::parse_compoun
auto component = TRY(parse_simple_selector(tokens));
if (!component.has_value())
break;
if (component->type == Selector::SimpleSelector::Type::TagName && !simple_selectors.is_empty()) {
// Tag-name selectors can only go at the beginning of a compound selector.
return ParseError::SyntaxError;
}
simple_selectors.append(component.release_value());
}

Expand Down
42 changes: 40 additions & 2 deletions Userland/Libraries/LibWeb/CSS/Selector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,11 @@ u32 Selector::specificity() const
// ignore the universal selector
break;
case SimpleSelector::Type::Nesting:
// We should have replaced this already
VERIFY_NOT_REACHED();
// "The specificity of the nesting selector is equal to the largest specificity among the complex selectors in the parent style rule’s selector list (identical to the behavior of :is()), or zero if no such selector list exists."
// - https://drafts.csswg.org/css-nesting/#ref-for-specificity
// The parented case is handled by replacing & with :is().
// So if we got here, the specificity is 0.
break;
}
}
}
Expand Down Expand Up @@ -606,4 +609,39 @@ Selector::SimpleSelector Selector::SimpleSelector::absolutized(Selector::SimpleS
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;
}

}
2 changes: 2 additions & 0 deletions Userland/Libraries/LibWeb/CSS/Selector.h
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ class Selector : public RefCounted<Selector> {

String serialize_a_group_of_selectors(SelectorList const& selectors);

SelectorList adapt_nested_relative_selector_list(SelectorList const&);

}

namespace AK {
Expand Down
6 changes: 4 additions & 2 deletions Userland/Libraries/LibWeb/CSS/SelectorEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -760,8 +760,10 @@ static inline bool matches(CSS::Selector::SimpleSelector const& component, Optio
// Pseudo-element matching/not-matching is handled in the top level matches().
return true;
case CSS::Selector::SimpleSelector::Type::Nesting:
// We should only try to match selectors that have been absolutized!
VERIFY_NOT_REACHED();
// Nesting either behaves like :is(), or like :scope.
// :is() is handled already, by us replacing it with :is() directly, so if we
// got here, it's :scope.
return matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoClassSelector { .type = CSS::PseudoClass::Scope }, style_sheet_for_rule, element, shadow_host, scope, selector_kind);
}
VERIFY_NOT_REACHED();
}
Expand Down
Loading