/* 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/. */ //! Gecko's definition of a pseudo-element. //! //! Note that a few autogenerated bits of this live in //! `pseudo_element_definition.mako.rs`. If you touch that file, you probably //! need to update the checked-in files for Servo. use crate::gecko_bindings::structs::{self, PseudoStyleType}; use crate::properties::longhands::display::computed_value::T as Display; use crate::properties::{ComputedValues, PropertyFlags}; use crate::selector_parser::{PseudoElementCascadeType, SelectorImpl}; use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase}; use crate::string_cache::Atom; use crate::values::serialize_atom_identifier; use crate::values::AtomIdent; use cssparser::{Parser, ToCss}; use selectors::parser::PseudoElement as PseudoElementTrait; use static_prefs::pref; use std::fmt; use style_traits::ParseError; include!(concat!( env!("OUT_DIR"), "/gecko/pseudo_element_definition.rs" )); /// The target we are using for parsing pseudo-elements. pub enum Target { /// When parsing a selector, we want to use the full syntax. Selector, /// When parsing the pseudo-element string (from CSSOM), we only accept CusomIdent for named /// view transition pseudo-elements. Cssom, } /// The type to hold the value of ``. /// /// ` = ? | ` /// ` = '*' | ` /// ` = ['.' ]+` /// /// This type should have at least one element. /// If there is no , the first element would be the universal symbol, i.e. '*'. /// In other words, when we match it, ".abc" is the same as "*.abc". /// Note that we also serialize ".abc" as "*.abc". /// /// We use a single ThinVec<> to represent this structure to avoid allocating too much memory for a /// single selectors::parser::Component (max: 24 bytes) and PseudoElement (max: 16 bytes). /// /// https://drafts.csswg.org/css-view-transitions-2/#typedef-pt-name-and-class-selector #[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToShmem)] pub struct PtNameAndClassSelector(thin_vec::ThinVec); impl PtNameAndClassSelector { /// Constructs a new one from a name. pub fn from_name(name: Atom) -> Self { Self(thin_vec![name]) } /// Returns the name component. pub fn name(&self) -> &Atom { debug_assert!(!self.0.is_empty()); self.0.first().expect("Shouldn't be empty") } /// Returns the classes component. pub fn classes(&self) -> &[Atom] { debug_assert!(!self.0.is_empty()); &self.0[1..] } /// Returns the vector we store. pub fn name_and_classes(&self) -> &thin_vec::ThinVec { &self.0 } /// Parse the pseudo-element tree name and/or class. /// |for_selector| is true if we are parsing the CSS selectors and so need to check the /// universal symbol, i.e. '*', and classes. // Note: We share the same type for both pseudo-element and pseudo-element selector. The // universal symbol (i.e. '*') and `` are used only in the selector (for // matching). pub fn parse<'i, 't>( input: &mut Parser<'i, 't>, target: Target, ) -> Result> { use crate::values::CustomIdent; use cssparser::Token; use style_traits::StyleParseErrorKind; // = '*' | let parse_pt_name = |input: &mut Parser<'i, '_>| { // For pseudo-element string, we don't accept '*'. if matches!(target, Target::Selector) && input.try_parse(|i| i.expect_delim('*')).is_ok() { Ok(atom!("*")) } else { CustomIdent::parse(input, &[]).map(|c| c.0) } }; let name = input.try_parse(parse_pt_name); // Skip for pseudo-element string. if matches!(target, Target::Cssom) { return name.map(Self::from_name); } // = ['.' ]+ let parse_pt_class = |input: &mut Parser<'i, '_>| { // The white space is forbidden: // 1. Between and // 2. Between any of the components of . let location = input.current_source_location(); match input.next_including_whitespace()? { Token::Delim('.') => (), t => return Err(location.new_unexpected_token_error(t.clone())), } // Whitespace is not allowed between '.' and the class name. if let Ok(token) = input.try_parse(|i| i.expect_whitespace()) { return Err(input.new_unexpected_token_error(Token::WhiteSpace(token))); } CustomIdent::parse(input, &[]).map(|c| c.0) }; // If there is no ``, it's fine to have whitespaces before the first '.'. if name.is_err() { input.skip_whitespace(); } let mut classes = thin_vec::ThinVec::new(); while let Ok(class) = input.try_parse(parse_pt_class) { classes.push(class); } // If we don't have ``, we must have ``, per the // syntax: ` ? | `. if name.is_err() && classes.is_empty() { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } // Use the universal symbol as the first element to present the part of // `` because they are equivalent (and the serialization is the same). let mut result = thin_vec![name.unwrap_or(atom!("*"))]; result.append(&mut classes); Ok(Self(result)) } } impl ToCss for PtNameAndClassSelector { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { let name = self.name(); if name == &atom!("*") { // serialize_atom_identifier() may serialize "*" as "\*", so we handle it separately. dest.write_char('*')?; } else { serialize_atom_identifier(name, dest)?; } for class in self.classes() { dest.write_char('.')?; serialize_atom_identifier(class, dest)?; } Ok(()) } } impl PseudoElementTrait for PseudoElement { type Impl = SelectorImpl; // ::slotted() should support all tree-abiding pseudo-elements, see // https://drafts.csswg.org/css-scoping/#slotted-pseudo // https://drafts.csswg.org/css-pseudo-4/#treelike #[inline] fn valid_after_slotted(&self) -> bool { matches!( *self, Self::Before | Self::After | Self::Marker | Self::Placeholder | Self::FileSelectorButton | Self::DetailsContent ) } // ::before/::after should support ::marker, but no others. // https://drafts.csswg.org/css-pseudo-4/#marker-pseudo #[inline] fn valid_after_before_or_after(&self) -> bool { matches!(*self, Self::Marker) } #[inline] fn accepts_state_pseudo_classes(&self) -> bool { // Note: if the pseudo element is a descendants of a pseudo element, `only-child` should be // allowed after it. self.supports_user_action_state() || self.is_in_pseudo_element_tree() } #[inline] fn specificity_count(&self) -> u32 { self.specificity_count() } #[inline] fn is_in_pseudo_element_tree(&self) -> bool { // All the named view transition pseudo-elements are the descendants of a pseudo-element // root. self.is_named_view_transition() } /// Whether this pseudo-element is "element-backed", which means that it inherits from its regular /// flat tree parent, which might not be the originating element. #[inline] fn is_element_backed(&self) -> bool { // Note: We don't include ::view-transition here because it inherits from the originating // element, instead of the snapshot containing block. self.is_named_view_transition() || *self == PseudoElement::DetailsContent } /// Whether the current pseudo element is ::before or ::after. #[inline] fn is_before_or_after(&self) -> bool { matches!(*self, PseudoElement::Before | PseudoElement::After) } } impl PseudoElement { /// Returns the kind of cascade type that a given pseudo is going to use. /// /// In Gecko we only compute ::before and ::after eagerly. We save the rules /// for anonymous boxes separately, so we resolve them as precomputed /// pseudos. /// /// We resolve the others lazily, see `Servo_ResolvePseudoStyle`. pub fn cascade_type(&self) -> PseudoElementCascadeType { if self.is_eager() { debug_assert!(!self.is_anon_box()); return PseudoElementCascadeType::Eager; } if self.is_precomputed() { return PseudoElementCascadeType::Precomputed; } PseudoElementCascadeType::Lazy } /// Gets the canonical index of this eagerly-cascaded pseudo-element. #[inline] pub fn eager_index(&self) -> usize { EAGER_PSEUDOS .iter() .position(|p| p == self) .expect("Not an eager pseudo") } /// Creates a pseudo-element from an eager index. #[inline] pub fn from_eager_index(i: usize) -> Self { EAGER_PSEUDOS[i].clone() } /// Whether animations for the current pseudo element are stored in the /// parent element. #[inline] pub fn animations_stored_in_parent(&self) -> bool { matches!(*self, Self::Before | Self::After | Self::Marker) } /// 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 this pseudo-element is the ::marker pseudo. #[inline] pub fn is_marker(&self) -> bool { *self == PseudoElement::Marker } /// Whether this pseudo-element is the ::selection pseudo. #[inline] pub fn is_selection(&self) -> bool { *self == PseudoElement::Selection } /// Whether this pseudo-element is ::first-letter. #[inline] pub fn is_first_letter(&self) -> bool { *self == PseudoElement::FirstLetter } /// Whether this pseudo-element is ::first-line. #[inline] pub fn is_first_line(&self) -> bool { *self == PseudoElement::FirstLine } /// Whether this pseudo-element is the ::-moz-color-swatch pseudo. #[inline] pub fn is_color_swatch(&self) -> bool { *self == PseudoElement::MozColorSwatch } /// Whether this pseudo-element is lazily-cascaded. #[inline] pub fn is_lazy(&self) -> bool { !self.is_eager() && !self.is_precomputed() } /// The identifier of the highlight this pseudo-element represents. pub fn highlight_name(&self) -> Option<&AtomIdent> { match *self { Self::Highlight(ref name) => Some(name), _ => None, } } /// Whether this pseudo-element is the ::highlight pseudo. pub fn is_highlight(&self) -> bool { matches!(*self, Self::Highlight(_)) } /// Whether this pseudo-element is the ::target-text pseudo. #[inline] pub fn is_target_text(&self) -> bool { *self == PseudoElement::TargetText } /// Whether this pseudo-element is a named view transition pseudo-element. pub fn is_named_view_transition(&self) -> bool { matches!( *self, Self::ViewTransitionGroup(..) | Self::ViewTransitionImagePair(..) | Self::ViewTransitionOld(..) | Self::ViewTransitionNew(..) ) } /// The count we contribute to the specificity from this pseudo-element. pub fn specificity_count(&self) -> u32 { match *self { Self::ViewTransitionGroup(ref name_and_class) | Self::ViewTransitionImagePair(ref name_and_class) | Self::ViewTransitionOld(ref name_and_class) | Self::ViewTransitionNew(ref name_and_class) => { // The specificity of a named view transition pseudo-element selector with either: // 1. a with a ; or // 2. a with at least one , // is equivalent to a type selector. // // The specificity of a named view transition pseudo-element selector with a `*` // argument and with an empty is zero. // https://drafts.csswg.org/css-view-transitions-2/#pseudo-element-class-additions (name_and_class.name() != &atom!("*") || !name_and_class.classes().is_empty()) as u32 }, _ => 1, } } /// Whether this pseudo-element supports user action selectors. pub fn supports_user_action_state(&self) -> bool { (self.flags() & structs::CSS_PSEUDO_ELEMENT_SUPPORTS_USER_ACTION_STATE) != 0 } /// Whether this pseudo-element is enabled for all content. pub fn enabled_in_content(&self) -> bool { match *self { Self::Highlight(..) => pref!("dom.customHighlightAPI.enabled"), Self::TargetText => pref!("dom.text_fragments.enabled"), Self::SliderFill | Self::SliderTrack | Self::SliderThumb => { pref!("layout.css.modern-range-pseudos.enabled") }, Self::DetailsContent => { pref!("layout.css.details-content.enabled") }, Self::ViewTransition | Self::ViewTransitionGroup(..) | Self::ViewTransitionImagePair(..) | Self::ViewTransitionOld(..) | Self::ViewTransitionNew(..) => pref!("dom.viewTransitions.enabled"), // If it's not explicitly enabled in UA sheets or chrome, then we're enabled for // content. _ => (self.flags() & structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS_AND_CHROME) == 0, } } /// Whether this pseudo is enabled explicitly in UA sheets. pub fn enabled_in_ua_sheets(&self) -> bool { (self.flags() & structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS) != 0 } /// Whether this pseudo is enabled explicitly in chrome sheets. pub fn enabled_in_chrome(&self) -> bool { (self.flags() & structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_CHROME) != 0 } /// Whether this pseudo-element skips flex/grid container display-based /// fixup. #[inline] pub fn skip_item_display_fixup(&self) -> bool { (self.flags() & structs::CSS_PSEUDO_ELEMENT_IS_FLEX_OR_GRID_ITEM) == 0 } /// Whether this pseudo-element is precomputed. #[inline] pub fn is_precomputed(&self) -> bool { self.is_anon_box() && !self.is_tree_pseudo_element() } /// Property flag that properties must have to apply to this pseudo-element. #[inline] pub fn property_restriction(&self) -> Option { Some(match *self { PseudoElement::FirstLetter => PropertyFlags::APPLIES_TO_FIRST_LETTER, PseudoElement::FirstLine => PropertyFlags::APPLIES_TO_FIRST_LINE, PseudoElement::Placeholder => PropertyFlags::APPLIES_TO_PLACEHOLDER, PseudoElement::Cue => PropertyFlags::APPLIES_TO_CUE, PseudoElement::Marker if static_prefs::pref!("layout.css.marker.restricted") => { PropertyFlags::APPLIES_TO_MARKER }, _ => return None, }) } /// Whether this pseudo-element should actually exist if it has /// the given styles. pub fn should_exist(&self, style: &ComputedValues) -> bool { debug_assert!(self.is_eager()); if style.get_box().clone_display() == Display::None { return false; } if self.is_before_or_after() && style.ineffective_content_property() { return false; } true } /// Parse the pseudo-element string without the check of enabled state. This may includes /// all possible PseudoElement, including tree pseudo-elements and anonymous box. // TODO: Bug 1845712. Merge this with the pseudo element part in parse_one_simple_selector(). pub fn parse_ignore_enabled_state<'i, 't>( input: &mut Parser<'i, 't>, ) -> Result> { use crate::gecko::selector_parser; use cssparser::Token; use selectors::parser::{is_css2_pseudo_element, SelectorParseErrorKind}; use style_traits::StyleParseErrorKind; // The pseudo-element string should start with ':'. input.expect_colon()?; let location = input.current_source_location(); let next = input.next_including_whitespace()?; if !matches!(next, Token::Colon) { // Parse a CSS2 pseudo-element. let name = match next { Token::Ident(name) if is_css2_pseudo_element(&name) => name, _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), }; return PseudoElement::from_slice(&name, false).ok_or(location.new_custom_error( SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name.clone()), )); } // Now we have double colons, so check the following tokens. match input.next_including_whitespace()?.clone() { Token::Ident(name) => { // We don't need to parse unknown ::-webkit-* pseudo-elements in this function. PseudoElement::from_slice(&name, false).ok_or(input.new_custom_error( SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name), )) }, Token::Function(name) => { // Note: ::slotted() and ::part() are not accepted in getComputedStyle(). // https://drafts.csswg.org/cssom/#dom-window-getcomputedstyle input.parse_nested_block(|input| { selector_parser::parse_functional_pseudo_element_with_name( name, input, Target::Cssom, ) }) }, t => return Err(input.new_unexpected_token_error(t)), } } /// Returns true if this pseudo-element matches its selector. pub fn matches_named_view_transition_pseudo_element( &self, selector: &Self, element: &super::wrapper::GeckoElement, ) -> bool { use crate::gecko_bindings::bindings; match (self, selector) { ( &Self::ViewTransitionGroup(ref name), &Self::ViewTransitionGroup(ref s_name_class), ) | ( &Self::ViewTransitionImagePair(ref name), &Self::ViewTransitionImagePair(ref s_name_class), ) | (&Self::ViewTransitionOld(ref name), &Self::ViewTransitionOld(ref s_name_class)) | (&Self::ViewTransitionNew(ref name), &Self::ViewTransitionNew(ref s_name_class)) => { // Named view transition pseudos accept the universal selector as the name, so we // check it first. // https://drafts.csswg.org/css-view-transitions-1/#named-view-transition-pseudo let s_name = s_name_class.name(); if s_name != name.name() && s_name != &atom!("*") { return false; } // We have to check class list only when the name is matched and there are one or // more s. s_name_class.classes().is_empty() || unsafe { bindings::Gecko_MatchViewTransitionClass( element.0, s_name_class.name_and_classes(), ) } }, _ => false, } } }