Bug 1452640: Implement the functional :host(..) selector. r?xidorn draft
authorEmilio Cobos Álvarez <emilio@crisal.io>
Mon, 09 Apr 2018 23:23:17 +0200
changeset 779430 8db97b7a27ae160af70dcbf038b29b801d0c0367
parent 779206 b48cc23827b1110da4a8fa1726c7190bd4d05500
push id105768
push userbmo:emilio@crisal.io
push dateMon, 09 Apr 2018 21:25:04 +0000
reviewersxidorn
bugs1452640
milestone61.0a1
Bug 1452640: Implement the functional :host(..) selector. r?xidorn We could invalidate in a slightly more fine-grained way, but I don't think it's worth the churn vs. keeping the special-cases minimal. MozReview-Commit-ID: 5DkQrgwg9GW
servo/components/selectors/builder.rs
servo/components/selectors/matching.rs
servo/components/selectors/parser.rs
servo/components/style/dom.rs
servo/components/style/invalidation/element/invalidation_map.rs
servo/components/style/invalidation/element/invalidator.rs
servo/components/style/selector_map.rs
servo/components/style/stylist.rs
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/css/css-scoping/host-functional-descendant-invalidation.html
--- a/servo/components/selectors/builder.rs
+++ b/servo/components/selectors/builder.rs
@@ -309,17 +309,17 @@ fn complex_selector_specificity<Impl>(mu
             Component::Class(..) |
             Component::AttributeInNoNamespace { .. } |
             Component::AttributeInNoNamespaceExists { .. } |
             Component::AttributeOther(..) |
 
             Component::FirstChild | Component::LastChild |
             Component::OnlyChild | Component::Root |
             Component::Empty | Component::Scope |
-            Component::Host |
+            Component::Host(..) |
             Component::NthChild(..) |
             Component::NthLastChild(..) |
             Component::NthOfType(..) |
             Component::NthLastOfType(..) |
             Component::FirstOfType | Component::LastOfType |
             Component::OnlyOfType |
             Component::NonTSPseudoClass(..) => {
                 specificity.class_like_selectors += 1
--- a/servo/components/selectors/matching.rs
+++ b/servo/components/selectors/matching.rs
@@ -687,20 +687,20 @@ where
     E: Element,
     F: FnMut(&E, ElementSelectorFlags),
 {
     debug_assert!(context.shared.is_nested() || !context.shared.in_negation());
 
     match *selector {
         Component::Combinator(_) => unreachable!(),
         Component::Slotted(ref selector) => {
+            // <slots> are never flattened tree slottables.
+            !element.is_html_slot_element() &&
+            element.assigned_slot().is_some() &&
             context.shared.nest(|context| {
-                // <slots> are never flattened tree slottables.
-                !element.is_html_slot_element() &&
-                element.assigned_slot().is_some() &&
                 matches_complex_selector(
                     selector.iter(),
                     element,
                     context,
                     flags_setter,
                 )
             })
         }
@@ -809,18 +809,28 @@ where
         }
         Component::Root => {
             element.is_root()
         }
         Component::Empty => {
             flags_setter(element, ElementSelectorFlags::HAS_EMPTY_SELECTOR);
             element.is_empty()
         }
-        Component::Host => {
-            context.shared.shadow_host().map_or(false, |host| host == element.opaque())
+        Component::Host(ref selector) => {
+            context.shared.shadow_host().map_or(false, |host| host == element.opaque()) &&
+            selector.as_ref().map_or(true, |selector| {
+                context.shared.nest(|context| {
+                    matches_complex_selector(
+                        selector.iter(),
+                        element,
+                        context,
+                        flags_setter,
+                    )
+                })
+            })
         }
         Component::Scope => {
             match context.shared.scope_element {
                 Some(ref scope_element) => element.opaque() == *scope_element,
                 None => element.is_root(),
             }
         }
         Component::NthChild(a, b) => {
--- a/servo/components/selectors/parser.rs
+++ b/servo/components/selectors/parser.rs
@@ -376,21 +376,24 @@ where
         V: SelectorVisitor<Impl = Impl>,
     {
         use self::Component::*;
         if !visitor.visit_simple_selector(self) {
             return false;
         }
 
         match *self {
-            Slotted(ref selectors) => {
-                for selector in selectors.iter() {
-                    if !selector.visit(visitor) {
-                        return false;
-                    }
+            Slotted(ref selector) => {
+                if !selector.visit(visitor) {
+                    return false;
+                }
+            }
+            Host(Some(ref selector)) => {
+                if !selector.visit(visitor) {
+                    return false;
                 }
             }
             Negation(ref negated) => {
                 for component in negated.iter() {
                     if !component.visit(visitor) {
                         return false;
                     }
                 }
@@ -613,17 +616,17 @@ impl<'a, Impl: 'a + SelectorImpl> Select
     pub fn next_sequence(&mut self) -> Option<Combinator> {
         self.next_combinator.take()
     }
 
     /// Whether this selector is a featureless host selector, with no
     /// combinators to the left.
     #[inline]
     pub(crate) fn is_featureless_host_selector(&mut self) -> bool {
-        self.all(|component| matches!(*component, Component::Host)) &&
+        self.all(|component| matches!(*component, Component::Host(..))) &&
         self.next_sequence().is_none()
     }
 
     /// Returns remaining count of the simple selectors and combinators in the Selector.
     #[inline]
     pub fn selector_length(&self) -> usize {
         self.iter.len()
     }
@@ -788,20 +791,16 @@ pub enum Component<Impl: SelectorImpl> {
     /// need to think about how this should interact with
     /// visit_complex_selector, and what the consumers of those APIs should do
     /// about the presence of combinators in negation.
     Negation(Box<[Component<Impl>]>),
     FirstChild, LastChild, OnlyChild,
     Root,
     Empty,
     Scope,
-    /// The `:host` pseudo-class:
-    ///
-    /// https://drafts.csswg.org/css-scoping/#host-selector
-    Host,
     NthChild(i32, i32),
     NthLastChild(i32, i32),
     NthOfType(i32, i32),
     NthLastOfType(i32, i32),
     FirstOfType,
     LastOfType,
     OnlyOfType,
     NonTSPseudoClass(Impl::NonTSPseudoClass),
@@ -810,17 +809,29 @@ pub enum Component<Impl: SelectorImpl> {
     ///
     /// https://drafts.csswg.org/css-scoping/#slotted-pseudo
     ///
     /// The selector here is a compound selector, that is, no combinators.
     ///
     /// NOTE(emilio): This should support a list of selectors, but as of this
     /// writing no other browser does, and that allows them to put ::slotted()
     /// in the rule hash, so we do that too.
+    ///
+    /// See https://github.com/w3c/csswg-drafts/issues/2158
     Slotted(Selector<Impl>),
+    /// The `:host` pseudo-class:
+    ///
+    /// https://drafts.csswg.org/css-scoping/#host-selector
+    ///
+    /// NOTE(emilio): This should support a list of selectors, but as of this
+    /// writing no other browser does, and that allows them to put ::slotted()
+    /// in the rule hash, so we do that too.
+    ///
+    /// See https://github.com/w3c/csswg-drafts/issues/2158
+    Host(Option<Selector<Impl>>),
     PseudoElement(Impl::PseudoElement),
 }
 
 impl<Impl: SelectorImpl> Component<Impl> {
     /// Compute the ancestor hash to check against the bloom filter.
     fn ancestor_hash(&self, quirks_mode: QuirksMode) -> Option<u32>
         where Impl::Identifier: PrecomputedHash,
               Impl::ClassName: PrecomputedHash,
@@ -1114,17 +1125,25 @@ impl<Impl: SelectorImpl> ToCss for Compo
             }
 
             FirstChild => dest.write_str(":first-child"),
             LastChild => dest.write_str(":last-child"),
             OnlyChild => dest.write_str(":only-child"),
             Root => dest.write_str(":root"),
             Empty => dest.write_str(":empty"),
             Scope => dest.write_str(":scope"),
-            Host => dest.write_str(":host"),
+            Host(ref selector) => {
+                dest.write_str(":host")?;
+                if let Some(ref selector) = *selector {
+                    dest.write_char('(')?;
+					selector.to_css(dest)?;
+                    dest.write_char(')')?;
+                }
+                Ok(())
+            },
             FirstOfType => dest.write_str(":first-of-type"),
             LastOfType => dest.write_str(":last-of-type"),
             OnlyOfType => dest.write_str(":only-of-type"),
             NthChild(a, b) | NthLastChild(a, b) | NthOfType(a, b) | NthLastOfType(a, b) => {
                 match *self {
                     NthChild(_, _) => dest.write_str(":nth-child(")?,
                     NthLastChild(_, _) => dest.write_str(":nth-last-child(")?,
                     NthOfType(_, _) => dest.write_str(":nth-of-type(")?,
@@ -1811,16 +1830,17 @@ where
     P: Parser<'i, Impl=Impl>,
     Impl: SelectorImpl,
 {
     match_ignore_ascii_case! { &name,
         "nth-child" => return Ok(parse_nth_pseudo_class(input, Component::NthChild)?),
         "nth-of-type" => return Ok(parse_nth_pseudo_class(input, Component::NthOfType)?),
         "nth-last-child" => return Ok(parse_nth_pseudo_class(input, Component::NthLastChild)?),
         "nth-last-of-type" => return Ok(parse_nth_pseudo_class(input, Component::NthLastOfType)?),
+        "host" => return Ok(Component::Host(Some(parse_inner_compound_selector(parser, input)?))),
         "not" => {
             if inside_negation {
                 return Err(input.new_custom_error(
                     SelectorParseErrorKind::UnexpectedIdent("not".into())
                 ));
             }
             return parse_negation(parser, input)
         },
@@ -1964,17 +1984,17 @@ where
 {
     (match_ignore_ascii_case! { &name,
         "first-child" => Ok(Component::FirstChild),
         "last-child"  => Ok(Component::LastChild),
         "only-child"  => Ok(Component::OnlyChild),
         "root" => Ok(Component::Root),
         "empty" => Ok(Component::Empty),
         "scope" => Ok(Component::Scope),
-        "host" if P::parse_host(parser) => Ok(Component::Host),
+        "host" if P::parse_host(parser) => Ok(Component::Host(None)),
         "first-of-type" => Ok(Component::FirstOfType),
         "last-of-type" => Ok(Component::LastOfType),
         "only-of-type" => Ok(Component::OnlyOfType),
         _ => Err(())
     }).or_else(|()| {
         P::parse_non_ts_pseudo_class(parser, location, name)
             .map(Component::NonTSPseudoClass)
     })
--- a/servo/components/style/dom.rs
+++ b/servo/components/style/dom.rs
@@ -789,16 +789,24 @@ pub trait TElement
             doc_rules_apply = false;
             f(
                 shadow.style_data(),
                 self.as_node().owner_doc().quirks_mode(),
                 Some(shadow.host()),
             );
         }
 
+        if let Some(shadow) = self.shadow_root() {
+            f(
+                shadow.style_data(),
+                self.as_node().owner_doc().quirks_mode(),
+                Some(shadow.host()),
+            );
+        }
+
         let mut current = self.assigned_slot();
         while let Some(slot) = current {
             // Slots can only have assigned nodes when in a shadow tree.
             let shadow = slot.containing_shadow().unwrap();
             f(
                 shadow.style_data(),
                 self.as_node().owner_doc().quirks_mode(),
                 Some(shadow.host()),
--- a/servo/components/style/invalidation/element/invalidation_map.rs
+++ b/servo/components/style/invalidation/element/invalidation_map.rs
@@ -202,44 +202,35 @@ impl InvalidationMap {
         self.id_to_selector.iter().fold(0, |accum, (_, ref v)| {
             accum + v.len()
         }) +
         self.class_to_selector.iter().fold(0, |accum, (_, ref v)| {
             accum + v.len()
         })
     }
 
-    /// Adds a selector to this `InvalidationMap`.  Returns Err(..) to
-    /// signify OOM.
-    pub fn note_selector(
-        &mut self,
-        selector: &Selector<SelectorImpl>,
-        quirks_mode: QuirksMode,
-    ) -> Result<(), FailedAllocationError> {
-        self.collect_invalidations_for(selector, quirks_mode)
-    }
-
     /// Clears this map, leaving it empty.
     pub fn clear(&mut self) {
         self.class_to_selector.clear();
         self.id_to_selector.clear();
         self.state_affecting_selectors.clear();
         self.document_state_selectors.clear();
         self.other_attribute_affecting_selectors.clear();
         self.has_id_attribute_selectors = false;
         self.has_class_attribute_selectors = false;
     }
 
-    // Returns Err(..) to signify OOM.
-    fn collect_invalidations_for(
+    /// Adds a selector to this `InvalidationMap`.  Returns Err(..) to
+    /// signify OOM.
+    pub fn note_selector(
         &mut self,
         selector: &Selector<SelectorImpl>,
-        quirks_mode: QuirksMode
+        quirks_mode: QuirksMode,
     ) -> Result<(), FailedAllocationError> {
-        debug!("InvalidationMap::collect_invalidations_for({:?})", selector);
+        debug!("InvalidationMap::note_selector({:?})", selector);
 
         let mut iter = selector.iter();
         let mut combinator;
         let mut index = 0;
 
         let mut document_state = DocumentState::empty();
 
         loop {
--- a/servo/components/style/invalidation/element/invalidator.rs
+++ b/servo/components/style/invalidation/element/invalidator.rs
@@ -1,17 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 //! The struct that takes care of encapsulating all the logic on where and how
 //! element styles need to be invalidated.
 
 use context::StackLimitChecker;
-use dom::{TElement, TNode};
+use dom::{TElement, TNode, TShadowRoot};
 use selector_parser::SelectorImpl;
 use selectors::matching::{CompoundSelectorMatchingResult, MatchingContext};
 use selectors::matching::matches_compound_selector_from;
 use selectors::parser::{Combinator, Component, Selector};
 use smallvec::SmallVec;
 use std::fmt;
 
 /// A trait to abstract the collection of invalidations for a given pass.
@@ -529,30 +529,32 @@ where
 
         if self.processor.light_tree_only() {
             let node = self.element.as_node();
             return self.invalidate_dom_descendants_of(node, invalidations);
         }
 
         let mut any_descendant = false;
 
-        // NOTE(emilio): This should not be needed for Shadow DOM for normal
-        // element state / attribute invalidations (it's needed for XBL though,
-        // due to the weird way the anon content there works (it doesn't block
-        // combinators)).
+        // NOTE(emilio): This is only needed for Shadow DOM to invalidate
+        // correctly on :host(..) changes. We could not do this instead and add
+        // a third kind of invalidation list instead, that walks shadow root
+        // children, but it's not clear it's worth it.
         //
-        // However, it's needed as of right now for document state invalidation,
-        // were we rely on iterating every element that ends up in the composed
-        // doc.
-        //
-        // Also, we could avoid having that special-case for document state
-        // invalidations if we invalidate for document state changes per
-        // subtree, though that's kind of annoying because we need to invalidate
-        // the shadow host subtree (to handle :host and ::slotted), and the
-        // actual shadow tree (to handle all other rules in the ShadowRoot).
+        // Also, it's needed as of right now for document state invalidation,
+        // where we rely on iterating every element that ends up in the composed
+        // doc, but we could fix that invalidating per subtree.
+        if let Some(root) = self.element.shadow_root() {
+            any_descendant |=
+                self.invalidate_dom_descendants_of(root.as_node(), invalidations);
+        }
+
+        // This is needed for XBL (technically) unconditionally, because XBL
+        // bindings do not block combinators in any way. However this is kinda
+        // broken anyway, since we should be looking at XBL rules too.
         if let Some(anon_content) = self.element.xbl_binding_anonymous_content() {
             any_descendant |=
                 self.invalidate_dom_descendants_of(anon_content, invalidations);
         }
 
         if let Some(before) = self.element.before_pseudo_element() {
             any_descendant |=
                 self.invalidate_pseudo_element_or_nac(before, invalidations);
--- a/servo/components/style/selector_map.rs
+++ b/servo/components/style/selector_map.rs
@@ -452,16 +452,17 @@ fn specific_bucket_for<'a>(
         // match the slotted <span>.
         //
         // FIXME(emilio, bug 1426516): The line below causes valgrind failures
         // and it's probably a false positive, we should uncomment whenever
         // jseward is back to confirm / whitelist it.
         //
         // Meanwhile taking the code path below is slower, but still correct.
         // Component::Slotted(ref selector) => find_bucket(selector.iter()),
+        Component::Host(Some(ref selector)) => find_bucket(selector.iter()),
         _ => Bucket::Universal
     }
 }
 
 /// Searches a compound selector from left to right, and returns the appropriate
 /// bucket for it.
 #[inline(always)]
 fn find_bucket<'a>(mut iter: SelectorIter<'a, SelectorImpl>) -> Bucket<'a> {
--- a/servo/components/style/stylist.rs
+++ b/servo/components/style/stylist.rs
@@ -2233,17 +2233,20 @@ impl CascadeData {
                         let rule = Rule::new(
                             selector.clone(),
                             hashes,
                             locked.clone(),
                             self.rules_source_order
                         );
 
                         if rebuild_kind.should_rebuild_invalidation() {
-                            self.invalidation_map.note_selector(&rule.selector, quirks_mode)?;
+                            self.invalidation_map.note_selector(
+                                selector,
+                                quirks_mode,
+                            )?;
                             let mut visitor = StylistSelectorVisitor {
                                 needs_revalidation: false,
                                 passed_rightmost_selector: false,
                                 attribute_dependencies: &mut self.attribute_dependencies,
                                 style_attribute_dependency: &mut self.style_attribute_dependency,
                                 state_dependencies: &mut self.state_dependencies,
                                 document_state_dependencies: &mut self.document_state_dependencies,
                                 mapped_ids: &mut self.mapped_ids,
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -313762,16 +313762,22 @@
     ]
    ],
    "css/css-scoping/host-dom-001.html": [
     [
      "/css/css-scoping/host-dom-001.html",
      {}
     ]
    ],
+   "css/css-scoping/host-functional-descendant-invalidation.html": [
+    [
+     "/css/css-scoping/host-functional-descendant-invalidation.html",
+     {}
+    ]
+   ],
    "css/css-scoping/shadow-cascade-order-001.html": [
     [
      "/css/css-scoping/shadow-cascade-order-001.html",
      {}
     ]
    ],
    "css/css-scoping/slotted-invalidation.html": [
     [
@@ -506746,21 +506752,25 @@
    "703ba0d07ece44f4cc017b5351dea3057337f234",
    "reftest"
   ],
   "css/css-scoping/host-descendant-002.html": [
    "c96ebd0da1c7b2ce00e56bbef54bdd382789e2f2",
    "reftest"
   ],
   "css/css-scoping/host-descendant-invalidation.html": [
-   "ec27e3cbe587470ecb945357c74954baf139d797",
+   "33ad555cd243bb5cd486b6ffb776f657d1185228",
    "testharness"
   ],
   "css/css-scoping/host-dom-001.html": [
-   "f77b672837e1c9728e53d74b533d79530fbd1249",
+   "33f34152fff538f6080b6fa36de337dca8a2b694",
+   "testharness"
+  ],
+  "css/css-scoping/host-functional-descendant-invalidation.html": [
+   "891d852131c37fd2148d46f5b6dfe32e1fc45bf3",
    "testharness"
   ],
   "css/css-scoping/host-multiple-001.html": [
    "eb45ceb8c80d2cfbeb5bd317ab906f0881a13435",
    "reftest"
   ],
   "css/css-scoping/host-nested-001.html": [
    "a0b74d2e6bf24e9142904a925f95e969d206db20",
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scoping/host-functional-descendant-invalidation.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<title>CSS Test: element style is correctly updated for rule with :host(..)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="help" href="https://drafts.csswg.org/css-scoping/#host-selector">
+<div id="host"><div id="slotted"></div></div>
+<script>
+test(function() {
+  let root = host.attachShadow({ mode: "open" });
+  root.innerHTML = `
+    <style>
+      :host ::slotted(div) { width: 100px; height: 100px; background: red; }
+      :host(.foo) ::slotted(div) { background: green; }
+    </style>
+    <slot></slot>
+  `;
+  assert_equals(getComputedStyle(slotted).backgroundColor, "rgb(255, 0, 0)");
+  host.classList.add('foo');
+  assert_equals(getComputedStyle(slotted).backgroundColor, "rgb(0, 128, 0)");
+});
+</script>