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-specific bits for selector-parsing. 6 7 use crate::element_state::{DocumentState, ElementState}; 8 use crate::gecko_bindings::structs::RawServoSelectorList; 9 use crate::gecko_bindings::sugar::ownership::{HasBoxFFI, HasFFI, HasSimpleFFI}; 10 use crate::invalidation::element::document_state::InvalidationMatchingData; 11 use crate::selector_parser::{Direction, SelectorParser}; 12 use crate::str::starts_with_ignore_ascii_case; 13 use crate::string_cache::{Atom, Namespace, WeakAtom, WeakNamespace}; 14 use crate::values::{AtomIdent, AtomString}; 15 use cssparser::{BasicParseError, BasicParseErrorKind, Parser}; 16 use cssparser::{CowRcStr, SourceLocation, ToCss, Token}; 17 use selectors::parser::{ParseErrorRecovery, SelectorParseErrorKind}; 18 use selectors::SelectorList; 19 use std::fmt; 20 use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss as ToCss_}; 21 22 pub use crate::gecko::pseudo_element::{ 23 PseudoElement, EAGER_PSEUDOS, EAGER_PSEUDO_COUNT, PSEUDO_COUNT, 24 }; 25 pub use crate::gecko::snapshot::SnapshotMap; 26 27 bitflags! { 28 // See NonTSPseudoClass::is_enabled_in() 29 struct NonTSPseudoClassFlag: u8 { 30 const PSEUDO_CLASS_ENABLED_IN_UA_SHEETS = 1 << 0; 31 const PSEUDO_CLASS_ENABLED_IN_CHROME = 1 << 1; 32 const PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME = 33 NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS.bits | 34 NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_CHROME.bits; 35 } 36 } 37 38 /// The type used to store the language argument to the `:lang` pseudo-class. 39 pub type Lang = AtomIdent; 40 41 macro_rules! pseudo_class_name { 42 ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { 43 /// Our representation of a non tree-structural pseudo-class. 44 #[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] 45 pub enum NonTSPseudoClass { 46 $( 47 #[doc = $css] 48 $name, 49 )* 50 /// The `:lang` pseudo-class. 51 Lang(Lang), 52 /// The `:dir` pseudo-class. 53 Dir(Direction), 54 /// The non-standard `:-moz-locale-dir` pseudo-class. 55 MozLocaleDir(Direction), 56 } 57 } 58 } 59 apply_non_ts_list!(pseudo_class_name); 60 61 impl ToCss for NonTSPseudoClass { to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write,62 fn to_css<W>(&self, dest: &mut W) -> fmt::Result 63 where 64 W: fmt::Write, 65 { 66 macro_rules! pseudo_class_serialize { 67 ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { 68 match *self { 69 $(NonTSPseudoClass::$name => concat!(":", $css),)* 70 NonTSPseudoClass::Lang(ref s) => { 71 dest.write_str(":lang(")?; 72 s.to_css(dest)?; 73 return dest.write_char(')'); 74 }, 75 NonTSPseudoClass::MozLocaleDir(ref dir) => { 76 dest.write_str(":-moz-locale-dir(")?; 77 dir.to_css(&mut CssWriter::new(dest))?; 78 return dest.write_char(')') 79 }, 80 NonTSPseudoClass::Dir(ref dir) => { 81 dest.write_str(":dir(")?; 82 dir.to_css(&mut CssWriter::new(dest))?; 83 return dest.write_char(')') 84 }, 85 } 86 } 87 } 88 let ser = apply_non_ts_list!(pseudo_class_serialize); 89 dest.write_str(ser) 90 } 91 } 92 93 impl NonTSPseudoClass { 94 /// Parses the name and returns a non-ts-pseudo-class if succeeds. 95 /// None otherwise. It doesn't check whether the pseudo-class is enabled 96 /// in a particular state. parse_non_functional(name: &str) -> Option<Self>97 pub fn parse_non_functional(name: &str) -> Option<Self> { 98 macro_rules! pseudo_class_parse { 99 ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { 100 match_ignore_ascii_case! { &name, 101 $($css => Some(NonTSPseudoClass::$name),)* 102 "-moz-full-screen" => Some(NonTSPseudoClass::Fullscreen), 103 "-moz-read-only" => Some(NonTSPseudoClass::ReadOnly), 104 "-moz-read-write" => Some(NonTSPseudoClass::ReadWrite), 105 "-moz-focusring" => Some(NonTSPseudoClass::FocusVisible), 106 "-moz-ui-valid" => Some(NonTSPseudoClass::UserValid), 107 "-moz-ui-invalid" => Some(NonTSPseudoClass::UserInvalid), 108 "-webkit-autofill" => Some(NonTSPseudoClass::Autofill), 109 _ => None, 110 } 111 } 112 } 113 apply_non_ts_list!(pseudo_class_parse) 114 } 115 116 /// Returns true if this pseudo-class has any of the given flags set. has_any_flag(&self, flags: NonTSPseudoClassFlag) -> bool117 fn has_any_flag(&self, flags: NonTSPseudoClassFlag) -> bool { 118 macro_rules! check_flag { 119 (_) => { 120 false 121 }; 122 ($flags:ident) => { 123 NonTSPseudoClassFlag::$flags.intersects(flags) 124 }; 125 } 126 macro_rules! pseudo_class_check_is_enabled_in { 127 ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { 128 match *self { 129 $(NonTSPseudoClass::$name => check_flag!($flags),)* 130 NonTSPseudoClass::MozLocaleDir(_) | 131 NonTSPseudoClass::Lang(_) | 132 NonTSPseudoClass::Dir(_) => false, 133 } 134 } 135 } 136 apply_non_ts_list!(pseudo_class_check_is_enabled_in) 137 } 138 139 /// Returns whether the pseudo-class is enabled in content sheets. 140 #[inline] is_enabled_in_content(&self) -> bool141 fn is_enabled_in_content(&self) -> bool { 142 if let NonTSPseudoClass::Autofill = *self { 143 return static_prefs::pref!("layout.css.autofill.enabled"); 144 } 145 if let NonTSPseudoClass::MozSubmitInvalid = *self { 146 return static_prefs::pref!("layout.css.moz-submit-invalid.enabled"); 147 } 148 !self.has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME) 149 } 150 151 /// Get the state flag associated with a pseudo-class, if any. state_flag(&self) -> ElementState152 pub fn state_flag(&self) -> ElementState { 153 macro_rules! flag { 154 (_) => { 155 ElementState::empty() 156 }; 157 ($state:ident) => { 158 ElementState::$state 159 }; 160 } 161 macro_rules! pseudo_class_state { 162 ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { 163 match *self { 164 $(NonTSPseudoClass::$name => flag!($state),)* 165 NonTSPseudoClass::Dir(..) | 166 NonTSPseudoClass::MozLocaleDir(..) | 167 NonTSPseudoClass::Lang(..) => ElementState::empty(), 168 } 169 } 170 } 171 apply_non_ts_list!(pseudo_class_state) 172 } 173 174 /// Get the document state flag associated with a pseudo-class, if any. document_state_flag(&self) -> DocumentState175 pub fn document_state_flag(&self) -> DocumentState { 176 match *self { 177 NonTSPseudoClass::MozLocaleDir(..) => DocumentState::NS_DOCUMENT_STATE_RTL_LOCALE, 178 NonTSPseudoClass::MozWindowInactive => DocumentState::NS_DOCUMENT_STATE_WINDOW_INACTIVE, 179 _ => DocumentState::empty(), 180 } 181 } 182 183 /// Returns true if the given pseudoclass should trigger style sharing cache 184 /// revalidation. needs_cache_revalidation(&self) -> bool185 pub fn needs_cache_revalidation(&self) -> bool { 186 self.state_flag().is_empty() && 187 !matches!( 188 *self, 189 // :dir() depends on state only, but doesn't use state_flag 190 // because its semantics don't quite match. Nevertheless, it 191 // doesn't need cache revalidation, because we already compare 192 // states for elements and candidates. 193 NonTSPseudoClass::Dir(_) | 194 // :-moz-is-html only depends on the state of the document and 195 // the namespace of the element; the former is invariant 196 // across all the elements involved and the latter is already 197 // checked for by our caching precondtions. 198 NonTSPseudoClass::MozIsHTML | 199 // We prevent style sharing for NAC. 200 NonTSPseudoClass::MozNativeAnonymous | 201 // :-moz-placeholder is parsed but never matches. 202 NonTSPseudoClass::MozPlaceholder | 203 // :-moz-locale-dir and :-moz-window-inactive depend only on 204 // the state of the document, which is invariant across all 205 // the elements involved in a given style cache. 206 NonTSPseudoClass::MozLocaleDir(_) | 207 NonTSPseudoClass::MozWindowInactive | 208 // Similar for the document themes. 209 NonTSPseudoClass::MozLWTheme | 210 NonTSPseudoClass::MozLWThemeBrightText | 211 NonTSPseudoClass::MozLWThemeDarkText 212 ) 213 } 214 } 215 216 impl ::selectors::parser::NonTSPseudoClass for NonTSPseudoClass { 217 type Impl = SelectorImpl; 218 219 #[inline] is_active_or_hover(&self) -> bool220 fn is_active_or_hover(&self) -> bool { 221 matches!(*self, NonTSPseudoClass::Active | NonTSPseudoClass::Hover) 222 } 223 224 /// We intentionally skip the link-related ones. 225 #[inline] is_user_action_state(&self) -> bool226 fn is_user_action_state(&self) -> bool { 227 matches!( 228 *self, 229 NonTSPseudoClass::Hover | NonTSPseudoClass::Active | NonTSPseudoClass::Focus 230 ) 231 } 232 } 233 234 /// The dummy struct we use to implement our selector parsing. 235 #[derive(Clone, Debug, Eq, PartialEq)] 236 pub struct SelectorImpl; 237 238 impl ::selectors::SelectorImpl for SelectorImpl { 239 type ExtraMatchingData = InvalidationMatchingData; 240 type AttrValue = AtomString; 241 type Identifier = AtomIdent; 242 type LocalName = AtomIdent; 243 type NamespacePrefix = AtomIdent; 244 type NamespaceUrl = Namespace; 245 type BorrowedNamespaceUrl = WeakNamespace; 246 type BorrowedLocalName = WeakAtom; 247 248 type PseudoElement = PseudoElement; 249 type NonTSPseudoClass = NonTSPseudoClass; 250 should_collect_attr_hash(name: &AtomIdent) -> bool251 fn should_collect_attr_hash(name: &AtomIdent) -> bool { 252 static_prefs::pref!("layout.css.bloom-filter-attribute-names.enabled") && 253 !crate::bloom::is_attr_name_excluded_from_filter(name) 254 } 255 } 256 257 impl<'a> SelectorParser<'a> { is_pseudo_class_enabled(&self, pseudo_class: &NonTSPseudoClass) -> bool258 fn is_pseudo_class_enabled(&self, pseudo_class: &NonTSPseudoClass) -> bool { 259 if pseudo_class.is_enabled_in_content() { 260 return true; 261 } 262 263 if self.in_user_agent_stylesheet() && 264 pseudo_class.has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS) 265 { 266 return true; 267 } 268 269 if self.chrome_rules_enabled() && 270 pseudo_class.has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_CHROME) 271 { 272 return true; 273 } 274 275 return false; 276 } 277 is_pseudo_element_enabled(&self, pseudo_element: &PseudoElement) -> bool278 fn is_pseudo_element_enabled(&self, pseudo_element: &PseudoElement) -> bool { 279 if pseudo_element.enabled_in_content() { 280 return true; 281 } 282 283 if self.in_user_agent_stylesheet() && pseudo_element.enabled_in_ua_sheets() { 284 return true; 285 } 286 287 if self.chrome_rules_enabled() && pseudo_element.enabled_in_chrome() { 288 return true; 289 } 290 291 return false; 292 } 293 } 294 295 impl<'a, 'i> ::selectors::Parser<'i> for SelectorParser<'a> { 296 type Impl = SelectorImpl; 297 type Error = StyleParseErrorKind<'i>; 298 299 #[inline] parse_slotted(&self) -> bool300 fn parse_slotted(&self) -> bool { 301 true 302 } 303 304 #[inline] parse_host(&self) -> bool305 fn parse_host(&self) -> bool { 306 true 307 } 308 309 #[inline] parse_is_and_where(&self) -> bool310 fn parse_is_and_where(&self) -> bool { 311 true 312 } 313 314 #[inline] is_and_where_error_recovery(&self) -> ParseErrorRecovery315 fn is_and_where_error_recovery(&self) -> ParseErrorRecovery { 316 if static_prefs::pref!("layout.css.is-and-where-better-error-recovery.enabled") { 317 ParseErrorRecovery::IgnoreInvalidSelector 318 } else { 319 ParseErrorRecovery::DiscardList 320 } 321 } 322 323 #[inline] parse_part(&self) -> bool324 fn parse_part(&self) -> bool { 325 true 326 } 327 328 #[inline] is_is_alias(&self, function: &str) -> bool329 fn is_is_alias(&self, function: &str) -> bool { 330 function.eq_ignore_ascii_case("-moz-any") 331 } 332 parse_non_ts_pseudo_class( &self, location: SourceLocation, name: CowRcStr<'i>, ) -> Result<NonTSPseudoClass, ParseError<'i>>333 fn parse_non_ts_pseudo_class( 334 &self, 335 location: SourceLocation, 336 name: CowRcStr<'i>, 337 ) -> Result<NonTSPseudoClass, ParseError<'i>> { 338 if let Some(pseudo_class) = NonTSPseudoClass::parse_non_functional(&name) { 339 if self.is_pseudo_class_enabled(&pseudo_class) { 340 return Ok(pseudo_class); 341 } 342 } 343 Err( 344 location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( 345 name, 346 )), 347 ) 348 } 349 parse_non_ts_functional_pseudo_class<'t>( &self, name: CowRcStr<'i>, parser: &mut Parser<'i, 't>, ) -> Result<NonTSPseudoClass, ParseError<'i>>350 fn parse_non_ts_functional_pseudo_class<'t>( 351 &self, 352 name: CowRcStr<'i>, 353 parser: &mut Parser<'i, 't>, 354 ) -> Result<NonTSPseudoClass, ParseError<'i>> { 355 let pseudo_class = match_ignore_ascii_case! { &name, 356 "lang" => { 357 let name = parser.expect_ident_or_string()?; 358 NonTSPseudoClass::Lang(Lang::from(name.as_ref())) 359 }, 360 "-moz-locale-dir" => { 361 NonTSPseudoClass::MozLocaleDir(Direction::parse(parser)?) 362 }, 363 "dir" => { 364 NonTSPseudoClass::Dir(Direction::parse(parser)?) 365 }, 366 _ => return Err(parser.new_custom_error( 367 SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name.clone()) 368 )) 369 }; 370 if self.is_pseudo_class_enabled(&pseudo_class) { 371 Ok(pseudo_class) 372 } else { 373 Err( 374 parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( 375 name, 376 )), 377 ) 378 } 379 } 380 parse_pseudo_element( &self, location: SourceLocation, name: CowRcStr<'i>, ) -> Result<PseudoElement, ParseError<'i>>381 fn parse_pseudo_element( 382 &self, 383 location: SourceLocation, 384 name: CowRcStr<'i>, 385 ) -> Result<PseudoElement, ParseError<'i>> { 386 if let Some(pseudo) = PseudoElement::from_slice(&name) { 387 if self.is_pseudo_element_enabled(&pseudo) { 388 return Ok(pseudo); 389 } 390 } 391 392 Err( 393 location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( 394 name, 395 )), 396 ) 397 } 398 parse_functional_pseudo_element<'t>( &self, name: CowRcStr<'i>, parser: &mut Parser<'i, 't>, ) -> Result<PseudoElement, ParseError<'i>>399 fn parse_functional_pseudo_element<'t>( 400 &self, 401 name: CowRcStr<'i>, 402 parser: &mut Parser<'i, 't>, 403 ) -> Result<PseudoElement, ParseError<'i>> { 404 if starts_with_ignore_ascii_case(&name, "-moz-tree-") { 405 // Tree pseudo-elements can have zero or more arguments, separated 406 // by either comma or space. 407 let mut args = Vec::new(); 408 loop { 409 let location = parser.current_source_location(); 410 match parser.next() { 411 Ok(&Token::Ident(ref ident)) => args.push(Atom::from(ident.as_ref())), 412 Ok(&Token::Comma) => {}, 413 Ok(t) => return Err(location.new_unexpected_token_error(t.clone())), 414 Err(BasicParseError { 415 kind: BasicParseErrorKind::EndOfInput, 416 .. 417 }) => break, 418 _ => unreachable!("Parser::next() shouldn't return any other error"), 419 } 420 } 421 let args = args.into_boxed_slice(); 422 if let Some(pseudo) = PseudoElement::tree_pseudo_element(&name, args) { 423 if self.is_pseudo_element_enabled(&pseudo) { 424 return Ok(pseudo); 425 } 426 } 427 } 428 Err( 429 parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( 430 name, 431 )), 432 ) 433 } 434 default_namespace(&self) -> Option<Namespace>435 fn default_namespace(&self) -> Option<Namespace> { 436 self.namespaces.default.clone() 437 } 438 namespace_for_prefix(&self, prefix: &AtomIdent) -> Option<Namespace>439 fn namespace_for_prefix(&self, prefix: &AtomIdent) -> Option<Namespace> { 440 self.namespaces.prefixes.get(prefix).cloned() 441 } 442 } 443 444 impl SelectorImpl { 445 /// A helper to traverse each eagerly cascaded pseudo-element, executing 446 /// `fun` on it. 447 #[inline] each_eagerly_cascaded_pseudo_element<F>(mut fun: F) where F: FnMut(PseudoElement),448 pub fn each_eagerly_cascaded_pseudo_element<F>(mut fun: F) 449 where 450 F: FnMut(PseudoElement), 451 { 452 for pseudo in &EAGER_PSEUDOS { 453 fun(pseudo.clone()) 454 } 455 } 456 } 457 458 unsafe impl HasFFI for SelectorList<SelectorImpl> { 459 type FFIType = RawServoSelectorList; 460 } 461 unsafe impl HasSimpleFFI for SelectorList<SelectorImpl> {} 462 unsafe impl HasBoxFFI for SelectorList<SelectorImpl> {} 463