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 }