/* 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 https://mozilla.org/MPL/2.0/. */ #![deny(missing_docs)] //! Servo's selector parser. use crate::attr::{AttrIdentifier, AttrValue}; use crate::dom::{OpaqueNode, TElement, TNode}; use crate::invalidation::element::document_state::InvalidationMatchingData; use crate::invalidation::element::element_wrapper::ElementSnapshot; use crate::properties::longhands::display::computed_value::T as Display; use crate::properties::{ComputedValues, PropertyFlags}; use crate::selector_parser::AttrValue as SelectorAttrValue; use crate::selector_parser::{PseudoElementCascadeType, SelectorParser}; use crate::values::{AtomIdent, AtomString}; use crate::{Atom, CaseSensitivityExt, LocalName, Namespace, Prefix}; use cssparser::{serialize_identifier, CowRcStr, Parser as CssParser, SourceLocation, ToCss}; use dom::{DocumentState, ElementState}; use fxhash::FxHashMap; use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint}; use selectors::parser::SelectorParseErrorKind; use selectors::visitor::SelectorVisitor; use std::fmt; use std::mem; use std::ops::{Deref, DerefMut}; use style_traits::{ParseError, StyleParseErrorKind}; /// A pseudo-element, both public and private. /// /// NB: If you add to this list, be sure to update `each_simple_pseudo_element` too. #[derive( Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, ToShmem, )] #[allow(missing_docs)] #[repr(usize)] pub enum PseudoElement { // Eager pseudos. Keep these first so that eager_index() works. After = 0, Before, Selection, // If/when :first-letter is added, update is_first_letter accordingly. // If/when :first-line is added, update is_first_line accordingly. // If/when ::first-letter, ::first-line, or ::placeholder are added, adjust // our property_restriction implementation to do property filtering for // them. Also, make sure the UA sheet has the !important rules some of the // APPLIES_TO_PLACEHOLDER properties expect! // Non-eager pseudos. DetailsSummary, DetailsContent, ServoText, ServoInputText, ServoTableWrapper, ServoAnonymousTableWrapper, ServoAnonymousTable, ServoAnonymousTableRow, ServoAnonymousTableCell, ServoAnonymousBlock, ServoInlineBlockWrapper, ServoInlineAbsolute, } /// The count of all pseudo-elements. pub const PSEUDO_COUNT: usize = PseudoElement::ServoInlineAbsolute as usize + 1; impl ::selectors::parser::PseudoElement for PseudoElement { type Impl = SelectorImpl; } impl ToCss for PseudoElement { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { use self::PseudoElement::*; dest.write_str(match *self { After => "::after", Before => "::before", Selection => "::selection", DetailsSummary => "::-servo-details-summary", DetailsContent => "::-servo-details-content", ServoText => "::-servo-text", ServoInputText => "::-servo-input-text", ServoTableWrapper => "::-servo-table-wrapper", ServoAnonymousTableWrapper => "::-servo-anonymous-table-wrapper", ServoAnonymousTable => "::-servo-anonymous-table", ServoAnonymousTableRow => "::-servo-anonymous-table-row", ServoAnonymousTableCell => "::-servo-anonymous-table-cell", ServoAnonymousBlock => "::-servo-anonymous-block", ServoInlineBlockWrapper => "::-servo-inline-block-wrapper", ServoInlineAbsolute => "::-servo-inline-absolute", }) } } /// The number of eager pseudo-elements. Keep this in sync with cascade_type. pub const EAGER_PSEUDO_COUNT: usize = 3; impl PseudoElement { /// Gets the canonical index of this eagerly-cascaded pseudo-element. #[inline] pub fn eager_index(&self) -> usize { debug_assert!(self.is_eager()); self.clone() as usize } /// An index for this pseudo-element to be indexed in an enumerated array. #[inline] pub fn index(&self) -> usize { self.clone() as usize } /// An array of `None`, one per pseudo-element. pub fn pseudo_none_array() -> [Option; PSEUDO_COUNT] { Default::default() } /// Creates a pseudo-element from an eager index. #[inline] pub fn from_eager_index(i: usize) -> Self { assert!(i < EAGER_PSEUDO_COUNT); let result: PseudoElement = unsafe { mem::transmute(i) }; debug_assert!(result.is_eager()); result } /// Whether the current pseudo element is ::before or ::after. #[inline] pub fn is_before_or_after(&self) -> bool { self.is_before() || self.is_after() } /// Whether this is an unknown ::-webkit- pseudo-element. #[inline] pub fn is_unknown_webkit_pseudo_element(&self) -> bool { false } /// Whether this pseudo-element is the ::marker pseudo. #[inline] pub fn is_marker(&self) -> bool { false } /// Whether this pseudo-element is the ::selection pseudo. #[inline] pub fn is_selection(&self) -> bool { *self == PseudoElement::Selection } /// Whether this pseudo-element is the ::before pseudo. #[inline] pub fn is_before(&self) -> bool { *self == PseudoElement::Before } /// Whether this pseudo-element is the ::after pseudo. #[inline] pub fn is_after(&self) -> bool { *self == PseudoElement::After } /// Whether the current pseudo element is :first-letter #[inline] pub fn is_first_letter(&self) -> bool { false } /// Whether the current pseudo element is :first-line #[inline] pub fn is_first_line(&self) -> bool { false } /// Whether this pseudo-element is the ::-moz-color-swatch pseudo. #[inline] pub fn is_color_swatch(&self) -> bool { false } /// Whether this pseudo-element is eagerly-cascaded. #[inline] pub fn is_eager(&self) -> bool { self.cascade_type() == PseudoElementCascadeType::Eager } /// Whether this pseudo-element is lazily-cascaded. #[inline] pub fn is_lazy(&self) -> bool { self.cascade_type() == PseudoElementCascadeType::Lazy } /// Whether this pseudo-element is for an anonymous box. pub fn is_anon_box(&self) -> bool { self.is_precomputed() } /// Whether this pseudo-element skips flex/grid container display-based /// fixup. #[inline] pub fn skip_item_display_fixup(&self) -> bool { !self.is_before_or_after() } /// Whether this pseudo-element is precomputed. #[inline] pub fn is_precomputed(&self) -> bool { self.cascade_type() == PseudoElementCascadeType::Precomputed } /// Returns which kind of cascade type has this pseudo. /// /// For more info on cascade types, see docs/components/style.md /// /// Note: Keep this in sync with EAGER_PSEUDO_COUNT. #[inline] pub fn cascade_type(&self) -> PseudoElementCascadeType { match *self { PseudoElement::After | PseudoElement::Before | PseudoElement::Selection => { PseudoElementCascadeType::Eager }, PseudoElement::DetailsSummary => PseudoElementCascadeType::Lazy, PseudoElement::DetailsContent | PseudoElement::ServoText | PseudoElement::ServoInputText | PseudoElement::ServoTableWrapper | PseudoElement::ServoAnonymousTableWrapper | PseudoElement::ServoAnonymousTable | PseudoElement::ServoAnonymousTableRow | PseudoElement::ServoAnonymousTableCell | PseudoElement::ServoAnonymousBlock | PseudoElement::ServoInlineBlockWrapper | PseudoElement::ServoInlineAbsolute => PseudoElementCascadeType::Precomputed, } } /// Covert non-canonical pseudo-element to canonical one, and keep a /// canonical one as it is. pub fn canonical(&self) -> PseudoElement { self.clone() } /// Stub, only Gecko needs this pub fn pseudo_info(&self) { () } /// Property flag that properties must have to apply to this pseudo-element. #[inline] pub fn property_restriction(&self) -> Option { None } /// Whether this pseudo-element should actually exist if it has /// the given styles. pub fn should_exist(&self, style: &ComputedValues) -> bool { let display = style.get_box().clone_display(); if display == Display::None { return false; } if self.is_before_or_after() && style.ineffective_content_property() { return false; } true } } /// The type used for storing `:lang` arguments. pub type Lang = Box; /// A non tree-structural pseudo-class. /// See https://drafts.csswg.org/selectors-4/#structural-pseudos #[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToShmem)] #[allow(missing_docs)] pub enum NonTSPseudoClass { Active, AnyLink, Checked, Defined, Disabled, Enabled, Focus, Fullscreen, Hover, Indeterminate, Lang(Lang), Link, PlaceholderShown, ReadWrite, ReadOnly, ServoNonZeroBorder, Target, Visited, } impl ::selectors::parser::NonTSPseudoClass for NonTSPseudoClass { type Impl = SelectorImpl; #[inline] fn is_active_or_hover(&self) -> bool { matches!(*self, NonTSPseudoClass::Active | NonTSPseudoClass::Hover) } #[inline] fn is_user_action_state(&self) -> bool { matches!( *self, NonTSPseudoClass::Active | NonTSPseudoClass::Hover | NonTSPseudoClass::Focus ) } fn visit(&self, _: &mut V) -> bool where V: SelectorVisitor, { true } } impl ToCss for NonTSPseudoClass { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { use self::NonTSPseudoClass::*; if let Lang(ref lang) = *self { dest.write_str(":lang(")?; serialize_identifier(lang, dest)?; return dest.write_char(')'); } dest.write_str(match *self { Active => ":active", AnyLink => ":any-link", Checked => ":checked", Defined => ":defined", Disabled => ":disabled", Enabled => ":enabled", Focus => ":focus", Fullscreen => ":fullscreen", Hover => ":hover", Indeterminate => ":indeterminate", Link => ":link", PlaceholderShown => ":placeholder-shown", ReadWrite => ":read-write", ReadOnly => ":read-only", ServoNonZeroBorder => ":-servo-nonzero-border", Target => ":target", Visited => ":visited", Lang(_) => unreachable!(), }) } } impl NonTSPseudoClass { /// Gets a given state flag for this pseudo-class. This is used to do /// selector matching, and it's set from the DOM. pub fn state_flag(&self) -> ElementState { use self::NonTSPseudoClass::*; match *self { Active => ElementState::IN_ACTIVE_STATE, Focus => ElementState::IN_FOCUS_STATE, Fullscreen => ElementState::IN_FULLSCREEN_STATE, Hover => ElementState::IN_HOVER_STATE, Defined => ElementState::IN_DEFINED_STATE, Enabled => ElementState::IN_ENABLED_STATE, Disabled => ElementState::IN_DISABLED_STATE, Checked => ElementState::IN_CHECKED_STATE, Indeterminate => ElementState::IN_INDETERMINATE_STATE, ReadOnly | ReadWrite => ElementState::IN_READWRITE_STATE, PlaceholderShown => ElementState::IN_PLACEHOLDER_SHOWN_STATE, Target => ElementState::IN_TARGET_STATE, AnyLink | Lang(_) | Link | Visited | ServoNonZeroBorder => ElementState::empty(), } } /// Get the document state flag associated with a pseudo-class, if any. pub fn document_state_flag(&self) -> DocumentState { DocumentState::empty() } /// Returns true if the given pseudoclass should trigger style sharing cache revalidation. pub fn needs_cache_revalidation(&self) -> bool { self.state_flag().is_empty() } } /// The abstract struct we implement the selector parser implementation on top /// of. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "servo", derive(MallocSizeOf))] pub struct SelectorImpl; impl ::selectors::SelectorImpl for SelectorImpl { type PseudoElement = PseudoElement; type NonTSPseudoClass = NonTSPseudoClass; type ExtraMatchingData = InvalidationMatchingData; type AttrValue = String; type Identifier = Atom; type ClassName = Atom; type PartName = Atom; type LocalName = LocalName; type NamespacePrefix = Prefix; type NamespaceUrl = Namespace; type BorrowedLocalName = LocalName; type BorrowedNamespaceUrl = Namespace; } impl<'a, 'i> ::selectors::Parser<'i> for SelectorParser<'a> { type Impl = SelectorImpl; type Error = StyleParseErrorKind<'i>; fn parse_non_ts_pseudo_class( &self, location: SourceLocation, name: CowRcStr<'i>, ) -> Result> { use self::NonTSPseudoClass::*; let pseudo_class = match_ignore_ascii_case! { &name, "active" => Active, "any-link" => AnyLink, "checked" => Checked, "defined" => Defined, "disabled" => Disabled, "enabled" => Enabled, "focus" => Focus, "fullscreen" => Fullscreen, "hover" => Hover, "indeterminate" => Indeterminate, "-moz-inert" => MozInert, "link" => Link, "placeholder-shown" => PlaceholderShown, "read-write" => ReadWrite, "read-only" => ReadOnly, "target" => Target, "visited" => Visited, "-servo-nonzero-border" => { if !self.in_user_agent_stylesheet() { return Err(location.new_custom_error( SelectorParseErrorKind::UnexpectedIdent("-servo-nonzero-border".into()) )) } ServoNonZeroBorder }, _ => return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))), }; Ok(pseudo_class) } fn parse_non_ts_functional_pseudo_class<'t>( &self, name: CowRcStr<'i>, parser: &mut CssParser<'i, 't>, ) -> Result> { use self::NonTSPseudoClass::*; let pseudo_class = match_ignore_ascii_case! { &name, "lang" => { Lang(parser.expect_ident_or_string()?.as_ref().into()) }, _ => return Err(parser.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))), }; Ok(pseudo_class) } fn parse_pseudo_element( &self, location: SourceLocation, name: CowRcStr<'i>, ) -> Result> { use self::PseudoElement::*; let pseudo_element = match_ignore_ascii_case! { &name, "before" => Before, "after" => After, "selection" => Selection, "-servo-details-summary" => { if !self.in_user_agent_stylesheet() { return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) } DetailsSummary }, "-servo-details-content" => { if !self.in_user_agent_stylesheet() { return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) } DetailsContent }, "-servo-text" => { if !self.in_user_agent_stylesheet() { return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) } ServoText }, "-servo-input-text" => { if !self.in_user_agent_stylesheet() { return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) } ServoInputText }, "-servo-table-wrapper" => { if !self.in_user_agent_stylesheet() { return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) } ServoTableWrapper }, "-servo-anonymous-table-wrapper" => { if !self.in_user_agent_stylesheet() { return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) } ServoAnonymousTableWrapper }, "-servo-anonymous-table" => { if !self.in_user_agent_stylesheet() { return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) } ServoAnonymousTable }, "-servo-anonymous-table-row" => { if !self.in_user_agent_stylesheet() { return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) } ServoAnonymousTableRow }, "-servo-anonymous-table-cell" => { if !self.in_user_agent_stylesheet() { return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) } ServoAnonymousTableCell }, "-servo-anonymous-block" => { if !self.in_user_agent_stylesheet() { return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) } ServoAnonymousBlock }, "-servo-inline-block-wrapper" => { if !self.in_user_agent_stylesheet() { return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) } ServoInlineBlockWrapper }, "-servo-inline-absolute" => { if !self.in_user_agent_stylesheet() { return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) } ServoInlineAbsolute }, _ => return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) }; Ok(pseudo_element) } fn default_namespace(&self) -> Option { self.namespaces.default.as_ref().map(|ns| ns.clone()) } fn namespace_for_prefix(&self, prefix: &Prefix) -> Option { self.namespaces.prefixes.get(prefix).cloned() } } impl SelectorImpl { /// A helper to traverse each eagerly cascaded pseudo-element, executing /// `fun` on it. #[inline] pub fn each_eagerly_cascaded_pseudo_element(mut fun: F) where F: FnMut(PseudoElement), { for i in 0..EAGER_PSEUDO_COUNT { fun(PseudoElement::from_eager_index(i)); } } } /// A map from elements to snapshots for the Servo style back-end. #[derive(Debug)] pub struct SnapshotMap(FxHashMap); impl SnapshotMap { /// Create a new empty `SnapshotMap`. pub fn new() -> Self { SnapshotMap(FxHashMap::default()) } /// Get a snapshot given an element. pub fn get(&self, el: &T) -> Option<&ServoElementSnapshot> { self.0.get(&el.as_node().opaque()) } } impl Deref for SnapshotMap { type Target = FxHashMap; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for SnapshotMap { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } /// Servo's version of an element snapshot. #[derive(Debug, Default, MallocSizeOf)] pub struct ServoElementSnapshot { /// The stored state of the element. pub state: Option, /// The set of stored attributes and its values. pub attrs: Option>, /// The set of changed attributes and its values. pub changed_attrs: Vec, /// Whether the class attribute changed or not. pub class_changed: bool, /// Whether the id attribute changed or not. pub id_changed: bool, /// Whether other attributes other than id or class changed or not. pub other_attributes_changed: bool, } impl ServoElementSnapshot { /// Create an empty element snapshot. pub fn new() -> Self { Self::default() } /// Returns whether the id attribute changed or not. pub fn id_changed(&self) -> bool { self.id_changed } /// Returns whether the class attribute changed or not. pub fn class_changed(&self) -> bool { self.class_changed } /// Returns whether other attributes other than id or class changed or not. pub fn other_attr_changed(&self) -> bool { self.other_attributes_changed } fn get_attr(&self, namespace: &Namespace, name: &LocalName) -> Option<&AttrValue> { self.attrs .as_ref() .unwrap() .iter() .find(|&&(ref ident, _)| ident.local_name == *name && ident.namespace == *namespace) .map(|&(_, ref v)| v) } /// Executes the callback once for each attribute that changed. #[inline] pub fn each_attr_changed(&self, mut callback: F) where F: FnMut(&LocalName), { for name in &self.changed_attrs { callback(name) } } fn any_attr_ignore_ns(&self, name: &LocalName, mut f: F) -> bool where F: FnMut(&AttrValue) -> bool, { self.attrs .as_ref() .unwrap() .iter() .any(|&(ref ident, ref v)| ident.local_name == *name && f(v)) } } impl ElementSnapshot for ServoElementSnapshot { fn state(&self) -> Option { self.state.clone() } fn has_attrs(&self) -> bool { self.attrs.is_some() } fn id_attr(&self) -> Option<&Atom> { self.get_attr(&ns!(), &local_name!("id")) .map(|v| v.as_atom()) } fn is_part(&self, _name: &AtomIdent) -> bool { false } fn imported_part(&self, _: &AtomIdent) -> Option { None } fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { self.get_attr(&ns!(), &local_name!("class")) .map_or(false, |v| { v.as_tokens() .iter() .any(|atom| case_sensitivity.eq_atom(atom, name)) }) } fn each_class(&self, mut callback: F) where F: FnMut(&AtomIdent), { if let Some(v) = self.get_attr(&ns!(), &local_name!("class")) { for class in v.as_tokens() { callback(AtomIdent::cast(class)); } } } fn lang_attr(&self) -> Option { self.get_attr(&ns!(xml), &local_name!("lang")) .or_else(|| self.get_attr(&ns!(), &local_name!("lang"))) .map(|v| SelectorAttrValue::from(v as &str)) } } impl ServoElementSnapshot { /// selectors::Element::attr_matches pub fn attr_matches( &self, ns: &NamespaceConstraint<&Namespace>, local_name: &LocalName, operation: &AttrSelectorOperation<&AtomString>, ) -> bool { match *ns { NamespaceConstraint::Specific(ref ns) => self .get_attr(ns, local_name) .map_or(false, |value| value.eval_selector(operation)), NamespaceConstraint::Any => { self.any_attr_ignore_ns(local_name, |value| value.eval_selector(operation)) }, } } } /// Returns whether the language is matched, as defined by /// [RFC 4647](https://tools.ietf.org/html/rfc4647#section-3.3.2). pub fn extended_filtering(tag: &str, range: &str) -> bool { range.split(',').any(|lang_range| { // step 1 let mut range_subtags = lang_range.split('\x2d'); let mut tag_subtags = tag.split('\x2d'); // step 2 // Note: [Level-4 spec](https://drafts.csswg.org/selectors/#lang-pseudo) check for wild card if let (Some(range_subtag), Some(tag_subtag)) = (range_subtags.next(), tag_subtags.next()) { if !(range_subtag.eq_ignore_ascii_case(tag_subtag) || range_subtag.eq_ignore_ascii_case("*")) { return false; } } let mut current_tag_subtag = tag_subtags.next(); // step 3 for range_subtag in range_subtags { // step 3a if range_subtag == "*" { continue; } match current_tag_subtag.clone() { Some(tag_subtag) => { // step 3c if range_subtag.eq_ignore_ascii_case(tag_subtag) { current_tag_subtag = tag_subtags.next(); continue; } // step 3d if tag_subtag.len() == 1 { return false; } // else step 3e - continue with loop current_tag_subtag = tag_subtags.next(); if current_tag_subtag.is_none() { return false; } }, // step 3b None => { return false; }, } } // step 4 true }) }