diff --git a/components/script/dom/element.rs b/components/script/dom/element.rs index 0dc3db0ebe5..d7f3b89ba2f 100644 --- a/components/script/dom/element.rs +++ b/components/script/dom/element.rs @@ -87,8 +87,9 @@ use ref_filter_map::ref_filter_map; use script_layout_interface::message::ReflowQueryType; use script_thread::Runnable; use selectors::attr::{AttrSelectorOperation, NamespaceConstraint}; -use selectors::matching::{ElementSelectorFlags, MatchingContext, MatchingMode, matches_selector_list}; +use selectors::matching::{ElementSelectorFlags, MatchingContext, MatchingMode}; use selectors::matching::{HAS_EDGE_CHILD_SELECTOR, HAS_SLOW_SELECTOR, HAS_SLOW_SELECTOR_LATER_SIBLINGS}; +use selectors::matching::{RelevantLinkStatus, matches_selector_list}; use servo_atoms::Atom; use std::ascii::AsciiExt; use std::borrow::Cow; @@ -2429,6 +2430,7 @@ impl<'a> ::selectors::Element for Root { fn match_non_ts_pseudo_class(&self, pseudo_class: &NonTSPseudoClass, _: &mut MatchingContext, + _: &RelevantLinkStatus, _: &mut F) -> bool where F: FnMut(&Self, ElementSelectorFlags), @@ -2478,6 +2480,20 @@ impl<'a> ::selectors::Element for Root { } } + fn is_link(&self) -> bool { + // FIXME: This is HTML only. + let node = self.upcast::(); + match node.type_id() { + // https://html.spec.whatwg.org/multipage/#selector-link + NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLAnchorElement)) | + NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLAreaElement)) | + NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLLinkElement)) => { + self.has_attribute(&local_name!("href")) + }, + _ => false, + } + } + fn get_id(&self) -> Option { self.id_attribute.borrow().clone() } @@ -2592,20 +2608,6 @@ impl Element { } } - fn is_link(&self) -> bool { - // FIXME: This is HTML only. - let node = self.upcast::(); - match node.type_id() { - // https://html.spec.whatwg.org/multipage/#selector-link - NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLAnchorElement)) | - NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLAreaElement)) | - NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLLinkElement)) => { - self.has_attribute(&local_name!("href")) - }, - _ => false, - } - } - /// Please call this method *only* for real click events /// /// https://html.spec.whatwg.org/multipage/#run-authentic-click-activation-steps diff --git a/components/script/layout_wrapper.rs b/components/script/layout_wrapper.rs index 5241d01785d..eab6f2fd9c9 100644 --- a/components/script/layout_wrapper.rs +++ b/components/script/layout_wrapper.rs @@ -51,7 +51,7 @@ use script_layout_interface::{OpaqueStyleAndLayoutData, PartialPersistentLayoutD use script_layout_interface::wrapper_traits::{DangerousThreadSafeLayoutNode, GetLayoutData, LayoutNode}; use script_layout_interface::wrapper_traits::{PseudoElementType, ThreadSafeLayoutElement, ThreadSafeLayoutNode}; use selectors::attr::{AttrSelectorOperation, NamespaceConstraint}; -use selectors::matching::{ElementSelectorFlags, MatchingContext}; +use selectors::matching::{ElementSelectorFlags, MatchingContext, RelevantLinkStatus}; use servo_atoms::Atom; use servo_url::ServoUrl; use std::fmt; @@ -680,6 +680,7 @@ impl<'le> ::selectors::Element for ServoLayoutElement<'le> { fn match_non_ts_pseudo_class(&self, pseudo_class: &NonTSPseudoClass, _: &mut MatchingContext, + _: &RelevantLinkStatus, _: &mut F) -> bool where F: FnMut(&Self, ElementSelectorFlags), @@ -687,16 +688,7 @@ impl<'le> ::selectors::Element for ServoLayoutElement<'le> { match *pseudo_class { // https://github.com/servo/servo/issues/8718 NonTSPseudoClass::Link | - NonTSPseudoClass::AnyLink => unsafe { - match self.as_node().script_type_id() { - // https://html.spec.whatwg.org/multipage/#selector-link - NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLAnchorElement)) | - NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLAreaElement)) | - NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLLinkElement)) => - (*self.element.unsafe_get()).get_attr_val_for_layout(&ns!(), &local_name!("href")).is_some(), - _ => false, - } - }, + NonTSPseudoClass::AnyLink => self.is_link(), NonTSPseudoClass::Visited => false, // FIXME(#15746): This is wrong, we need to instead use extended filtering as per RFC4647 @@ -731,6 +723,20 @@ impl<'le> ::selectors::Element for ServoLayoutElement<'le> { } } + #[inline] + fn is_link(&self) -> bool { + unsafe { + match self.as_node().script_type_id() { + // https://html.spec.whatwg.org/multipage/#selector-link + NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLAnchorElement)) | + NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLAreaElement)) | + NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLLinkElement)) => + (*self.element.unsafe_get()).get_attr_val_for_layout(&ns!(), &local_name!("href")).is_some(), + _ => false, + } + } + } + #[inline] fn get_id(&self) -> Option { unsafe { @@ -1187,6 +1193,7 @@ impl<'le> ::selectors::Element for ServoThreadSafeLayoutElement<'le> { fn match_non_ts_pseudo_class(&self, _: &NonTSPseudoClass, _: &mut MatchingContext, + _: &RelevantLinkStatus, _: &mut F) -> bool where F: FnMut(&Self, ElementSelectorFlags), @@ -1196,6 +1203,11 @@ impl<'le> ::selectors::Element for ServoThreadSafeLayoutElement<'le> { false } + fn is_link(&self) -> bool { + warn!("ServoThreadSafeLayoutElement::is_link called"); + false + } + fn get_id(&self) -> Option { debug!("ServoThreadSafeLayoutElement::get_id called"); None diff --git a/components/selectors/matching.rs b/components/selectors/matching.rs index 50b686f2558..439308754fe 100644 --- a/components/selectors/matching.rs +++ b/components/selectors/matching.rs @@ -94,6 +94,16 @@ pub enum MatchingMode { ForStatelessPseudoElement, } +/// The mode to use when matching unvisited and visited links. +#[derive(PartialEq, Eq, Copy, Clone, Debug)] +pub enum VisitedHandlingMode { + /// All links are matched as if they are unvisted. + AllLinksUnvisited, + /// A element's "relevant link" is the element being matched if it is a link + /// or the nearest ancestor link. The relevant link is matched as though it + /// is visited, and all other links are matched as if they are unvisited. + RelevantLinkVisited, +} /// Data associated with the matching process for a element. This context is /// used across many selectors for an element, so it's not appropriate for @@ -106,6 +116,13 @@ pub struct MatchingContext<'a> { pub matching_mode: MatchingMode, /// The bloom filter used to fast-reject selectors. pub bloom_filter: Option<&'a BloomFilter>, + /// Input that controls how matching for links is handled. + pub visited_handling: VisitedHandlingMode, + /// Output that records whether we encountered a "relevant link" while + /// matching _any_ selector for this element. (This differs from + /// `RelevantLinkStatus` which tracks the status for the _current_ selector + /// only.) + relevant_link_found: bool, } impl<'a> MatchingContext<'a> { @@ -118,6 +135,8 @@ impl<'a> MatchingContext<'a> { relations: StyleRelations::empty(), matching_mode: matching_mode, bloom_filter: bloom_filter, + visited_handling: VisitedHandlingMode::AllLinksUnvisited, + relevant_link_found: false, } } } @@ -156,6 +175,100 @@ fn may_match(sel: &SelectorInner, true } +/// Tracks whether we are currently looking for relevant links for a given +/// complex selector. A "relevant link" is the element being matched if it is a +/// link or the nearest ancestor link. +/// +/// `matches_complex_selector` creates a new instance of this for each complex +/// selector we try to match for an element. This is done because `is_visited` +/// and `is_unvisited` are based on relevant link state of only the current +/// complex selector being matched (not the global relevant link status for all +/// selectors in `MatchingContext`). +#[derive(PartialEq, Eq, Copy, Clone)] +pub enum RelevantLinkStatus { + /// Looking for a possible relevant link. This is the initial mode when + /// matching a selector. + Looking, + /// Not looking for a relevant link. We transition to this mode if we + /// encounter a sibiling combinator (since only ancestor combinators are + /// allowed for this purpose). + NotLooking, + /// Found a relevant link for the element being matched. + Found, +} + +impl Default for RelevantLinkStatus { + fn default() -> Self { + RelevantLinkStatus::NotLooking + } +} + +impl RelevantLinkStatus { + /// If we found the relevant link for this element, record that in the + /// overall matching context for the element as a whole and stop looking for + /// addtional links. + fn examine_potential_link(&self, element: &E, context: &mut MatchingContext) + -> RelevantLinkStatus + where E: Element, + { + if *self != RelevantLinkStatus::Looking { + return *self + } + + if !element.is_link() { + return *self + } + + // We found a relevant link. Record this in the `MatchingContext`, + // where we track whether one was found for _any_ selector (meaning + // this field might already be true from a previous selector). + context.relevant_link_found = true; + // Also return `Found` to update the relevant link status for _this_ + // specific selector's matching process. + RelevantLinkStatus::Found + } + + /// Returns whether an element is considered visited for the purposes of + /// matching. This is true only if the element is a link, an relevant link + /// exists for the element, and the visited handling mode is set to accept + /// relevant links as visited. + pub fn is_visited(&self, element: &E, context: &MatchingContext) -> bool + where E: Element, + { + if !element.is_link() { + return false + } + + // Non-relevant links are always unvisited. + if *self != RelevantLinkStatus::Found { + return false + } + + context.visited_handling == VisitedHandlingMode::RelevantLinkVisited + } + + /// Returns whether an element is considered unvisited for the purposes of + /// matching. Assuming the element is a link, this is always true for + /// non-relevant links, since only relevant links can potentially be treated + /// as visited. If this is a relevant link, then is it unvisited if the + /// visited handling mode is set to treat all links as unvisted (including + /// relevant links). + pub fn is_unvisited(&self, element: &E, context: &MatchingContext) -> bool + where E: Element, + { + if !element.is_link() { + return false + } + + // Non-relevant links are always unvisited. + if *self != RelevantLinkStatus::Found { + return true + } + + context.visited_handling == VisitedHandlingMode::AllLinksUnvisited + } +} + /// A result of selector matching, includes 3 failure types, /// /// NotMatchedAndRestartFromClosestLaterSibling @@ -267,6 +380,7 @@ pub fn matches_complex_selector(complex_selector: &ComplexSelector true, _ => false @@ -276,13 +390,16 @@ pub fn matches_complex_selector(complex_selector: &ComplexSelector(mut selector_iter: SelectorIter, element: &E, context: &mut MatchingContext, + relevant_link: RelevantLinkStatus, flags_setter: &mut F) -> SelectorMatchingResult where E: Element, F: FnMut(&E, ElementSelectorFlags), { + let mut relevant_link = relevant_link.examine_potential_link(element, context); + let matches_all_simple_selectors = selector_iter.all(|simple| { - matches_simple_selector(simple, element, context, flags_setter) + matches_simple_selector(simple, element, context, &relevant_link, flags_setter) }); let combinator = selector_iter.next_sequence(); @@ -300,6 +417,9 @@ fn matches_complex_selector_internal(mut selector_iter: SelectorIter { let (mut next_element, candidate_not_found) = match c { Combinator::NextSibling | Combinator::LaterSibling => { + // Only ancestor combinators are allowed while looking for + // relevant links, so switch to not looking. + relevant_link = RelevantLinkStatus::NotLooking; (element.prev_sibling_element(), SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant) } @@ -321,6 +441,7 @@ fn matches_complex_selector_internal(mut selector_iter: SelectorIter( selector: &Component, element: &E, context: &mut MatchingContext, + relevant_link: &RelevantLinkStatus, flags_setter: &mut F) -> bool where E: Element, @@ -465,7 +587,7 @@ fn matches_simple_selector( ) } Component::NonTSPseudoClass(ref pc) => { - element.match_non_ts_pseudo_class(pc, context, flags_setter) + element.match_non_ts_pseudo_class(pc, context, relevant_link, flags_setter) } Component::FirstChild => { matches_first_child(element, flags_setter) @@ -509,7 +631,7 @@ fn matches_simple_selector( matches_generic_nth_child(element, 0, 1, true, true, flags_setter) } Component::Negation(ref negated) => { - !negated.iter().all(|ss| matches_simple_selector(ss, element, context, flags_setter)) + !negated.iter().all(|ss| matches_simple_selector(ss, element, context, relevant_link, flags_setter)) } } } diff --git a/components/selectors/tree.rs b/components/selectors/tree.rs index 276c788d05b..6122f22490f 100644 --- a/components/selectors/tree.rs +++ b/components/selectors/tree.rs @@ -2,11 +2,11 @@ * 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/. */ -//! Traits that nodes must implement. Breaks the otherwise-cyclic dependency between layout and -//! style. +//! Traits that nodes must implement. Breaks the otherwise-cyclic dependency +//! between layout and style. use attr::{AttrSelectorOperation, NamespaceConstraint}; -use matching::{ElementSelectorFlags, MatchingContext}; +use matching::{ElementSelectorFlags, MatchingContext, RelevantLinkStatus}; use parser::SelectorImpl; pub trait Element: Sized { @@ -50,6 +50,7 @@ pub trait Element: Sized { fn match_non_ts_pseudo_class(&self, pc: &::NonTSPseudoClass, context: &mut MatchingContext, + relevant_link: &RelevantLinkStatus, flags_setter: &mut F) -> bool where F: FnMut(&Self, ElementSelectorFlags); @@ -58,6 +59,9 @@ pub trait Element: Sized { context: &mut MatchingContext) -> bool; + /// Whether this element is a `link`. + fn is_link(&self) -> bool; + fn get_id(&self) -> Option<::Identifier>; fn has_class(&self, name: &::ClassName) -> bool; diff --git a/components/style/gecko/wrapper.rs b/components/style/gecko/wrapper.rs index fc56362810c..638efdf8b78 100644 --- a/components/style/gecko/wrapper.rs +++ b/components/style/gecko/wrapper.rs @@ -65,7 +65,7 @@ use rule_tree::CascadeLevel as ServoCascadeLevel; use selector_parser::ElementExt; use selectors::Element; use selectors::attr::{AttrSelectorOperation, AttrSelectorOperator, CaseSensitivity, NamespaceConstraint}; -use selectors::matching::{ElementSelectorFlags, MatchingContext, MatchingMode}; +use selectors::matching::{ElementSelectorFlags, MatchingContext, MatchingMode, RelevantLinkStatus}; use shared_lock::Locked; use sink::Push; use std::cell::RefCell; @@ -1236,6 +1236,7 @@ impl<'le> ::selectors::Element for GeckoElement<'le> { fn match_non_ts_pseudo_class(&self, pseudo_class: &NonTSPseudoClass, context: &mut MatchingContext, + relevant_link: &RelevantLinkStatus, flags_setter: &mut F) -> bool where F: FnMut(&Self, ElementSelectorFlags), @@ -1243,8 +1244,6 @@ impl<'le> ::selectors::Element for GeckoElement<'le> { use selectors::matching::*; match *pseudo_class { NonTSPseudoClass::AnyLink | - NonTSPseudoClass::Link | - NonTSPseudoClass::Visited | NonTSPseudoClass::Active | NonTSPseudoClass::Focus | NonTSPseudoClass::Hover | @@ -1293,6 +1292,8 @@ impl<'le> ::selectors::Element for GeckoElement<'le> { // here, to handle `:any-link` correctly. self.get_state().intersects(pseudo_class.state_flag()) }, + NonTSPseudoClass::Link => relevant_link.is_unvisited(self, context), + NonTSPseudoClass::Visited => relevant_link.is_visited(self, context), NonTSPseudoClass::MozFirstNode => { flags_setter(self, HAS_EDGE_CHILD_SELECTOR); let mut elem = self.as_node(); @@ -1369,6 +1370,15 @@ impl<'le> ::selectors::Element for GeckoElement<'le> { } } + #[inline] + fn is_link(&self) -> bool { + let mut context = MatchingContext::new(MatchingMode::Normal, None); + self.match_non_ts_pseudo_class(&NonTSPseudoClass::AnyLink, + &mut context, + &RelevantLinkStatus::default(), + &mut |_, _| {}) + } + fn get_id(&self) -> Option { if !self.has_id() { return None; @@ -1420,14 +1430,6 @@ impl<'a> NamespaceConstraintHelpers for NamespaceConstraint<&'a Namespace> { } impl<'le> ElementExt for GeckoElement<'le> { - #[inline] - fn is_link(&self) -> bool { - let mut context = MatchingContext::new(MatchingMode::Normal, None); - self.match_non_ts_pseudo_class(&NonTSPseudoClass::AnyLink, - &mut context, - &mut |_, _| {}) - } - #[inline] fn matches_user_and_author_rules(&self) -> bool { self.flags() & (NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE as u32) == 0 diff --git a/components/style/restyle_hints.rs b/components/style/restyle_hints.rs index 2d5d9aed27c..38ceb84d2b2 100644 --- a/components/style/restyle_hints.rs +++ b/components/style/restyle_hints.rs @@ -20,7 +20,7 @@ use selector_map::{SelectorMap, SelectorMapEntry}; use selector_parser::{NonTSPseudoClass, PseudoElement, SelectorImpl, Snapshot, SnapshotMap, AttrValue}; use selectors::Element; use selectors::attr::{AttrSelectorOperation, NamespaceConstraint}; -use selectors::matching::{ElementSelectorFlags, MatchingContext, MatchingMode}; +use selectors::matching::{ElementSelectorFlags, MatchingContext, MatchingMode, RelevantLinkStatus}; use selectors::matching::matches_selector; use selectors::parser::{Combinator, Component, Selector, SelectorInner, SelectorMethods}; use selectors::visitor::SelectorVisitor; @@ -535,6 +535,7 @@ impl<'a, E> Element for ElementWrapper<'a, E> fn match_non_ts_pseudo_class(&self, pseudo_class: &NonTSPseudoClass, context: &mut MatchingContext, + relevant_link: &RelevantLinkStatus, _setter: &mut F) -> bool where F: FnMut(&Self, ElementSelectorFlags), @@ -580,6 +581,7 @@ impl<'a, E> Element for ElementWrapper<'a, E> if flag.is_empty() { return self.element.match_non_ts_pseudo_class(pseudo_class, context, + relevant_link, &mut |_, _| {}) } match self.snapshot().and_then(|s| s.state()) { @@ -587,6 +589,7 @@ impl<'a, E> Element for ElementWrapper<'a, E> None => { self.element.match_non_ts_pseudo_class(pseudo_class, context, + relevant_link, &mut |_, _| {}) } } @@ -600,6 +603,14 @@ impl<'a, E> Element for ElementWrapper<'a, E> self.element.match_pseudo_element(pseudo_element, context) } + fn is_link(&self) -> bool { + let mut context = MatchingContext::new(MatchingMode::Normal, None); + self.match_non_ts_pseudo_class(&NonTSPseudoClass::AnyLink, + &mut context, + &RelevantLinkStatus::default(), + &mut |_, _| {}) + } + fn parent_element(&self) -> Option { self.element.parent_element() .map(|e| ElementWrapper::new(e, self.snapshot_map)) diff --git a/components/style/selector_parser.rs b/components/style/selector_parser.rs index 9e4e5deeaeb..e1a7a0aecda 100644 --- a/components/style/selector_parser.rs +++ b/components/style/selector_parser.rs @@ -103,9 +103,6 @@ pub enum PseudoElementCascadeType { /// An extension to rust-selector's `Element` trait. pub trait ElementExt: Element + Debug { - /// Whether this element is a `link`. - fn is_link(&self) -> bool; - /// Whether this element should match user and author rules. /// /// We use this for Native Anonymous Content in Gecko. diff --git a/components/style/servo/selector_parser.rs b/components/style/servo/selector_parser.rs index 1cae314ddec..34cec12b83f 100644 --- a/components/style/servo/selector_parser.rs +++ b/components/style/servo/selector_parser.rs @@ -16,7 +16,6 @@ use restyle_hints::ElementSnapshot; use selector_parser::{ElementExt, PseudoElementCascadeType, SelectorParser}; use selectors::Element; use selectors::attr::{AttrSelectorOperation, NamespaceConstraint}; -use selectors::matching::{MatchingContext, MatchingMode}; use selectors::parser::SelectorMethods; use selectors::visitor::SelectorVisitor; use std::borrow::Cow; @@ -601,13 +600,6 @@ impl ServoElementSnapshot { } impl + Debug> ElementExt for E { - fn is_link(&self) -> bool { - let mut context = MatchingContext::new(MatchingMode::Normal, None); - self.match_non_ts_pseudo_class(&NonTSPseudoClass::AnyLink, - &mut context, - &mut |_, _| {}) - } - #[inline] fn matches_user_and_author_rules(&self) -> bool { true