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