Bug 1364412: Allow pseudo-element selectors to store state. r?bholley draft
authorEmilio Cobos Álvarez <emilio@crisal.io>
Fri, 12 May 2017 15:51:46 +0200
changeset 577933 e6feacac29cd04f60435f0caaf0079f6e89c0194
parent 577886 1a4d93b432d98329335ae4d242d79c85fb23879b
child 577934 77b07afe50e3e283d80ff825c93be5945fb4af5e
push id58837
push userbmo:emilio+bugs@crisal.io
push dateMon, 15 May 2017 17:46:39 +0000
reviewersbholley
bugs1364412
milestone55.0a1
Bug 1364412: Allow pseudo-element selectors to store state. r?bholley MozReview-Commit-ID: CzAwg2uxqO2
servo/components/selectors/parser.rs
servo/components/style/gecko/selector_parser.rs
servo/components/style/stylist.rs
--- a/servo/components/selectors/parser.rs
+++ b/servo/components/selectors/parser.rs
@@ -56,17 +56,17 @@ macro_rules! with_all_bounds {
             type BorrowedNamespaceUrl: ?Sized + Eq;
             type BorrowedLocalName: ?Sized + Eq + Hash;
 
             /// non tree-structural pseudo-classes
             /// (see: https://drafts.csswg.org/selectors/#structural-pseudos)
             type NonTSPseudoClass: $($CommonBounds)* + Sized + ToCss + SelectorMethods<Impl = Self>;
 
             /// pseudo-elements
-            type PseudoElement: $($CommonBounds)* + Sized + ToCss;
+            type PseudoElementSelector: $($CommonBounds)* + Sized + ToCss;
         }
     }
 }
 
 macro_rules! with_bounds {
     ( [ $( $CommonBounds: tt )* ] [ $( $FromStr: tt )* ]) => {
         with_all_bounds! {
             [$($CommonBounds)* + $($FromStr)* + Display]
@@ -93,18 +93,18 @@ pub trait Parser {
 
     fn parse_non_ts_functional_pseudo_class
         (&self, _name: Cow<str>, _arguments: &mut CssParser)
         -> Result<<Self::Impl as SelectorImpl>::NonTSPseudoClass, ()>
     {
         Err(())
     }
 
-    fn parse_pseudo_element(&self, _name: Cow<str>)
-                            -> Result<<Self::Impl as SelectorImpl>::PseudoElement, ()> {
+    fn parse_pseudo_element(&self, _name: Cow<str>, _input: &mut CssParser)
+                            -> Result<<Self::Impl as SelectorImpl>::PseudoElementSelector, ()> {
         Err(())
     }
 
     fn default_namespace(&self) -> Option<<Self::Impl as SelectorImpl>::NamespaceUrl> {
         None
     }
 
     fn namespace_for_prefix(&self, _prefix: &<Self::Impl as SelectorImpl>::NamespacePrefix)
@@ -174,17 +174,17 @@ impl<Impl: SelectorImpl> SelectorInner<I
         let complex = ComplexSelector::from_vec(vec);
         Self::new(complex)
     }
 }
 
 #[derive(PartialEq, Eq, Hash, Clone)]
 pub struct Selector<Impl: SelectorImpl> {
     pub inner: SelectorInner<Impl>,
-    pub pseudo_element: Option<Impl::PseudoElement>,
+    pub pseudo_element: Option<Impl::PseudoElementSelector>,
     pub specificity: u32,
 }
 
 pub trait SelectorMethods {
     type Impl: SelectorImpl;
 
     fn visit<V>(&self, visitor: &mut V) -> bool
         where V: SelectorVisitor<Impl = Self::Impl>;
@@ -811,17 +811,17 @@ impl From<Specificity> for u32 {
     fn from(specificity: Specificity) -> u32 {
         cmp::min(specificity.id_selectors, MAX_10BIT) << 20
         | cmp::min(specificity.class_like_selectors, MAX_10BIT) << 10
         | cmp::min(specificity.element_selectors, MAX_10BIT)
     }
 }
 
 fn specificity<Impl>(complex_selector: &ComplexSelector<Impl>,
-                     pseudo_element: Option<&Impl::PseudoElement>)
+                     pseudo_element: Option<&Impl::PseudoElementSelector>)
                      -> u32
                      where Impl: SelectorImpl {
     let mut specificity = complex_selector_specificity(complex_selector);
     if pseudo_element.is_some() {
         specificity.element_selectors += 1;
     }
     specificity.into()
 }
@@ -911,17 +911,17 @@ fn parse_selector<P, Impl>(parser: &P, i
 /// If we parse N <= 4 entries, we save no reallocations.
 /// If we parse 4 < N <= 8 entries, we save one reallocation.
 /// If we parse N > 8 entries, we save two reallocations.
 type ParseVec<Impl> = SmallVec<[Component<Impl>; 8]>;
 
 fn parse_complex_selector_and_pseudo_element<P, Impl>(
         parser: &P,
         input: &mut CssParser)
-        -> Result<(ComplexSelector<Impl>, Option<Impl::PseudoElement>), ()>
+        -> Result<(ComplexSelector<Impl>, Option<Impl::PseudoElementSelector>), ()>
     where P: Parser<Impl=Impl>, Impl: SelectorImpl
 {
     let mut sequence = ParseVec::new();
     let mut pseudo_element;
     'outer_loop: loop {
         // Parse a sequence of simple selectors.
         pseudo_element = parse_compound_selector(parser, input, &mut sequence,
                                                  /* inside_negation = */ false)?;
@@ -1009,17 +1009,17 @@ fn parse_type_selector<P, Impl>(parser: 
             Ok(true)
         }
     }
 }
 
 #[derive(Debug)]
 enum SimpleSelectorParseResult<Impl: SelectorImpl> {
     SimpleSelector(Component<Impl>),
-    PseudoElement(Impl::PseudoElement),
+    PseudoElement(Impl::PseudoElementSelector),
 }
 
 /// * `Err(())`: Invalid selector, abort
 /// * `Ok(None)`: Not a simple selector, could be something else. `input` was not consumed.
 /// * `Ok(Some((namespace, local_name)))`: `None` for the local name means a `*` universal selector
 fn parse_qualified_name<'i, 't, P, Impl>
                        (parser: &P, input: &mut CssParser<'i, 't>,
                         in_attr_selector: bool)
@@ -1205,17 +1205,17 @@ fn parse_negation<P, Impl>(parser: &P,
 /// | [ HASH | class | attrib | pseudo | negation ]+
 ///
 /// `Err(())` means invalid selector
 fn parse_compound_selector<P, Impl>(
     parser: &P,
     input: &mut CssParser,
     mut sequence: &mut ParseVec<Impl>,
     inside_negation: bool)
-    -> Result<Option<Impl::PseudoElement>, ()>
+    -> Result<Option<Impl::PseudoElementSelector>, ()>
     where P: Parser<Impl=Impl>, Impl: SelectorImpl
 {
     // Consume any leading whitespace.
     loop {
         let position = input.position();
         if !matches!(input.next_including_whitespace(), Ok(Token::WhiteSpace(_))) {
             input.reset(position);
             break
@@ -1330,33 +1330,33 @@ fn parse_one_simple_selector<P, Impl>(pa
             match input.next_including_whitespace() {
                 Ok(Token::Ident(name)) => {
                     // Supported CSS 2.1 pseudo-elements only.
                     // ** Do not add to this list! **
                     if name.eq_ignore_ascii_case("before") ||
                        name.eq_ignore_ascii_case("after") ||
                        name.eq_ignore_ascii_case("first-line") ||
                        name.eq_ignore_ascii_case("first-letter") {
-                        let pseudo_element = P::parse_pseudo_element(parser, name)?;
+                        let pseudo_element = P::parse_pseudo_element(parser, name, input)?;
                         Ok(Some(SimpleSelectorParseResult::PseudoElement(pseudo_element)))
                     } else {
                         let pseudo_class = parse_simple_pseudo_class(parser, name)?;
                         Ok(Some(SimpleSelectorParseResult::SimpleSelector(pseudo_class)))
                     }
                 }
                 Ok(Token::Function(name)) => {
                     let pseudo = input.parse_nested_block(|input| {
                         parse_functional_pseudo_class(parser, input, name, inside_negation)
                     })?;
                     Ok(Some(SimpleSelectorParseResult::SimpleSelector(pseudo)))
                 }
                 Ok(Token::Colon) => {
                     match input.next_including_whitespace() {
                         Ok(Token::Ident(name)) => {
-                            let pseudo = P::parse_pseudo_element(parser, name)?;
+                            let pseudo = P::parse_pseudo_element(parser, name, input)?;
                             Ok(Some(SimpleSelectorParseResult::PseudoElement(pseudo)))
                         }
                         _ => Err(())
                     }
                 }
                 _ => Err(())
             }
         }
@@ -1500,17 +1500,17 @@ pub mod tests {
                                                 parser: &mut CssParser)
                                                 -> Result<PseudoClass, ()> {
             match_ignore_ascii_case! { &name,
                 "lang" => Ok(PseudoClass::Lang(try!(parser.expect_ident_or_string()).into_owned())),
                 _ => Err(())
             }
         }
 
-        fn parse_pseudo_element(&self, name: Cow<str>)
+        fn parse_pseudo_element(&self, name: Cow<str>, input: &mut CssParser)
                                 -> Result<PseudoElement, ()> {
             match_ignore_ascii_case! { &name,
                 "before" => Ok(PseudoElement::Before),
                 "after" => Ok(PseudoElement::After),
                 _ => Err(())
             }
         }
 
--- a/servo/components/style/gecko/selector_parser.rs
+++ b/servo/components/style/gecko/selector_parser.rs
@@ -1,15 +1,16 @@
 /* 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/. */
 
 //! Gecko-specific bits for selector-parsing.
 
 use cssparser::{Parser, ToCss};
+use element_state::{IN_ACTIVE_STATE, IN_FOCUS_STATE, IN_HOVER_STATE};
 use element_state::ElementState;
 use gecko_bindings::structs::CSSPseudoClassType;
 use gecko_bindings::structs::nsIAtom;
 use selector_parser::{SelectorParser, PseudoElementCascadeType};
 use selectors::parser::{ComplexSelector, SelectorMethods};
 use selectors::visitor::SelectorVisitor;
 use std::borrow::Cow;
 use std::fmt;
@@ -320,16 +321,25 @@ impl NonTSPseudoClass {
                     $(NonTSPseudoClass::$s_name(..) => check_flag!($s_flags),)*
                     NonTSPseudoClass::MozAny(_) => false,
                 }
             }
         }
         apply_non_ts_list!(pseudo_class_check_internal)
     }
 
+    /// https://drafts.csswg.org/selectors-4/#useraction-pseudos
+    ///
+    /// We intentionally skip the link-related ones.
+    fn is_safe_user_action_state(&self) -> bool {
+        matches!(*self, NonTSPseudoClass::Hover |
+                        NonTSPseudoClass::Active |
+                        NonTSPseudoClass::Focus)
+    }
+
     /// Get the state flag associated with a pseudo-class, if any.
     pub fn state_flag(&self) -> ElementState {
         macro_rules! flag {
             (_) => (ElementState::empty());
             ($state:ident) => (::element_state::$state);
         }
         macro_rules! pseudo_class_state {
             (bare: [$(($css:expr, $name:ident, $gecko_type:tt, $state:tt, $flags:tt),)*],
@@ -370,27 +380,74 @@ impl NonTSPseudoClass {
         apply_non_ts_list!(pseudo_class_geckotype)
     }
 }
 
 /// The dummy struct we use to implement our selector parsing.
 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
 pub struct SelectorImpl;
 
+/// Some subset of pseudo-elements in Gecko are sensitive to some state
+/// selectors.
+///
+/// We store the sensitive states in this struct in order to properly handle
+/// these.
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub struct PseudoElementSelector {
+    pseudo: PseudoElement,
+    state: ElementState,
+}
+
+impl PseudoElementSelector {
+    /// Returns the pseudo-element this selector represents.
+    pub fn pseudo_element(&self) -> &PseudoElement {
+        &self.pseudo
+    }
+}
+
+impl ToCss for PseudoElementSelector {
+    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+        where W: fmt::Write,
+    {
+        if cfg!(debug_assertions) {
+            let mut state = self.state;
+            state.remove(IN_HOVER_STATE | IN_ACTIVE_STATE | IN_FOCUS_STATE);
+            assert_eq!(state, ElementState::empty(),
+                       "Unhandled pseudo-element state selector?");
+        }
+
+        self.pseudo.to_css(dest)?;
+
+        if self.state.contains(IN_HOVER_STATE) {
+            dest.write_str(":hover")?
+        }
+
+        if self.state.contains(IN_ACTIVE_STATE) {
+            dest.write_str(":active")?
+        }
+
+        if self.state.contains(IN_FOCUS_STATE) {
+            dest.write_str(":focus")?
+        }
+
+        Ok(())
+    }
+}
+
 impl ::selectors::SelectorImpl for SelectorImpl {
     type AttrValue = Atom;
     type Identifier = Atom;
     type ClassName = Atom;
     type LocalName = Atom;
     type NamespacePrefix = Atom;
     type NamespaceUrl = Namespace;
     type BorrowedNamespaceUrl = WeakNamespace;
     type BorrowedLocalName = WeakAtom;
 
-    type PseudoElement = PseudoElement;
+    type PseudoElementSelector = PseudoElementSelector;
     type NonTSPseudoClass = NonTSPseudoClass;
 }
 
 impl<'a> ::selectors::Parser for SelectorParser<'a> {
     type Impl = SelectorImpl;
 
     fn parse_non_ts_pseudo_class(&self, name: Cow<str>) -> Result<NonTSPseudoClass, ()> {
         macro_rules! pseudo_class_parse {
@@ -442,21 +499,44 @@ impl<'a> ::selectors::Parser for Selecto
         let pseudo_class = apply_non_ts_list!(pseudo_class_string_parse);
         if !pseudo_class.is_internal() || self.in_user_agent_stylesheet() {
             Ok(pseudo_class)
         } else {
             Err(())
         }
     }
 
-    fn parse_pseudo_element(&self, name: Cow<str>) -> Result<PseudoElement, ()> {
-        match PseudoElement::from_slice(&name, self.in_user_agent_stylesheet()) {
-            Some(pseudo) => Ok(pseudo),
-            None => Err(()),
-        }
+    fn parse_pseudo_element(&self, name: Cow<str>, input: &mut Parser) -> Result<PseudoElementSelector, ()> {
+        let pseudo =
+            match PseudoElement::from_slice(&name, self.in_user_agent_stylesheet()) {
+                Some(pseudo) => pseudo,
+                None => return Err(()),
+            };
+
+        let state = input.try(|input| {
+            let mut state = ElementState::empty();
+
+            while !input.is_exhausted() {
+                input.expect_colon()?;
+                let ident = input.expect_ident()?;
+                let pseudo_class = self.parse_non_ts_pseudo_class(ident)?;
+
+                if !pseudo_class.is_safe_user_action_state() {
+                    return Err(())
+                }
+                state.insert(pseudo_class.state_flag());
+            }
+
+            Ok(state)
+        });
+
+        Ok(PseudoElementSelector {
+            pseudo: pseudo,
+            state: state.unwrap_or(ElementState::empty()),
+        })
     }
 
     fn default_namespace(&self) -> Option<Namespace> {
         self.namespaces.default.clone()
     }
 
     fn namespace_for_prefix(&self, prefix: &Atom) -> Option<Namespace> {
         self.namespaces.prefixes.get(prefix).cloned()
--- a/servo/components/style/stylist.rs
+++ b/servo/components/style/stylist.rs
@@ -444,19 +444,19 @@ impl Stylist {
     }
 
     #[inline]
     fn add_rule_to_map(&mut self,
                        selector: &Selector<SelectorImpl>,
                        rule: &Arc<Locked<StyleRule>>,
                        stylesheet: &Stylesheet)
     {
-        let map = if let Some(ref pseudo) = selector.pseudo_element {
+        let map = if let Some(ref pseudo_selector) = selector.pseudo_element {
             self.pseudos_map
-                .entry(pseudo.clone())
+                .entry(pseudo_selector.pseudo_element().clone())
                 .or_insert_with(PerPseudoElementSelectorMap::new)
                 .borrow_for_origin(&stylesheet.origin)
         } else {
             self.element_map.borrow_for_origin(&stylesheet.origin)
         };
 
         map.insert(Rule::new(selector.inner.clone(),
                              rule.clone(),