tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

pseudo_element.rs (20864B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
      4 
      5 //! Gecko's definition of a pseudo-element.
      6 //!
      7 //! Note that a few autogenerated bits of this live in
      8 //! `pseudo_element_definition.mako.rs`. If you touch that file, you probably
      9 //! need to update the checked-in files for Servo.
     10 
     11 use crate::gecko_bindings::structs::{self, PseudoStyleType};
     12 use crate::properties::longhands::display::computed_value::T as Display;
     13 use crate::properties::{ComputedValues, PropertyFlags};
     14 use crate::selector_parser::{PseudoElementCascadeType, SelectorImpl};
     15 use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase};
     16 use crate::string_cache::Atom;
     17 use crate::values::serialize_atom_identifier;
     18 use crate::values::AtomIdent;
     19 use cssparser::{Parser, ToCss};
     20 use selectors::parser::PseudoElement as PseudoElementTrait;
     21 use static_prefs::pref;
     22 use std::fmt;
     23 use style_traits::ParseError;
     24 
     25 include!(concat!(
     26    env!("OUT_DIR"),
     27    "/gecko/pseudo_element_definition.rs"
     28 ));
     29 
     30 /// The target we are using for parsing pseudo-elements.
     31 pub enum Target {
     32    /// When parsing a selector, we want to use the full syntax.
     33    Selector,
     34    /// When parsing the pseudo-element string (from CSSOM), we only accept CusomIdent for named
     35    /// view transition pseudo-elements.
     36    Cssom,
     37 }
     38 
     39 /// The type to hold the value of `<pt-name-and-class-selector>`.
     40 ///
     41 /// `<pt-name-and-class-selector> = <pt-name-selector> <pt-class-selector>? | <pt-class-selector>`
     42 /// `<pt-name-selector> = '*' | <custom-ident>`
     43 /// `<pt-class-selector> = ['.' <custom-ident>]+`
     44 ///
     45 /// This type should have at least one element.
     46 /// If there is no <pt-name-selector>, the first element would be the universal symbol, i.e. '*'.
     47 /// In other words, when we match it, ".abc" is the same as "*.abc".
     48 /// Note that we also serialize ".abc" as "*.abc".
     49 ///
     50 /// We use a single ThinVec<> to represent this structure to avoid allocating too much memory for a
     51 /// single selectors::parser::Component (max: 24 bytes) and PseudoElement (max: 16 bytes).
     52 ///
     53 /// https://drafts.csswg.org/css-view-transitions-2/#typedef-pt-name-and-class-selector
     54 #[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToShmem)]
     55 pub struct PtNameAndClassSelector(thin_vec::ThinVec<Atom>);
     56 
     57 impl PtNameAndClassSelector {
     58    /// Constructs a new one from a name.
     59    pub fn from_name(name: Atom) -> Self {
     60        Self(thin_vec::thin_vec![name])
     61    }
     62 
     63    /// Returns the name component.
     64    pub fn name(&self) -> &Atom {
     65        debug_assert!(!self.0.is_empty());
     66        self.0.first().expect("Shouldn't be empty")
     67    }
     68 
     69    /// Returns the classes component.
     70    pub fn classes(&self) -> &[Atom] {
     71        debug_assert!(!self.0.is_empty());
     72        &self.0[1..]
     73    }
     74 
     75    /// Returns the vector we store.
     76    pub fn name_and_classes(&self) -> &thin_vec::ThinVec<Atom> {
     77        &self.0
     78    }
     79 
     80    /// Parse the pseudo-element tree name and/or class.
     81    /// |for_selector| is true if we are parsing the CSS selectors and so need to check the
     82    /// universal symbol, i.e. '*', and classes.
     83    // Note: We share the same type for both pseudo-element and pseudo-element selector. The
     84    // universal symbol (i.e. '*') and `<pt-class-selector>` are used only in the selector (for
     85    // matching).
     86    pub fn parse<'i, 't>(
     87        input: &mut Parser<'i, 't>,
     88        target: Target,
     89    ) -> Result<Self, ParseError<'i>> {
     90        use crate::values::CustomIdent;
     91        use cssparser::Token;
     92        use style_traits::StyleParseErrorKind;
     93 
     94        // <pt-name-selector> = '*' | <custom-ident>
     95        let parse_pt_name = |input: &mut Parser<'i, '_>| {
     96            // For pseudo-element string, we don't accept '*'.
     97            if matches!(target, Target::Selector)
     98                && input.try_parse(|i| i.expect_delim('*')).is_ok()
     99            {
    100                Ok(atom!("*"))
    101            } else {
    102                CustomIdent::parse(input, &[]).map(|c| c.0)
    103            }
    104        };
    105        let name = input.try_parse(parse_pt_name);
    106 
    107        // Skip <pt-class-selector> for pseudo-element string.
    108        if matches!(target, Target::Cssom) {
    109            return name.map(Self::from_name);
    110        }
    111 
    112        // <pt-class-selector> = ['.' <custom-ident>]+
    113        let parse_pt_class = |input: &mut Parser<'i, '_>| {
    114            // The white space is forbidden:
    115            // 1. Between <pt-name-selector> and <pt-class-selector>
    116            // 2. Between any of the components of <pt-class-selector>.
    117            let location = input.current_source_location();
    118            match input.next_including_whitespace()? {
    119                Token::Delim('.') => (),
    120                t => return Err(location.new_unexpected_token_error(t.clone())),
    121            }
    122            // Whitespace is not allowed between '.' and the class name.
    123            if let Ok(token) = input.try_parse(|i| i.expect_whitespace()) {
    124                return Err(input.new_unexpected_token_error(Token::WhiteSpace(token)));
    125            }
    126            CustomIdent::parse(input, &[]).map(|c| c.0)
    127        };
    128        // If there is no `<pt-name-selector>`, it's fine to have whitespaces before the first '.'.
    129        if name.is_err() {
    130            input.skip_whitespace();
    131        }
    132        let mut classes = thin_vec::ThinVec::new();
    133        while let Ok(class) = input.try_parse(parse_pt_class) {
    134            classes.push(class);
    135        }
    136 
    137        // If we don't have `<pt-name-selector>`, we must have `<pt-class-selector>`, per the
    138        // syntax: `<pt-name-selector> <pt-class-selector>? | <pt-class-selector>`.
    139        if name.is_err() && classes.is_empty() {
    140            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
    141        }
    142 
    143        // Use the universal symbol as the first element to present the part of
    144        // `<pt-name-selector>` because they are equivalent (and the serialization is the same).
    145        let mut result = thin_vec::thin_vec![name.unwrap_or(atom!("*"))];
    146        result.append(&mut classes);
    147 
    148        Ok(Self(result))
    149    }
    150 }
    151 
    152 impl ToCss for PtNameAndClassSelector {
    153    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
    154    where
    155        W: fmt::Write,
    156    {
    157        let name = self.name();
    158        if name == &atom!("*") {
    159            // serialize_atom_identifier() may serialize "*" as "\*", so we handle it separately.
    160            dest.write_char('*')?;
    161        } else {
    162            serialize_atom_identifier(name, dest)?;
    163        }
    164 
    165        for class in self.classes() {
    166            dest.write_char('.')?;
    167            serialize_atom_identifier(class, dest)?;
    168        }
    169 
    170        Ok(())
    171    }
    172 }
    173 
    174 impl PseudoElementTrait for PseudoElement {
    175    type Impl = SelectorImpl;
    176 
    177    // ::slotted() should support all tree-abiding pseudo-elements, see
    178    // https://drafts.csswg.org/css-scoping/#slotted-pseudo
    179    // https://drafts.csswg.org/css-pseudo-4/#treelike
    180    #[inline]
    181    fn valid_after_slotted(&self) -> bool {
    182        matches!(
    183            *self,
    184            Self::Before
    185                | Self::After
    186                | Self::Marker
    187                | Self::Placeholder
    188                | Self::FileSelectorButton
    189                | Self::DetailsContent
    190        )
    191    }
    192 
    193    // ::before/::after should support ::marker, but no others.
    194    // https://drafts.csswg.org/css-pseudo-4/#marker-pseudo
    195    #[inline]
    196    fn valid_after_before_or_after(&self) -> bool {
    197        matches!(*self, Self::Marker)
    198    }
    199 
    200    #[inline]
    201    fn accepts_state_pseudo_classes(&self) -> bool {
    202        // Note: if the pseudo element is a descendants of a pseudo element, `only-child` should be
    203        // allowed after it.
    204        self.supports_user_action_state() || self.is_in_pseudo_element_tree()
    205    }
    206 
    207    #[inline]
    208    fn specificity_count(&self) -> u32 {
    209        self.specificity_count()
    210    }
    211 
    212    #[inline]
    213    fn is_in_pseudo_element_tree(&self) -> bool {
    214        // All the named view transition pseudo-elements are the descendants of a pseudo-element
    215        // root.
    216        self.is_named_view_transition()
    217    }
    218 
    219    /// Whether this pseudo-element is "element-backed", which means that it inherits from its regular
    220    /// flat tree parent, which might not be the originating element.
    221    #[inline]
    222    fn is_element_backed(&self) -> bool {
    223        // Note: We don't include ::view-transition here because it inherits from the originating
    224        // element, instead of the snapshot containing block.
    225        self.is_named_view_transition() || *self == PseudoElement::DetailsContent
    226    }
    227 
    228    /// Whether the current pseudo element is ::before or ::after.
    229    #[inline]
    230    fn is_before_or_after(&self) -> bool {
    231        matches!(*self, PseudoElement::Before | PseudoElement::After)
    232    }
    233 }
    234 
    235 impl PseudoElement {
    236    /// Returns the kind of cascade type that a given pseudo is going to use.
    237    ///
    238    /// In Gecko we only compute ::before and ::after eagerly. We save the rules
    239    /// for anonymous boxes separately, so we resolve them as precomputed
    240    /// pseudos.
    241    ///
    242    /// We resolve the others lazily, see `Servo_ResolvePseudoStyle`.
    243    pub fn cascade_type(&self) -> PseudoElementCascadeType {
    244        if self.is_eager() {
    245            debug_assert!(!self.is_anon_box());
    246            return PseudoElementCascadeType::Eager;
    247        }
    248 
    249        if self.is_precomputed() {
    250            return PseudoElementCascadeType::Precomputed;
    251        }
    252 
    253        PseudoElementCascadeType::Lazy
    254    }
    255 
    256    /// Gets the canonical index of this eagerly-cascaded pseudo-element.
    257    #[inline]
    258    pub fn eager_index(&self) -> usize {
    259        EAGER_PSEUDOS
    260            .iter()
    261            .position(|p| p == self)
    262            .expect("Not an eager pseudo")
    263    }
    264 
    265    /// Creates a pseudo-element from an eager index.
    266    #[inline]
    267    pub fn from_eager_index(i: usize) -> Self {
    268        EAGER_PSEUDOS[i].clone()
    269    }
    270 
    271    /// Whether animations for the current pseudo element are stored in the
    272    /// parent element.
    273    #[inline]
    274    pub fn animations_stored_in_parent(&self) -> bool {
    275        matches!(*self, Self::Before | Self::After | Self::Marker | Self::Backdrop)
    276    }
    277 
    278    /// Whether this pseudo-element is the ::before pseudo.
    279    #[inline]
    280    pub fn is_before(&self) -> bool {
    281        *self == PseudoElement::Before
    282    }
    283 
    284    /// Whether this pseudo-element is the ::after pseudo.
    285    #[inline]
    286    pub fn is_after(&self) -> bool {
    287        *self == PseudoElement::After
    288    }
    289 
    290    /// Whether this pseudo-element is the ::marker pseudo.
    291    #[inline]
    292    pub fn is_marker(&self) -> bool {
    293        *self == PseudoElement::Marker
    294    }
    295 
    296    /// Whether this pseudo-element is the ::selection pseudo.
    297    #[inline]
    298    pub fn is_selection(&self) -> bool {
    299        *self == PseudoElement::Selection
    300    }
    301 
    302    /// Whether this pseudo-element is ::first-letter.
    303    #[inline]
    304    pub fn is_first_letter(&self) -> bool {
    305        *self == PseudoElement::FirstLetter
    306    }
    307 
    308    /// Whether this pseudo-element is ::first-line.
    309    #[inline]
    310    pub fn is_first_line(&self) -> bool {
    311        *self == PseudoElement::FirstLine
    312    }
    313 
    314    /// Whether this pseudo-element is the ::-moz-color-swatch pseudo.
    315    #[inline]
    316    pub fn is_color_swatch(&self) -> bool {
    317        *self == PseudoElement::MozColorSwatch
    318    }
    319 
    320    /// Whether this pseudo-element is lazily-cascaded.
    321    #[inline]
    322    pub fn is_lazy(&self) -> bool {
    323        !self.is_eager() && !self.is_precomputed()
    324    }
    325 
    326    /// The identifier of the highlight this pseudo-element represents.
    327    pub fn highlight_name(&self) -> Option<&AtomIdent> {
    328        match *self {
    329            Self::Highlight(ref name) => Some(name),
    330            _ => None,
    331        }
    332    }
    333 
    334    /// Whether this pseudo-element is the ::highlight pseudo.
    335    pub fn is_highlight(&self) -> bool {
    336        matches!(*self, Self::Highlight(_))
    337    }
    338 
    339    /// Whether this pseudo-element is the ::target-text pseudo.
    340    #[inline]
    341    pub fn is_target_text(&self) -> bool {
    342        *self == PseudoElement::TargetText
    343    }
    344 
    345    /// Whether this pseudo-element is a named view transition pseudo-element.
    346    pub fn is_named_view_transition(&self) -> bool {
    347        matches!(
    348            *self,
    349            Self::ViewTransitionGroup(..)
    350                | Self::ViewTransitionImagePair(..)
    351                | Self::ViewTransitionOld(..)
    352                | Self::ViewTransitionNew(..)
    353        )
    354    }
    355 
    356    /// The count we contribute to the specificity from this pseudo-element.
    357    pub fn specificity_count(&self) -> u32 {
    358        match *self {
    359            Self::ViewTransitionGroup(ref name_and_class)
    360            | Self::ViewTransitionImagePair(ref name_and_class)
    361            | Self::ViewTransitionOld(ref name_and_class)
    362            | Self::ViewTransitionNew(ref name_and_class) => {
    363                // The specificity of a named view transition pseudo-element selector with either:
    364                // 1. a <pt-name-selector> with a <custom-ident>; or
    365                // 2. a <pt-class-selector> with at least one <custom-ident>,
    366                // is equivalent to a type selector.
    367                //
    368                // The specificity of a named view transition pseudo-element selector with a `*`
    369                // argument and with an empty <pt-class-selector> is zero.
    370                // https://drafts.csswg.org/css-view-transitions-2/#pseudo-element-class-additions
    371                (name_and_class.name() != &atom!("*") || !name_and_class.classes().is_empty())
    372                    as u32
    373            },
    374            _ => 1,
    375        }
    376    }
    377 
    378    /// Whether this pseudo-element supports user action selectors.
    379    pub fn supports_user_action_state(&self) -> bool {
    380        (self.flags() & structs::CSS_PSEUDO_ELEMENT_SUPPORTS_USER_ACTION_STATE) != 0
    381    }
    382 
    383    /// Whether this pseudo-element is enabled for all content.
    384    pub fn enabled_in_content(&self) -> bool {
    385        match *self {
    386            Self::Highlight(..) => pref!("dom.customHighlightAPI.enabled"),
    387            Self::TargetText => pref!("dom.text_fragments.enabled"),
    388            Self::SliderFill | Self::SliderTrack | Self::SliderThumb => {
    389                pref!("layout.css.modern-range-pseudos.enabled")
    390            },
    391            Self::DetailsContent => {
    392                pref!("layout.css.details-content.enabled")
    393            },
    394            Self::ViewTransition
    395            | Self::ViewTransitionGroup(..)
    396            | Self::ViewTransitionImagePair(..)
    397            | Self::ViewTransitionOld(..)
    398            | Self::ViewTransitionNew(..) => pref!("dom.viewTransitions.enabled"),
    399            // If it's not explicitly enabled in UA sheets or chrome, then we're enabled for
    400            // content.
    401            _ => (self.flags() & structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS_AND_CHROME) == 0,
    402        }
    403    }
    404 
    405    /// Whether this pseudo is enabled explicitly in UA sheets.
    406    pub fn enabled_in_ua_sheets(&self) -> bool {
    407        (self.flags() & structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS) != 0
    408    }
    409 
    410    /// Whether this pseudo is enabled explicitly in chrome sheets.
    411    pub fn enabled_in_chrome(&self) -> bool {
    412        (self.flags() & structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_CHROME) != 0
    413    }
    414 
    415    /// Whether this pseudo-element skips flex/grid container display-based
    416    /// fixup.
    417    #[inline]
    418    pub fn skip_item_display_fixup(&self) -> bool {
    419        (self.flags() & structs::CSS_PSEUDO_ELEMENT_IS_FLEX_OR_GRID_ITEM) == 0
    420    }
    421 
    422    /// Whether this pseudo-element is precomputed.
    423    #[inline]
    424    pub fn is_precomputed(&self) -> bool {
    425        self.is_anon_box() && !self.is_tree_pseudo_element()
    426    }
    427 
    428    /// Property flag that properties must have to apply to this pseudo-element.
    429    #[inline]
    430    pub fn property_restriction(&self) -> Option<PropertyFlags> {
    431        Some(match *self {
    432            PseudoElement::FirstLetter => PropertyFlags::APPLIES_TO_FIRST_LETTER,
    433            PseudoElement::FirstLine => PropertyFlags::APPLIES_TO_FIRST_LINE,
    434            PseudoElement::Placeholder => PropertyFlags::APPLIES_TO_PLACEHOLDER,
    435            PseudoElement::Cue => PropertyFlags::APPLIES_TO_CUE,
    436            PseudoElement::Marker if static_prefs::pref!("layout.css.marker.restricted") => {
    437                PropertyFlags::APPLIES_TO_MARKER
    438            },
    439            _ => return None,
    440        })
    441    }
    442 
    443    /// Whether this pseudo-element should actually exist if it has
    444    /// the given styles.
    445    pub fn should_exist(&self, style: &ComputedValues) -> bool {
    446        debug_assert!(self.is_eager());
    447 
    448        if style.get_box().clone_display() == Display::None {
    449            return false;
    450        }
    451 
    452        if self.is_before_or_after() && style.ineffective_content_property() {
    453            return false;
    454        }
    455 
    456        true
    457    }
    458 
    459    /// Parse the pseudo-element string without the check of enabled state. This may includes
    460    /// all possible PseudoElement, including tree pseudo-elements and anonymous box.
    461    // TODO: Bug 1845712. Merge this with the pseudo element part in parse_one_simple_selector().
    462    pub fn parse_ignore_enabled_state<'i, 't>(
    463        input: &mut Parser<'i, 't>,
    464    ) -> Result<Self, ParseError<'i>> {
    465        use crate::gecko::selector_parser;
    466        use cssparser::Token;
    467        use selectors::parser::{is_css2_pseudo_element, SelectorParseErrorKind};
    468        use style_traits::StyleParseErrorKind;
    469 
    470        // The pseudo-element string should start with ':'.
    471        input.expect_colon()?;
    472 
    473        let location = input.current_source_location();
    474        let next = input.next_including_whitespace()?;
    475        if !matches!(next, Token::Colon) {
    476            // Parse a CSS2 pseudo-element.
    477            let name = match next {
    478                Token::Ident(name) if is_css2_pseudo_element(&name) => name,
    479                _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
    480            };
    481            return PseudoElement::from_slice(&name, false).ok_or(location.new_custom_error(
    482                SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name.clone()),
    483            ));
    484        }
    485 
    486        // Now we have double colons, so check the following tokens.
    487        match input.next_including_whitespace()?.clone() {
    488            Token::Ident(name) => {
    489                // We don't need to parse unknown ::-webkit-* pseudo-elements in this function.
    490                PseudoElement::from_slice(&name, false).ok_or(input.new_custom_error(
    491                    SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name),
    492                ))
    493            },
    494            Token::Function(name) => {
    495                // Note: ::slotted() and ::part() are not accepted in getComputedStyle().
    496                // https://drafts.csswg.org/cssom/#dom-window-getcomputedstyle
    497                input.parse_nested_block(|input| {
    498                    selector_parser::parse_functional_pseudo_element_with_name(
    499                        name,
    500                        input,
    501                        Target::Cssom,
    502                    )
    503                })
    504            },
    505            t => return Err(input.new_unexpected_token_error(t)),
    506        }
    507    }
    508 
    509    /// Returns true if this pseudo-element matches its selector.
    510    pub fn matches_named_view_transition_pseudo_element(
    511        &self,
    512        selector: &Self,
    513        element: &super::wrapper::GeckoElement,
    514    ) -> bool {
    515        use crate::gecko_bindings::bindings;
    516 
    517        match (self, selector) {
    518            (
    519                &Self::ViewTransitionGroup(ref name),
    520                &Self::ViewTransitionGroup(ref s_name_class),
    521            )
    522            | (
    523                &Self::ViewTransitionImagePair(ref name),
    524                &Self::ViewTransitionImagePair(ref s_name_class),
    525            )
    526            | (&Self::ViewTransitionOld(ref name), &Self::ViewTransitionOld(ref s_name_class))
    527            | (&Self::ViewTransitionNew(ref name), &Self::ViewTransitionNew(ref s_name_class)) => {
    528                // Named view transition pseudos accept the universal selector as the name, so we
    529                // check it first.
    530                // https://drafts.csswg.org/css-view-transitions-1/#named-view-transition-pseudo
    531                let s_name = s_name_class.name();
    532                if s_name != name.name() && s_name != &atom!("*") {
    533                    return false;
    534                }
    535 
    536                // We have to check class list only when the name is matched and there are one or
    537                // more <pt-class-selector>s.
    538                s_name_class.classes().is_empty()
    539                    || unsafe {
    540                        bindings::Gecko_MatchViewTransitionClass(
    541                            element.0,
    542                            s_name_class.name_and_classes(),
    543                        )
    544                    }
    545            },
    546            _ => false,
    547        }
    548    }
    549 }