Commit a8a4bfec authored by Anders Hartvoll Ruud's avatar Anders Hartvoll Ruud Committed by Commit Bot

[:is/:where] Support :is/:where inside :not

This adds support for using :is inside :not, without expanding the
capabilities of :not itself. (In other words, we still only accept
a single simple selector when using non-nesting selectors directly
within :not).

For invalidation, we do need to traverse into the complex selectors
nested (deep) inside :not(), for example to mark .b as invalidates-
self for ".a :not(:is(.b))", but at the :not-boundary during feature
extraction, we avoid propagating the features, such that .a is marked
with whole-subtree recalc. We already have similar logic in
ExtractInvalidationSetFeaturesFromCompound which prevents feature
extraction for selectors held by :not directly.

Note that CSSParserSelector::IsSimple() only has one call-site, and
that's during parsing of :not().

Bug: 568705
Change-Id: Ic7cb421c4d8f6a6e85cfea30c987db2e0fed64fa
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2463830
Commit-Queue: Anders Hartvoll Ruud <andruud@chromium.org>
Reviewed-by: default avatarRune Lillesveen <futhark@chromium.org>
Cr-Commit-Position: refs/heads/master@{#816328}
parent ebfa88e0
...@@ -59,8 +59,17 @@ void CSSParserSelector::SetSelectorList( ...@@ -59,8 +59,17 @@ void CSSParserSelector::SetSelectorList(
} }
bool CSSParserSelector::IsSimple() const { bool CSSParserSelector::IsSimple() const {
if (selector_->SelectorList() || if (selector_->SelectorList()) {
selector_->Match() == CSSSelector::kPseudoElement) switch (selector_->GetPseudoType()) {
case CSSSelector::kPseudoIs:
case CSSSelector::kPseudoWhere:
break;
default:
return false;
}
}
if (selector_->Match() == CSSSelector::kPseudoElement)
return false; return false;
if (!tag_history_) if (!tag_history_)
......
...@@ -511,6 +511,10 @@ static const SelectorTestCase is_where_nesting_data[] = { ...@@ -511,6 +511,10 @@ static const SelectorTestCase is_where_nesting_data[] = {
{":is(.a :is(.b .c), .d)"}, {":is(.a :is(.b .c), .d)"},
{":is(.a :where(.b .c), .d)"}, {":is(.a :where(.b .c), .d)"},
{":where(.a :is(.b .c), .d)"}, {":where(.a :is(.b .c), .d)"},
{":not(:is(.a))"},
{":not(:is(.a, .b))"},
{":not(:is(.a + .b, .c .d))"},
{":not(:where(:not(.a)))"},
{"::slotted(:is(.a))"}, {"::slotted(:is(.a))"},
{"::slotted(:is(div.a))"}, {"::slotted(:is(div.a))"},
{"::slotted(:is(.a, .b))"}, {"::slotted(:is(.a, .b))"},
......
...@@ -720,7 +720,8 @@ void RuleFeatureSet::UpdateRuleSetInvalidation( ...@@ -720,7 +720,8 @@ void RuleFeatureSet::UpdateRuleSetInvalidation(
void RuleFeatureSet::ExtractInvalidationSetFeaturesFromSelectorList( void RuleFeatureSet::ExtractInvalidationSetFeaturesFromSelectorList(
const CSSSelector& simple_selector, const CSSSelector& simple_selector,
InvalidationSetFeatures& features, InvalidationSetFeatures& features,
PositionType position) { PositionType position,
CSSSelector::PseudoType pseudo_type) {
AutoRestoreMaxDirectAdjacentSelectors restore_max(&features); AutoRestoreMaxDirectAdjacentSelectors restore_max(&features);
const CSSSelectorList* selector_list = simple_selector.SelectorList(); const CSSSelectorList* selector_list = simple_selector.SelectorList();
...@@ -755,10 +756,15 @@ void RuleFeatureSet::ExtractInvalidationSetFeaturesFromSelectorList( ...@@ -755,10 +756,15 @@ void RuleFeatureSet::ExtractInvalidationSetFeaturesFromSelectorList(
} }
// Don't add any features if one of the sub-selectors of does not contain // Don't add any features if one of the sub-selectors of does not contain
// any invalidation set features. E.g. :-webkit-any(*, span). // any invalidation set features. E.g. :-webkit-any(*, span).
//
// :not() counts as not having features, since we should invalidate elements
// _without_ those features.
if (pseudo_type != CSSSelector::kPseudoNot) {
if (all_sub_selectors_have_features) if (all_sub_selectors_have_features)
features.NarrowToFeatures(any_features); features.NarrowToFeatures(any_features);
features.has_features_for_rule_set_invalidation |= features.has_features_for_rule_set_invalidation |=
all_sub_selectors_have_features_for_ruleset_invalidation; all_sub_selectors_have_features_for_ruleset_invalidation;
}
} }
const CSSSelector* RuleFeatureSet::ExtractInvalidationSetFeaturesFromCompound( const CSSSelector* RuleFeatureSet::ExtractInvalidationSetFeaturesFromCompound(
...@@ -805,7 +811,7 @@ const CSSSelector* RuleFeatureSet::ExtractInvalidationSetFeaturesFromCompound( ...@@ -805,7 +811,7 @@ const CSSSelector* RuleFeatureSet::ExtractInvalidationSetFeaturesFromCompound(
} }
ExtractInvalidationSetFeaturesFromSelectorList(*simple_selector, features, ExtractInvalidationSetFeaturesFromSelectorList(*simple_selector, features,
position); position, pseudo);
if (features.invalidation_flags.InvalidatesParts()) if (features.invalidation_flags.InvalidatesParts())
metadata_.invalidates_parts = true; metadata_.invalidates_parts = true;
......
...@@ -403,7 +403,8 @@ class CORE_EXPORT RuleFeatureSet { ...@@ -403,7 +403,8 @@ class CORE_EXPORT RuleFeatureSet {
CSSSelector::PseudoType = CSSSelector::kPseudoUnknown); CSSSelector::PseudoType = CSSSelector::kPseudoUnknown);
void ExtractInvalidationSetFeaturesFromSelectorList(const CSSSelector&, void ExtractInvalidationSetFeaturesFromSelectorList(const CSSSelector&,
InvalidationSetFeatures&, InvalidationSetFeatures&,
PositionType); PositionType,
CSSSelector::PseudoType);
void UpdateFeaturesFromCombinator( void UpdateFeaturesFromCombinator(
const CSSSelector&, const CSSSelector&,
const CSSSelector* last_compound_selector_in_adjacent_chain, const CSSSelector* last_compound_selector_in_adjacent_chain,
......
...@@ -1566,6 +1566,22 @@ RefTestData ref_equal_test_data[] = { ...@@ -1566,6 +1566,22 @@ RefTestData ref_equal_test_data[] = {
{":is(.a, *) .b", ".a .b, * .b"}, {":is(.a, *) .b", ".a .b, * .b"},
{":is(.a + .b, .c) *", ".a + .b *, .c *"}, {":is(.a + .b, .c) *", ".a + .b *, .c *"},
{":is(.a + *, .c) *", ".a + * *, .c *"}, {":is(.a + *, .c) *", ".a + * *, .c *"},
// TODO(andruud): At the time of writing these :not() tests, we only
// support a single simple selector inside :not(). When a complex selector
// list is supported, some refs should be rewritten to be less strange.
{":not(:is(.a))", ":not(.a)"},
{":not(:is(.a, .b))", ":not(.a), :not(.b)"},
{":not(:is(.a .b))", ":not(.b), .a .b"},
{":not(:is(.a .b, .c + .d))", ":not(.b), :not(.d), .a .b, .c + .d"},
{".a :not(:is(.b .c))", ".a :not(.c), .b .c"},
{":not(:is(.a)) .b", ":not(.a) .b"},
{":not(:is(.a .b, .c)) :not(:is(.d + .e, .f))",
":not(.a):not(.b) :not(.f), :not(.a):not(.b) :not(.e),"
":not(.c) :not(.f), .d + .e"},
// We don't have any special support for nested :not(): it's treated
// as a single :not() level in terms of invalidation:
{".a :not(:is(:not(.b), .c))", ".a :not(.b), .a :not(.c)"},
{":not(:is(:not(.a), .b)) .c", ":not(.a) .c, :not(.b) .c"},
// clang-format on // clang-format on
}; };
......
<!DOCTYPE html>
<title>:is() inside :not()</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<link rel="help" href="https://drafts.csswg.org/selectors-4/#matches">
<link rel="help" href="https://drafts.csswg.org/selectors/#negation">
<main id=main>
<div id=a><div id=d></div></div>
<div id=b><div id=e></div></div>
<div id=c><div id=f></div></div>
</main>
<script>
function formatElements(elements) {
return elements.map(e => e.id).sort().join();
}
// Test that |selector| returns the given elements in #main.
function test_selector(selector, expected) {
test(function() {
let actual = Array.from(main.querySelectorAll(selector));
assert_equals(formatElements(actual), formatElements(expected));
}, `${selector} matches expected elements`);
}
test_selector(':not(:is(#a))', [b, c, d, e, f]);
test_selector(':not(:where(#b))', [a, c, d, e, f]);
test_selector(':not(:where(:root #c))', [a, b, d, e, f]);
test_selector(':not(:is(#a, #b))', [c, d, e, f]);
test_selector(':not(:is(#b div))', [a, b, c, d, f]);
test_selector(':not(:is(#a div, div + div))', [a, e, f]);
test_selector(':not(:is(span))', [a, b, c, d, e, f]);
test_selector(':not(:is(div))', []);
test_selector(':not(:is(*|div))', []);
test_selector(':not(:is(*|*))', []);
test_selector(':not(:is(*))', []);
test_selector(':not(:is(svg|div))', [a, b, c, d, e, f]);
test_selector(':not(:is(:not(div)))', [a, b, c, d, e, f]);
test_selector(':not(:is(span, b, i))', [a, b, c, d, e, f]);
test_selector(':not(:is(span, b, i, div))', []);
test_selector(':not(:is(#b ~ div div, * + #c))', [a, b, d, e]);
test_selector(':not(:is(div > :not(#e)))', [a, b, c, e]);
test_selector(':not(:is(div > :not(:where(#e, #f))))', [a, b, c, e, f]);
</script>
...@@ -42,4 +42,7 @@ ...@@ -42,4 +42,7 @@
assert_valid(true, "{}(div) + bar", "Combinators after"); assert_valid(true, "{}(div) + bar", "Combinators after");
assert_valid(true, "::part(foo):is(:hover)", "After part with simple pseudo-class"); assert_valid(true, "::part(foo):is(:hover)", "After part with simple pseudo-class");
assert_valid(false, "::part(foo):is([attr='value'])", "After part with invalid selector after"); assert_valid(false, "::part(foo):is([attr='value'])", "After part with invalid selector after");
assert_valid(true, ":not({}(div))", "Nested inside :not, without combinators");
assert_valid(true, ":not({}(div .foo))", "Nested inside :not, with combinators");
</script> </script>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment