| //! CSS selectors. |
| |
| use crate::compat::Feature; |
| use crate::error::{ParserError, PrinterError, SelectorError}; |
| use crate::parser::ParserFlags; |
| use crate::printer::Printer; |
| use crate::properties::custom::TokenList; |
| use crate::rules::StyleContext; |
| use crate::stylesheet::{ParserOptions, PrinterOptions}; |
| use crate::targets::{should_compile, Targets}; |
| use crate::traits::{Parse, ParseWithOptions, ToCss}; |
| use crate::values::ident::{CustomIdent, Ident}; |
| use crate::values::string::CSSString; |
| use crate::vendor_prefix::VendorPrefix; |
| #[cfg(feature = "visitor")] |
| use crate::visitor::{Visit, VisitTypes, Visitor}; |
| use crate::{macros::enum_property, values::string::CowArcStr}; |
| use cssparser::*; |
| use parcel_selectors::parser::{NthType, SelectorParseErrorKind}; |
| use parcel_selectors::{ |
| attr::{AttrSelectorOperator, ParsedAttrSelectorOperation, ParsedCaseSensitivity}, |
| parser::SelectorImpl, |
| }; |
| use smallvec::SmallVec; |
| use std::collections::HashSet; |
| use std::fmt; |
| |
| #[cfg(feature = "serde")] |
| use crate::serialization::*; |
| |
| mod private { |
| #[derive(Debug, Clone, PartialEq, Eq, Hash)] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| pub struct Selectors; |
| |
| #[cfg(feature = "into_owned")] |
| impl<'any> ::static_self::IntoOwned<'any> for Selectors { |
| type Owned = Self; |
| |
| fn into_owned(self) -> Self::Owned { |
| self |
| } |
| } |
| } |
| |
| #[cfg(feature = "into_owned")] |
| fn _assert_into_owned() { |
| // Ensure that we provide into_owned |
| |
| use static_self::IntoOwned; |
| fn _assert<'any, T: IntoOwned<'any>>() {} |
| |
| _assert::<SelectorList>(); |
| } |
| |
| use private::Selectors; |
| |
| /// A list of selectors. |
| pub type SelectorList<'i> = parcel_selectors::SelectorList<'i, Selectors>; |
| /// A CSS selector, including a list of components. |
| pub type Selector<'i> = parcel_selectors::parser::Selector<'i, Selectors>; |
| /// An individual component within a selector. |
| pub type Component<'i> = parcel_selectors::parser::Component<'i, Selectors>; |
| /// A combinator. |
| pub use parcel_selectors::parser::Combinator; |
| |
| impl<'i> SelectorImpl<'i> for Selectors { |
| type AttrValue = CSSString<'i>; |
| type Identifier = Ident<'i>; |
| type LocalName = Ident<'i>; |
| type NamespacePrefix = Ident<'i>; |
| type NamespaceUrl = CowArcStr<'i>; |
| type BorrowedNamespaceUrl = CowArcStr<'i>; |
| type BorrowedLocalName = Ident<'i>; |
| |
| type NonTSPseudoClass = PseudoClass<'i>; |
| type PseudoElement = PseudoElement<'i>; |
| type VendorPrefix = VendorPrefix; |
| |
| type ExtraMatchingData = (); |
| |
| fn to_css<W: fmt::Write>(selectors: &SelectorList<'i>, dest: &mut W) -> std::fmt::Result { |
| let mut printer = Printer::new(dest, PrinterOptions::default()); |
| serialize_selector_list(selectors.0.iter(), &mut printer, None, false).map_err(|_| std::fmt::Error) |
| } |
| } |
| |
| pub(crate) struct SelectorParser<'a, 'o, 'i> { |
| pub is_nesting_allowed: bool, |
| pub options: &'a ParserOptions<'o, 'i>, |
| } |
| |
| impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, 'i> { |
| type Impl = Selectors; |
| type Error = ParserError<'i>; |
| |
| fn parse_non_ts_pseudo_class( |
| &self, |
| loc: SourceLocation, |
| name: CowRcStr<'i>, |
| ) -> Result<PseudoClass<'i>, ParseError<'i, Self::Error>> { |
| use PseudoClass::*; |
| let pseudo_class = match_ignore_ascii_case! { &name, |
| // https://drafts.csswg.org/selectors-4/#useraction-pseudos |
| "hover" => Hover, |
| "active" => Active, |
| "focus" => Focus, |
| "focus-visible" => FocusVisible, |
| "focus-within" => FocusWithin, |
| |
| // https://drafts.csswg.org/selectors-4/#time-pseudos |
| "current" => Current, |
| "past" => Past, |
| "future" => Future, |
| |
| // https://drafts.csswg.org/selectors-4/#resource-pseudos |
| "playing" => Playing, |
| "paused" => Paused, |
| "seeking" => Seeking, |
| "buffering" => Buffering, |
| "stalled" => Stalled, |
| "muted" => Muted, |
| "volume-locked" => VolumeLocked, |
| |
| // https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class |
| "fullscreen" => Fullscreen(VendorPrefix::None), |
| "-webkit-full-screen" => Fullscreen(VendorPrefix::WebKit), |
| "-moz-full-screen" => Fullscreen(VendorPrefix::Moz), |
| "-ms-fullscreen" => Fullscreen(VendorPrefix::Ms), |
| |
| // https://drafts.csswg.org/selectors/#display-state-pseudos |
| "open" => Open, |
| "closed" => Closed, |
| "modal" => Modal, |
| "picture-in-picture" => PictureInPicture, |
| |
| // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open |
| "popover-open" => PopoverOpen, |
| |
| // https://drafts.csswg.org/selectors-4/#the-defined-pseudo |
| "defined" => Defined, |
| |
| // https://drafts.csswg.org/selectors-4/#location |
| "any-link" => AnyLink(VendorPrefix::None), |
| "-webkit-any-link" => AnyLink(VendorPrefix::WebKit), |
| "-moz-any-link" => AnyLink(VendorPrefix::Moz), |
| "link" => Link, |
| "local-link" => LocalLink, |
| "target" => Target, |
| "target-within" => TargetWithin, |
| "visited" => Visited, |
| |
| // https://drafts.csswg.org/selectors-4/#input-pseudos |
| "enabled" => Enabled, |
| "disabled" => Disabled, |
| "read-only" => ReadOnly(VendorPrefix::None), |
| "-moz-read-only" => ReadOnly(VendorPrefix::Moz), |
| "read-write" => ReadWrite(VendorPrefix::None), |
| "-moz-read-write" => ReadWrite(VendorPrefix::Moz), |
| "placeholder-shown" => PlaceholderShown(VendorPrefix::None), |
| "-moz-placeholder" => PlaceholderShown(VendorPrefix::Moz), |
| "-ms-input-placeholder" => PlaceholderShown(VendorPrefix::Ms), |
| "default" => Default, |
| "checked" => Checked, |
| "indeterminate" => Indeterminate, |
| "blank" => Blank, |
| "valid" => Valid, |
| "invalid" => Invalid, |
| "in-range" => InRange, |
| "out-of-range" => OutOfRange, |
| "required" => Required, |
| "optional" => Optional, |
| "user-valid" => UserValid, |
| "user-invalid" => UserInvalid, |
| |
| // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill |
| "autofill" => Autofill(VendorPrefix::None), |
| "-webkit-autofill" => Autofill(VendorPrefix::WebKit), |
| "-o-autofill" => Autofill(VendorPrefix::O), |
| |
| // https://drafts.csswg.org/css-view-transitions-2/#pseudo-classes-for-selective-vt |
| "active-view-transition" => ActiveViewTransition, |
| |
| // https://webkit.org/blog/363/styling-scrollbars/ |
| "horizontal" => WebKitScrollbar(WebKitScrollbarPseudoClass::Horizontal), |
| "vertical" => WebKitScrollbar(WebKitScrollbarPseudoClass::Vertical), |
| "decrement" => WebKitScrollbar(WebKitScrollbarPseudoClass::Decrement), |
| "increment" => WebKitScrollbar(WebKitScrollbarPseudoClass::Increment), |
| "start" => WebKitScrollbar(WebKitScrollbarPseudoClass::Start), |
| "end" => WebKitScrollbar(WebKitScrollbarPseudoClass::End), |
| "double-button" => WebKitScrollbar(WebKitScrollbarPseudoClass::DoubleButton), |
| "single-button" => WebKitScrollbar(WebKitScrollbarPseudoClass::SingleButton), |
| "no-button" => WebKitScrollbar(WebKitScrollbarPseudoClass::NoButton), |
| "corner-present" => WebKitScrollbar(WebKitScrollbarPseudoClass::CornerPresent), |
| "window-inactive" => WebKitScrollbar(WebKitScrollbarPseudoClass::WindowInactive), |
| |
| "local" | "global" if self.options.css_modules.is_some() => { |
| return Err(loc.new_custom_error(SelectorParseErrorKind::AmbiguousCssModuleClass(name.clone()))) |
| }, |
| |
| _ => { |
| if !name.starts_with('-') { |
| self.options.warn(loc.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClass(name.clone()))); |
| } |
| Custom { name: name.into() } |
| } |
| }; |
| |
| Ok(pseudo_class) |
| } |
| |
| fn parse_non_ts_functional_pseudo_class<'t>( |
| &self, |
| name: CowRcStr<'i>, |
| parser: &mut cssparser::Parser<'i, 't>, |
| ) -> Result<PseudoClass<'i>, ParseError<'i, Self::Error>> { |
| use PseudoClass::*; |
| let pseudo_class = match_ignore_ascii_case! { &name, |
| "lang" => { |
| let languages = parser.parse_comma_separated(|parser| { |
| parser.expect_ident_or_string() |
| .map(|s| s.into()) |
| .map_err(|e| e.into()) |
| })?; |
| Lang { languages } |
| }, |
| "dir" => Dir { direction: Direction::parse(parser)? }, |
| // https://drafts.csswg.org/css-view-transitions-2/#the-active-view-transition-type-pseudo |
| "active-view-transition-type" => { |
| let kind = Parse::parse(parser)?; |
| ActiveViewTransitionType { kind } |
| }, |
| "local" if self.options.css_modules.is_some() => Local { selector: Box::new(Selector::parse(self, parser)?) }, |
| "global" if self.options.css_modules.is_some() => Global { selector: Box::new(Selector::parse(self, parser)?) }, |
| _ => { |
| if !name.starts_with('-') { |
| self.options.warn(parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClass(name.clone()))); |
| } |
| let mut args = Vec::new(); |
| TokenList::parse_raw(parser, &mut args, &self.options, 0)?; |
| CustomFunction { |
| name: name.into(), |
| arguments: TokenList(args) |
| } |
| }, |
| }; |
| |
| Ok(pseudo_class) |
| } |
| |
| fn parse_any_prefix<'t>(&self, name: &str) -> Option<VendorPrefix> { |
| match_ignore_ascii_case! { &name, |
| "-webkit-any" => Some(VendorPrefix::WebKit), |
| "-moz-any" => Some(VendorPrefix::Moz), |
| _ => None |
| } |
| } |
| |
| fn parse_pseudo_element( |
| &self, |
| loc: SourceLocation, |
| name: CowRcStr<'i>, |
| ) -> Result<PseudoElement<'i>, ParseError<'i, Self::Error>> { |
| use PseudoElement::*; |
| let pseudo_element = match_ignore_ascii_case! { &name, |
| "before" => Before, |
| "after" => After, |
| "first-line" => FirstLine, |
| "first-letter" => FirstLetter, |
| "details-content" => DetailsContent, |
| "target-text" => TargetText, |
| "cue" => Cue, |
| "cue-region" => CueRegion, |
| "selection" => Selection(VendorPrefix::None), |
| "-moz-selection" => Selection(VendorPrefix::Moz), |
| "placeholder" => Placeholder(VendorPrefix::None), |
| "-webkit-input-placeholder" => Placeholder(VendorPrefix::WebKit), |
| "-moz-placeholder" => Placeholder(VendorPrefix::Moz), |
| "-ms-input-placeholder" => Placeholder(VendorPrefix::Moz), |
| "marker" => Marker, |
| "backdrop" => Backdrop(VendorPrefix::None), |
| "-webkit-backdrop" => Backdrop(VendorPrefix::WebKit), |
| "file-selector-button" => FileSelectorButton(VendorPrefix::None), |
| "-webkit-file-upload-button" => FileSelectorButton(VendorPrefix::WebKit), |
| "-ms-browse" => FileSelectorButton(VendorPrefix::Ms), |
| |
| "-webkit-scrollbar" => WebKitScrollbar(WebKitScrollbarPseudoElement::Scrollbar), |
| "-webkit-scrollbar-button" => WebKitScrollbar(WebKitScrollbarPseudoElement::Button), |
| "-webkit-scrollbar-track" => WebKitScrollbar(WebKitScrollbarPseudoElement::Track), |
| "-webkit-scrollbar-track-piece" => WebKitScrollbar(WebKitScrollbarPseudoElement::TrackPiece), |
| "-webkit-scrollbar-thumb" => WebKitScrollbar(WebKitScrollbarPseudoElement::Thumb), |
| "-webkit-scrollbar-corner" => WebKitScrollbar(WebKitScrollbarPseudoElement::Corner), |
| "-webkit-resizer" => WebKitScrollbar(WebKitScrollbarPseudoElement::Resizer), |
| |
| "picker-icon" => PickerIcon, |
| "checkmark" => Checkmark, |
| |
| "view-transition" => ViewTransition, |
| |
| _ => { |
| if !name.starts_with('-') { |
| self.options.warn(loc.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoElement(name.clone()))); |
| } |
| Custom { name: name.into() } |
| } |
| }; |
| |
| Ok(pseudo_element) |
| } |
| |
| fn parse_functional_pseudo_element<'t>( |
| &self, |
| name: CowRcStr<'i>, |
| arguments: &mut Parser<'i, 't>, |
| ) -> Result<<Self::Impl as SelectorImpl<'i>>::PseudoElement, ParseError<'i, Self::Error>> { |
| use PseudoElement::*; |
| let pseudo_element = match_ignore_ascii_case! { &name, |
| "cue" => CueFunction { selector: Box::new(Selector::parse(self, arguments)?) }, |
| "cue-region" => CueRegionFunction { selector: Box::new(Selector::parse(self, arguments)?) }, |
| "picker" => PickerFunction { identifier: Ident::parse(arguments)? }, |
| "view-transition-group" => ViewTransitionGroup { part: ViewTransitionPartSelector::parse(arguments)? }, |
| "view-transition-image-pair" => ViewTransitionImagePair { part: ViewTransitionPartSelector::parse(arguments)? }, |
| "view-transition-old" => ViewTransitionOld { part: ViewTransitionPartSelector::parse(arguments)? }, |
| "view-transition-new" => ViewTransitionNew { part: ViewTransitionPartSelector::parse(arguments)? }, |
| _ => { |
| if !name.starts_with('-') { |
| self.options.warn(arguments.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoElement(name.clone()))); |
| } |
| let mut args = Vec::new(); |
| TokenList::parse_raw(arguments, &mut args, &self.options, 0)?; |
| CustomFunction { name: name.into(), arguments: TokenList(args) } |
| } |
| }; |
| |
| Ok(pseudo_element) |
| } |
| |
| #[inline] |
| fn parse_slotted(&self) -> bool { |
| true |
| } |
| |
| #[inline] |
| fn parse_host(&self) -> bool { |
| true |
| } |
| |
| #[inline] |
| fn parse_is_and_where(&self) -> bool { |
| true |
| } |
| |
| #[inline] |
| fn parse_part(&self) -> bool { |
| true |
| } |
| |
| fn default_namespace(&self) -> Option<CowArcStr<'i>> { |
| None |
| } |
| |
| fn namespace_for_prefix(&self, prefix: &Ident<'i>) -> Option<CowArcStr<'i>> { |
| Some(prefix.0.clone()) |
| } |
| |
| #[inline] |
| fn is_nesting_allowed(&self) -> bool { |
| self.is_nesting_allowed |
| } |
| |
| fn deep_combinator_enabled(&self) -> bool { |
| self.options.flags.contains(ParserFlags::DEEP_SELECTOR_COMBINATOR) |
| } |
| } |
| |
| enum_property! { |
| /// The [:dir()](https://drafts.csswg.org/selectors-4/#the-dir-pseudo) pseudo class. |
| #[derive(Eq, Hash)] |
| pub enum Direction { |
| /// Left to right |
| Ltr, |
| /// Right to left |
| Rtl, |
| } |
| } |
| |
| /// A pseudo class. |
| #[derive(Clone, PartialEq, Eq, Hash)] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "kind", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum PseudoClass<'i> { |
| // https://drafts.csswg.org/selectors-4/#linguistic-pseudos |
| /// The [:lang()](https://drafts.csswg.org/selectors-4/#the-lang-pseudo) pseudo class. |
| Lang { |
| /// A list of language codes. |
| #[cfg_attr(feature = "serde", serde(borrow))] |
| languages: Vec<CowArcStr<'i>>, |
| }, |
| /// The [:dir()](https://drafts.csswg.org/selectors-4/#the-dir-pseudo) pseudo class. |
| Dir { |
| /// A direction. |
| direction: Direction, |
| }, |
| |
| // https://drafts.csswg.org/selectors-4/#useraction-pseudos |
| /// The [:hover](https://drafts.csswg.org/selectors-4/#the-hover-pseudo) pseudo class. |
| Hover, |
| /// The [:active](https://drafts.csswg.org/selectors-4/#the-active-pseudo) pseudo class. |
| Active, |
| /// The [:focus](https://drafts.csswg.org/selectors-4/#the-focus-pseudo) pseudo class. |
| Focus, |
| /// The [:focus-visible](https://drafts.csswg.org/selectors-4/#the-focus-visible-pseudo) pseudo class. |
| FocusVisible, |
| /// The [:focus-within](https://drafts.csswg.org/selectors-4/#the-focus-within-pseudo) pseudo class. |
| FocusWithin, |
| |
| // https://drafts.csswg.org/selectors-4/#time-pseudos |
| /// The [:current](https://drafts.csswg.org/selectors-4/#the-current-pseudo) pseudo class. |
| Current, |
| /// The [:past](https://drafts.csswg.org/selectors-4/#the-past-pseudo) pseudo class. |
| Past, |
| /// The [:future](https://drafts.csswg.org/selectors-4/#the-future-pseudo) pseudo class. |
| Future, |
| |
| // https://drafts.csswg.org/selectors-4/#resource-pseudos |
| /// The [:playing](https://drafts.csswg.org/selectors-4/#selectordef-playing) pseudo class. |
| Playing, |
| /// The [:paused](https://drafts.csswg.org/selectors-4/#selectordef-paused) pseudo class. |
| Paused, |
| /// The [:seeking](https://drafts.csswg.org/selectors-4/#selectordef-seeking) pseudo class. |
| Seeking, |
| /// The [:buffering](https://drafts.csswg.org/selectors-4/#selectordef-buffering) pseudo class. |
| Buffering, |
| /// The [:stalled](https://drafts.csswg.org/selectors-4/#selectordef-stalled) pseudo class. |
| Stalled, |
| /// The [:muted](https://drafts.csswg.org/selectors-4/#selectordef-muted) pseudo class. |
| Muted, |
| /// The [:volume-locked](https://drafts.csswg.org/selectors-4/#selectordef-volume-locked) pseudo class. |
| VolumeLocked, |
| |
| /// The [:fullscreen](https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class) pseudo class. |
| #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))] |
| Fullscreen(VendorPrefix), |
| |
| // https://drafts.csswg.org/selectors/#display-state-pseudos |
| /// The [:open](https://drafts.csswg.org/selectors/#selectordef-open) pseudo class. |
| Open, |
| /// The [:closed](https://drafts.csswg.org/selectors/#selectordef-closed) pseudo class. |
| Closed, |
| /// The [:modal](https://drafts.csswg.org/selectors/#modal-state) pseudo class. |
| Modal, |
| /// The [:picture-in-picture](https://drafts.csswg.org/selectors/#pip-state) pseudo class. |
| PictureInPicture, |
| |
| // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open |
| /// The [:popover-open](https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open) pseudo class. |
| PopoverOpen, |
| |
| /// The [:defined](https://drafts.csswg.org/selectors-4/#the-defined-pseudo) pseudo class. |
| Defined, |
| |
| // https://drafts.csswg.org/selectors-4/#location |
| /// The [:any-link](https://drafts.csswg.org/selectors-4/#the-any-link-pseudo) pseudo class. |
| #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))] |
| AnyLink(VendorPrefix), |
| /// The [:link](https://drafts.csswg.org/selectors-4/#link-pseudo) pseudo class. |
| Link, |
| /// The [:local-link](https://drafts.csswg.org/selectors-4/#the-local-link-pseudo) pseudo class. |
| LocalLink, |
| /// The [:target](https://drafts.csswg.org/selectors-4/#the-target-pseudo) pseudo class. |
| Target, |
| /// The [:target-within](https://drafts.csswg.org/selectors-4/#the-target-within-pseudo) pseudo class. |
| TargetWithin, |
| /// The [:visited](https://drafts.csswg.org/selectors-4/#visited-pseudo) pseudo class. |
| Visited, |
| |
| // https://drafts.csswg.org/selectors-4/#input-pseudos |
| /// The [:enabled](https://drafts.csswg.org/selectors-4/#enabled-pseudo) pseudo class. |
| Enabled, |
| /// The [:disabled](https://drafts.csswg.org/selectors-4/#disabled-pseudo) pseudo class. |
| Disabled, |
| /// The [:read-only](https://drafts.csswg.org/selectors-4/#read-only-pseudo) pseudo class. |
| #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))] |
| ReadOnly(VendorPrefix), |
| /// The [:read-write](https://drafts.csswg.org/selectors-4/#read-write-pseudo) pseudo class. |
| #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))] |
| ReadWrite(VendorPrefix), |
| /// The [:placeholder-shown](https://drafts.csswg.org/selectors-4/#placeholder) pseudo class. |
| #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))] |
| PlaceholderShown(VendorPrefix), |
| /// The [:default](https://drafts.csswg.org/selectors-4/#the-default-pseudo) pseudo class. |
| Default, |
| /// The [:checked](https://drafts.csswg.org/selectors-4/#checked) pseudo class. |
| Checked, |
| /// The [:indeterminate](https://drafts.csswg.org/selectors-4/#indeterminate) pseudo class. |
| Indeterminate, |
| /// The [:blank](https://drafts.csswg.org/selectors-4/#blank) pseudo class. |
| Blank, |
| /// The [:valid](https://drafts.csswg.org/selectors-4/#valid-pseudo) pseudo class. |
| Valid, |
| /// The [:invalid](https://drafts.csswg.org/selectors-4/#invalid-pseudo) pseudo class. |
| Invalid, |
| /// The [:in-range](https://drafts.csswg.org/selectors-4/#in-range-pseudo) pseudo class. |
| InRange, |
| /// The [:out-of-range](https://drafts.csswg.org/selectors-4/#out-of-range-pseudo) pseudo class. |
| OutOfRange, |
| /// The [:required](https://drafts.csswg.org/selectors-4/#required-pseudo) pseudo class. |
| Required, |
| /// The [:optional](https://drafts.csswg.org/selectors-4/#optional-pseudo) pseudo class. |
| Optional, |
| /// The [:user-valid](https://drafts.csswg.org/selectors-4/#user-valid-pseudo) pseudo class. |
| UserValid, |
| /// The [:used-invalid](https://drafts.csswg.org/selectors-4/#user-invalid-pseudo) pseudo class. |
| UserInvalid, |
| |
| /// The [:autofill](https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill) pseudo class. |
| #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))] |
| Autofill(VendorPrefix), |
| |
| /// The [:active-view-transition](https://drafts.csswg.org/css-view-transitions-2/#the-active-view-transition-pseudo) pseudo class. |
| ActiveViewTransition, |
| /// The [:active-view-transition-type()](https://drafts.csswg.org/css-view-transitions-2/#the-active-view-transition-type-pseudo) pseudo class. |
| ActiveViewTransitionType { |
| /// A view transition type. |
| #[cfg_attr(feature = "serde", serde(rename = "type"))] |
| kind: SmallVec<[CustomIdent<'i>; 1]>, |
| }, |
| |
| // CSS modules |
| /// The CSS modules :local() pseudo class. |
| Local { |
| /// A local selector. |
| selector: Box<Selector<'i>>, |
| }, |
| /// The CSS modules :global() pseudo class. |
| Global { |
| /// A global selector. |
| selector: Box<Selector<'i>>, |
| }, |
| |
| /// A [webkit scrollbar](https://webkit.org/blog/363/styling-scrollbars/) pseudo class. |
| // https://webkit.org/blog/363/styling-scrollbars/ |
| #[cfg_attr( |
| feature = "serde", |
| serde(rename = "webkit-scrollbar", with = "ValueWrapper::<WebKitScrollbarPseudoClass>") |
| )] |
| WebKitScrollbar(WebKitScrollbarPseudoClass), |
| /// An unknown pseudo class. |
| Custom { |
| /// The pseudo class name. |
| name: CowArcStr<'i>, |
| }, |
| /// An unknown functional pseudo class. |
| CustomFunction { |
| /// The pseudo class name. |
| name: CowArcStr<'i>, |
| /// The arguments of the pseudo class function. |
| arguments: TokenList<'i>, |
| }, |
| } |
| |
| /// A [webkit scrollbar](https://webkit.org/blog/363/styling-scrollbars/) pseudo class. |
| #[derive(Clone, Eq, PartialEq, Hash)] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum WebKitScrollbarPseudoClass { |
| /// :horizontal |
| Horizontal, |
| /// :vertical |
| Vertical, |
| /// :decrement |
| Decrement, |
| /// :increment |
| Increment, |
| /// :start |
| Start, |
| /// :end |
| End, |
| /// :double-button |
| DoubleButton, |
| /// :single-button |
| SingleButton, |
| /// :no-button |
| NoButton, |
| /// :corner-present |
| CornerPresent, |
| /// :window-inactive |
| WindowInactive, |
| } |
| |
| impl<'i> parcel_selectors::parser::NonTSPseudoClass<'i> for PseudoClass<'i> { |
| type Impl = Selectors; |
| |
| fn is_active_or_hover(&self) -> bool { |
| matches!(*self, PseudoClass::Active | PseudoClass::Hover) |
| } |
| |
| fn is_user_action_state(&self) -> bool { |
| matches!( |
| *self, |
| PseudoClass::Active |
| | PseudoClass::Hover |
| | PseudoClass::Focus |
| | PseudoClass::FocusWithin |
| | PseudoClass::FocusVisible |
| ) |
| } |
| |
| fn is_valid_before_webkit_scrollbar(&self) -> bool { |
| !matches!(*self, PseudoClass::WebKitScrollbar(..)) |
| } |
| |
| fn is_valid_after_webkit_scrollbar(&self) -> bool { |
| // https://github.com/WebKit/WebKit/blob/02fbf9b7aa435edb96cbf563a8d4dcf1aa73b4b3/Source/WebCore/css/parser/CSSSelectorParser.cpp#L285 |
| matches!( |
| *self, |
| PseudoClass::WebKitScrollbar(..) |
| | PseudoClass::Enabled |
| | PseudoClass::Disabled |
| | PseudoClass::Hover |
| | PseudoClass::Active |
| ) |
| } |
| } |
| |
| impl<'i> cssparser::ToCss for PseudoClass<'i> { |
| fn to_css<W>(&self, dest: &mut W) -> std::fmt::Result |
| where |
| W: fmt::Write, |
| { |
| let mut s = String::new(); |
| serialize_pseudo_class(self, &mut Printer::new(&mut s, Default::default()), None) |
| .map_err(|_| std::fmt::Error)?; |
| write!(dest, "{}", s) |
| } |
| } |
| |
| fn serialize_pseudo_class<'a, 'i, W>( |
| pseudo_class: &PseudoClass<'i>, |
| dest: &mut Printer<W>, |
| context: Option<&StyleContext>, |
| ) -> Result<(), PrinterError> |
| where |
| W: fmt::Write, |
| { |
| use PseudoClass::*; |
| match pseudo_class { |
| Lang { languages: lang } => { |
| dest.write_str(":lang(")?; |
| let mut first = true; |
| for lang in lang { |
| if first { |
| first = false; |
| } else { |
| dest.delim(',', false)?; |
| } |
| serialize_identifier(lang, dest)?; |
| } |
| return dest.write_str(")"); |
| } |
| Dir { direction: dir } => { |
| dest.write_str(":dir(")?; |
| dir.to_css(dest)?; |
| return dest.write_str(")"); |
| } |
| _ => {} |
| } |
| |
| macro_rules! write_prefixed { |
| ($prefix: ident, $val: expr) => {{ |
| dest.write_char(':')?; |
| // If the printer has a vendor prefix override, use that. |
| let vp = if !dest.vendor_prefix.is_empty() { |
| (dest.vendor_prefix & *$prefix).or_none() |
| } else { |
| *$prefix |
| }; |
| vp.to_css(dest)?; |
| dest.write_str($val) |
| }}; |
| } |
| |
| macro_rules! pseudo { |
| ($key: ident, $s: literal) => {{ |
| let class = if let Some(pseudo_classes) = &dest.pseudo_classes { |
| pseudo_classes.$key |
| } else { |
| None |
| }; |
| |
| if let Some(class) = class { |
| dest.write_char('.')?; |
| dest.write_ident(class, true) |
| } else { |
| dest.write_str($s) |
| } |
| }}; |
| } |
| |
| match pseudo_class { |
| // https://drafts.csswg.org/selectors-4/#useraction-pseudos |
| Hover => pseudo!(hover, ":hover"), |
| Active => pseudo!(active, ":active"), |
| Focus => pseudo!(focus, ":focus"), |
| FocusVisible => pseudo!(focus_visible, ":focus-visible"), |
| FocusWithin => pseudo!(focus_within, ":focus-within"), |
| |
| // https://drafts.csswg.org/selectors-4/#time-pseudos |
| Current => dest.write_str(":current"), |
| Past => dest.write_str(":past"), |
| Future => dest.write_str(":future"), |
| |
| // https://drafts.csswg.org/selectors-4/#resource-pseudos |
| Playing => dest.write_str(":playing"), |
| Paused => dest.write_str(":paused"), |
| Seeking => dest.write_str(":seeking"), |
| Buffering => dest.write_str(":buffering"), |
| Stalled => dest.write_str(":stalled"), |
| Muted => dest.write_str(":muted"), |
| VolumeLocked => dest.write_str(":volume-locked"), |
| |
| // https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class |
| Fullscreen(prefix) => { |
| dest.write_char(':')?; |
| let vp = if !dest.vendor_prefix.is_empty() { |
| (dest.vendor_prefix & *prefix).or_none() |
| } else { |
| *prefix |
| }; |
| vp.to_css(dest)?; |
| if vp == VendorPrefix::WebKit || vp == VendorPrefix::Moz { |
| dest.write_str("full-screen") |
| } else { |
| dest.write_str("fullscreen") |
| } |
| } |
| |
| // https://drafts.csswg.org/selectors/#display-state-pseudos |
| Open => dest.write_str(":open"), |
| Closed => dest.write_str(":closed"), |
| Modal => dest.write_str(":modal"), |
| PictureInPicture => dest.write_str(":picture-in-picture"), |
| |
| // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open |
| PopoverOpen => dest.write_str(":popover-open"), |
| |
| // https://drafts.csswg.org/selectors-4/#the-defined-pseudo |
| Defined => dest.write_str(":defined"), |
| |
| // https://drafts.csswg.org/selectors-4/#location |
| AnyLink(prefix) => write_prefixed!(prefix, "any-link"), |
| Link => dest.write_str(":link"), |
| LocalLink => dest.write_str(":local-link"), |
| Target => dest.write_str(":target"), |
| TargetWithin => dest.write_str(":target-within"), |
| Visited => dest.write_str(":visited"), |
| |
| // https://drafts.csswg.org/selectors-4/#input-pseudos |
| Enabled => dest.write_str(":enabled"), |
| Disabled => dest.write_str(":disabled"), |
| ReadOnly(prefix) => write_prefixed!(prefix, "read-only"), |
| ReadWrite(prefix) => write_prefixed!(prefix, "read-write"), |
| PlaceholderShown(prefix) => write_prefixed!(prefix, "placeholder-shown"), |
| Default => dest.write_str(":default"), |
| Checked => dest.write_str(":checked"), |
| Indeterminate => dest.write_str(":indeterminate"), |
| Blank => dest.write_str(":blank"), |
| Valid => dest.write_str(":valid"), |
| Invalid => dest.write_str(":invalid"), |
| InRange => dest.write_str(":in-range"), |
| OutOfRange => dest.write_str(":out-of-range"), |
| Required => dest.write_str(":required"), |
| Optional => dest.write_str(":optional"), |
| UserValid => dest.write_str(":user-valid"), |
| UserInvalid => dest.write_str(":user-invalid"), |
| |
| // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill |
| Autofill(prefix) => write_prefixed!(prefix, "autofill"), |
| |
| ActiveViewTransition => dest.write_str(":active-view-transition"), |
| ActiveViewTransitionType { kind } => { |
| dest.write_str(":active-view-transition-type(")?; |
| kind.to_css(dest)?; |
| dest.write_char(')') |
| } |
| |
| Local { selector } => serialize_selector(selector, dest, context, false), |
| Global { selector } => { |
| let css_module = std::mem::take(&mut dest.css_module); |
| serialize_selector(selector, dest, context, false)?; |
| dest.css_module = css_module; |
| Ok(()) |
| } |
| |
| // https://webkit.org/blog/363/styling-scrollbars/ |
| WebKitScrollbar(s) => { |
| use WebKitScrollbarPseudoClass::*; |
| dest.write_str(match s { |
| Horizontal => ":horizontal", |
| Vertical => ":vertical", |
| Decrement => ":decrement", |
| Increment => ":increment", |
| Start => ":start", |
| End => ":end", |
| DoubleButton => ":double-button", |
| SingleButton => ":single-button", |
| NoButton => ":no-button", |
| CornerPresent => ":corner-present", |
| WindowInactive => ":window-inactive", |
| }) |
| } |
| |
| Lang { languages: _ } | Dir { direction: _ } => unreachable!(), |
| Custom { name } => { |
| dest.write_char(':')?; |
| return dest.write_str(&name); |
| } |
| CustomFunction { name, arguments: args } => { |
| dest.write_char(':')?; |
| dest.write_str(name)?; |
| dest.write_char('(')?; |
| args.to_css_raw(dest)?; |
| dest.write_char(')') |
| } |
| } |
| } |
| |
| impl<'i> PseudoClass<'i> { |
| pub(crate) fn is_equivalent(&self, other: &PseudoClass<'i>) -> bool { |
| use PseudoClass::*; |
| match (self, other) { |
| (Fullscreen(_), Fullscreen(_)) |
| | (AnyLink(_), AnyLink(_)) |
| | (ReadOnly(_), ReadOnly(_)) |
| | (ReadWrite(_), ReadWrite(_)) |
| | (PlaceholderShown(_), PlaceholderShown(_)) |
| | (Autofill(_), Autofill(_)) => true, |
| (a, b) => a == b, |
| } |
| } |
| |
| pub(crate) fn get_prefix(&self) -> VendorPrefix { |
| use PseudoClass::*; |
| match self { |
| Fullscreen(p) | AnyLink(p) | ReadOnly(p) | ReadWrite(p) | PlaceholderShown(p) | Autofill(p) => *p, |
| _ => VendorPrefix::empty(), |
| } |
| } |
| |
| pub(crate) fn get_necessary_prefixes(&mut self, targets: Targets) -> VendorPrefix { |
| use crate::prefixes::Feature; |
| use PseudoClass::*; |
| let (p, feature) = match self { |
| Fullscreen(p) => (p, Feature::PseudoClassFullscreen), |
| AnyLink(p) => (p, Feature::PseudoClassAnyLink), |
| ReadOnly(p) => (p, Feature::PseudoClassReadOnly), |
| ReadWrite(p) => (p, Feature::PseudoClassReadWrite), |
| PlaceholderShown(p) => (p, Feature::PseudoClassPlaceholderShown), |
| Autofill(p) => (p, Feature::PseudoClassAutofill), |
| _ => return VendorPrefix::empty(), |
| }; |
| |
| *p = targets.prefixes(*p, feature); |
| *p |
| } |
| } |
| |
| /// A pseudo element. |
| #[derive(PartialEq, Eq, Hash, Clone, Debug)] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "kind", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum PseudoElement<'i> { |
| /// The [::after](https://drafts.csswg.org/css-pseudo-4/#selectordef-after) pseudo element. |
| After, |
| /// The [::before](https://drafts.csswg.org/css-pseudo-4/#selectordef-before) pseudo element. |
| Before, |
| /// The [::first-line](https://drafts.csswg.org/css-pseudo-4/#first-line-pseudo) pseudo element. |
| FirstLine, |
| /// The [::first-letter](https://drafts.csswg.org/css-pseudo-4/#first-letter-pseudo) pseudo element. |
| FirstLetter, |
| /// The [::details-content](https://drafts.csswg.org/css-pseudo-4/#details-content-pseudo) |
| DetailsContent, |
| /// The [::target-text](https://drafts.csswg.org/css-pseudo-4/#selectordef-target-text) |
| TargetText, |
| /// The [::selection](https://drafts.csswg.org/css-pseudo-4/#selectordef-selection) pseudo element. |
| #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))] |
| Selection(VendorPrefix), |
| /// The [::placeholder](https://drafts.csswg.org/css-pseudo-4/#placeholder-pseudo) pseudo element. |
| #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))] |
| Placeholder(VendorPrefix), |
| /// The [::marker](https://drafts.csswg.org/css-pseudo-4/#marker-pseudo) pseudo element. |
| Marker, |
| /// The [::backdrop](https://fullscreen.spec.whatwg.org/#::backdrop-pseudo-element) pseudo element. |
| #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))] |
| Backdrop(VendorPrefix), |
| /// The [::file-selector-button](https://drafts.csswg.org/css-pseudo-4/#file-selector-button-pseudo) pseudo element. |
| #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))] |
| FileSelectorButton(VendorPrefix), |
| /// A [webkit scrollbar](https://webkit.org/blog/363/styling-scrollbars/) pseudo element. |
| #[cfg_attr( |
| feature = "serde", |
| serde(rename = "webkit-scrollbar", with = "ValueWrapper::<WebKitScrollbarPseudoElement>") |
| )] |
| WebKitScrollbar(WebKitScrollbarPseudoElement), |
| /// The [::cue](https://w3c.github.io/webvtt/#the-cue-pseudo-element) pseudo element. |
| Cue, |
| /// The [::cue-region](https://w3c.github.io/webvtt/#the-cue-region-pseudo-element) pseudo element. |
| CueRegion, |
| /// The [::cue()](https://w3c.github.io/webvtt/#cue-selector) functional pseudo element. |
| CueFunction { |
| /// The selector argument. |
| selector: Box<Selector<'i>>, |
| }, |
| /// The [::cue-region()](https://w3c.github.io/webvtt/#cue-region-selector) functional pseudo element. |
| CueRegionFunction { |
| /// The selector argument. |
| selector: Box<Selector<'i>>, |
| }, |
| /// The [::view-transition](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition) pseudo element. |
| ViewTransition, |
| /// The [::view-transition-group()](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition-group-pt-name-selector) functional pseudo element. |
| #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] |
| ViewTransitionGroup { |
| /// A part name selector. |
| part: ViewTransitionPartSelector<'i>, |
| }, |
| /// The [::view-transition-image-pair()](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition-image-pair-pt-name-selector) functional pseudo element. |
| #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] |
| ViewTransitionImagePair { |
| /// A part name selector. |
| part: ViewTransitionPartSelector<'i>, |
| }, |
| /// The [::view-transition-old()](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition-old-pt-name-selector) functional pseudo element. |
| #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] |
| ViewTransitionOld { |
| /// A part name selector. |
| part: ViewTransitionPartSelector<'i>, |
| }, |
| /// The [::view-transition-new()](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition-new-pt-name-selector) functional pseudo element. |
| #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] |
| ViewTransitionNew { |
| /// A part name selector. |
| part: ViewTransitionPartSelector<'i>, |
| }, |
| /// The [::picker()](https://drafts.csswg.org/css-forms-1/#the-picker-pseudo-element) functional pseudo element. |
| PickerFunction { |
| /// A form control identifier. |
| identifier: Ident<'i>, |
| }, |
| /// The [::picker-icon](https://drafts.csswg.org/css-forms-1/#picker-opener-icon-the-picker-icon-pseudo-element) pseudo element. |
| PickerIcon, |
| /// The [::checkmark](https://drafts.csswg.org/css-forms-1/#styling-checkmarks-the-checkmark-pseudo-element) pseudo element. |
| Checkmark, |
| /// An unknown pseudo element. |
| Custom { |
| /// The name of the pseudo element. |
| #[cfg_attr(feature = "serde", serde(borrow))] |
| name: CowArcStr<'i>, |
| }, |
| /// An unknown functional pseudo element. |
| CustomFunction { |
| ///The name of the pseudo element. |
| name: CowArcStr<'i>, |
| /// The arguments of the pseudo element function. |
| arguments: TokenList<'i>, |
| }, |
| } |
| |
| /// A [webkit scrollbar](https://webkit.org/blog/363/styling-scrollbars/) pseudo element. |
| #[derive(PartialEq, Eq, Clone, Debug, Hash)] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum WebKitScrollbarPseudoElement { |
| /// ::-webkit-scrollbar |
| Scrollbar, |
| /// ::-webkit-scrollbar-button |
| Button, |
| /// ::-webkit-scrollbar-track |
| Track, |
| /// ::-webkit-scrollbar-track-piece |
| TrackPiece, |
| /// ::-webkit-scrollbar-thumb |
| Thumb, |
| /// ::-webkit-scrollbar-corner |
| Corner, |
| /// ::-webkit-resizer |
| Resizer, |
| } |
| |
| /// A [view transition part name](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#typedef-pt-name-selector). |
| #[derive(PartialEq, Eq, Clone, Debug, Hash)] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum ViewTransitionPartName<'i> { |
| /// * |
| #[cfg_attr(feature = "serde", serde(rename = "*"))] |
| All, |
| /// <custom-ident> |
| #[cfg_attr(feature = "serde", serde(borrow, untagged))] |
| Name(CustomIdent<'i>), |
| } |
| |
| #[cfg(feature = "jsonschema")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))] |
| impl<'a> schemars::JsonSchema for ViewTransitionPartName<'a> { |
| fn is_referenceable() -> bool { |
| true |
| } |
| |
| fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { |
| str::json_schema(gen) |
| } |
| |
| fn schema_name() -> String { |
| "ViewTransitionPartName".into() |
| } |
| } |
| |
| impl<'i> Parse<'i> for ViewTransitionPartName<'i> { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| if input.try_parse(|input| input.expect_delim('*')).is_ok() { |
| return Ok(ViewTransitionPartName::All); |
| } |
| |
| Ok(ViewTransitionPartName::Name(CustomIdent::parse(input)?)) |
| } |
| } |
| |
| impl<'i> ToCss for ViewTransitionPartName<'i> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| match self { |
| ViewTransitionPartName::All => dest.write_char('*'), |
| ViewTransitionPartName::Name(name) => name.to_css(dest), |
| } |
| } |
| } |
| |
| /// A [view transition part selector](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#typedef-pt-name-selector). |
| #[derive(PartialEq, Eq, Clone, Debug, Hash)] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub struct ViewTransitionPartSelector<'i> { |
| /// The view transition part name. |
| #[cfg_attr(feature = "serde", serde(borrow))] |
| name: Option<ViewTransitionPartName<'i>>, |
| /// A list of view transition classes. |
| classes: Vec<CustomIdent<'i>>, |
| } |
| |
| impl<'i> Parse<'i> for ViewTransitionPartSelector<'i> { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| input.skip_whitespace(); |
| let name = input.try_parse(ViewTransitionPartName::parse).ok(); |
| let mut classes = Vec::new(); |
| while let Ok(token) = input.next_including_whitespace() { |
| if matches!(token, Token::Delim('.')) { |
| match input.next_including_whitespace() { |
| Ok(Token::Ident(id)) => classes.push(CustomIdent(id.into())), |
| _ => return Err(input.new_custom_error(ParserError::SelectorError(SelectorError::InvalidState))), |
| } |
| } else { |
| return Err(input.new_custom_error(ParserError::SelectorError(SelectorError::InvalidState))); |
| } |
| } |
| |
| Ok(ViewTransitionPartSelector { name, classes }) |
| } |
| } |
| |
| impl<'i> ToCss for ViewTransitionPartSelector<'i> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| if let Some(name) = &self.name { |
| name.to_css(dest)?; |
| } |
| for class in &self.classes { |
| dest.write_char('.')?; |
| class.to_css(dest)?; |
| } |
| Ok(()) |
| } |
| } |
| |
| impl<'i> cssparser::ToCss for PseudoElement<'i> { |
| fn to_css<W>(&self, dest: &mut W) -> std::fmt::Result |
| where |
| W: fmt::Write, |
| { |
| let mut s = String::new(); |
| serialize_pseudo_element(self, &mut Printer::new(&mut s, Default::default()), None) |
| .map_err(|_| std::fmt::Error)?; |
| write!(dest, "{}", s) |
| } |
| } |
| |
| fn serialize_pseudo_element<'a, 'i, W>( |
| pseudo_element: &PseudoElement, |
| dest: &mut Printer<W>, |
| context: Option<&StyleContext>, |
| ) -> Result<(), PrinterError> |
| where |
| W: fmt::Write, |
| { |
| use PseudoElement::*; |
| |
| macro_rules! write_prefix { |
| ($prefix: ident) => {{ |
| dest.write_str("::")?; |
| // If the printer has a vendor prefix override, use that. |
| let vp = if !dest.vendor_prefix.is_empty() { |
| (dest.vendor_prefix & *$prefix).or_none() |
| } else { |
| *$prefix |
| }; |
| vp.to_css(dest)?; |
| vp |
| }}; |
| } |
| |
| macro_rules! write_prefixed { |
| ($prefix: ident, $val: expr) => {{ |
| write_prefix!($prefix); |
| dest.write_str($val) |
| }}; |
| } |
| |
| match pseudo_element { |
| // CSS2 pseudo elements support a single colon syntax in addition |
| // to the more correct double colon for other pseudo elements. |
| // We use that here because it's supported everywhere and is shorter. |
| After => dest.write_str(":after"), |
| Before => dest.write_str(":before"), |
| FirstLine => dest.write_str(":first-line"), |
| FirstLetter => dest.write_str(":first-letter"), |
| DetailsContent => dest.write_str("::details-content"), |
| TargetText => dest.write_str("::target-text"), |
| Marker => dest.write_str("::marker"), |
| Selection(prefix) => write_prefixed!(prefix, "selection"), |
| Cue => dest.write_str("::cue"), |
| CueRegion => dest.write_str("::cue-region"), |
| CueFunction { selector } => { |
| dest.write_str("::cue(")?; |
| serialize_selector(selector, dest, context, false)?; |
| dest.write_char(')') |
| } |
| CueRegionFunction { selector } => { |
| dest.write_str("::cue-region(")?; |
| serialize_selector(selector, dest, context, false)?; |
| dest.write_char(')') |
| } |
| Placeholder(prefix) => { |
| let vp = write_prefix!(prefix); |
| if vp == VendorPrefix::WebKit || vp == VendorPrefix::Ms { |
| dest.write_str("input-placeholder") |
| } else { |
| dest.write_str("placeholder") |
| } |
| } |
| Backdrop(prefix) => write_prefixed!(prefix, "backdrop"), |
| FileSelectorButton(prefix) => { |
| let vp = write_prefix!(prefix); |
| if vp == VendorPrefix::WebKit { |
| dest.write_str("file-upload-button") |
| } else if vp == VendorPrefix::Ms { |
| dest.write_str("browse") |
| } else { |
| dest.write_str("file-selector-button") |
| } |
| } |
| WebKitScrollbar(s) => { |
| use WebKitScrollbarPseudoElement::*; |
| dest.write_str(match s { |
| Scrollbar => "::-webkit-scrollbar", |
| Button => "::-webkit-scrollbar-button", |
| Track => "::-webkit-scrollbar-track", |
| TrackPiece => "::-webkit-scrollbar-track-piece", |
| Thumb => "::-webkit-scrollbar-thumb", |
| Corner => "::-webkit-scrollbar-corner", |
| Resizer => "::-webkit-resizer", |
| }) |
| } |
| ViewTransition => dest.write_str("::view-transition"), |
| ViewTransitionGroup { part } => { |
| dest.write_str("::view-transition-group(")?; |
| part.to_css(dest)?; |
| dest.write_char(')') |
| } |
| ViewTransitionImagePair { part } => { |
| dest.write_str("::view-transition-image-pair(")?; |
| part.to_css(dest)?; |
| dest.write_char(')') |
| } |
| ViewTransitionOld { part } => { |
| dest.write_str("::view-transition-old(")?; |
| part.to_css(dest)?; |
| dest.write_char(')') |
| } |
| ViewTransitionNew { part } => { |
| dest.write_str("::view-transition-new(")?; |
| part.to_css(dest)?; |
| dest.write_char(')') |
| } |
| PickerFunction { identifier } => { |
| dest.write_str("::picker(")?; |
| identifier.to_css(dest)?; |
| dest.write_char(')') |
| } |
| PickerIcon => dest.write_str("::picker-icon"), |
| Checkmark => dest.write_str("::checkmark"), |
| Custom { name: val } => { |
| dest.write_str("::")?; |
| return dest.write_str(val); |
| } |
| CustomFunction { name, arguments: args } => { |
| dest.write_str("::")?; |
| dest.write_str(name)?; |
| dest.write_char('(')?; |
| args.to_css_raw(dest)?; |
| dest.write_char(')') |
| } |
| } |
| } |
| |
| impl<'i> parcel_selectors::parser::PseudoElement<'i> for PseudoElement<'i> { |
| type Impl = Selectors; |
| |
| fn accepts_state_pseudo_classes(&self) -> bool { |
| // Be lenient. |
| true |
| } |
| |
| fn valid_after_slotted(&self) -> bool { |
| // ::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 |
| matches!( |
| *self, |
| PseudoElement::Before |
| | PseudoElement::After |
| | PseudoElement::Marker |
| | PseudoElement::Placeholder(_) |
| | PseudoElement::FileSelectorButton(_) |
| ) |
| } |
| |
| fn is_webkit_scrollbar(&self) -> bool { |
| matches!(*self, PseudoElement::WebKitScrollbar(..)) |
| } |
| |
| fn is_view_transition(&self) -> bool { |
| matches!( |
| *self, |
| PseudoElement::ViewTransitionGroup { .. } |
| | PseudoElement::ViewTransitionImagePair { .. } |
| | PseudoElement::ViewTransitionNew { .. } |
| | PseudoElement::ViewTransitionOld { .. } |
| ) |
| } |
| |
| fn is_unknown(&self) -> bool { |
| matches!( |
| *self, |
| PseudoElement::Custom { .. } | PseudoElement::CustomFunction { .. }, |
| ) |
| } |
| } |
| |
| impl<'i> PseudoElement<'i> { |
| pub(crate) fn is_equivalent(&self, other: &PseudoElement<'i>) -> bool { |
| use PseudoElement::*; |
| match (self, other) { |
| (Selection(_), Selection(_)) |
| | (Placeholder(_), Placeholder(_)) |
| | (Backdrop(_), Backdrop(_)) |
| | (FileSelectorButton(_), FileSelectorButton(_)) => true, |
| (a, b) => a == b, |
| } |
| } |
| |
| pub(crate) fn get_prefix(&self) -> VendorPrefix { |
| use PseudoElement::*; |
| match self { |
| Selection(p) | Placeholder(p) | Backdrop(p) | FileSelectorButton(p) => *p, |
| _ => VendorPrefix::empty(), |
| } |
| } |
| |
| pub(crate) fn get_necessary_prefixes(&mut self, targets: Targets) -> VendorPrefix { |
| use crate::prefixes::Feature; |
| use PseudoElement::*; |
| let (p, feature) = match self { |
| Selection(p) => (p, Feature::PseudoElementSelection), |
| Placeholder(p) => (p, Feature::PseudoElementPlaceholder), |
| Backdrop(p) => (p, Feature::PseudoElementBackdrop), |
| FileSelectorButton(p) => (p, Feature::PseudoElementFileSelectorButton), |
| _ => return VendorPrefix::empty(), |
| }; |
| |
| *p = targets.prefixes(*p, feature); |
| *p |
| } |
| } |
| |
| impl<'a, 'i> ToCss for SelectorList<'i> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: fmt::Write, |
| { |
| serialize_selector_list(self.0.iter(), dest, dest.context(), false) |
| } |
| } |
| |
| impl ToCss for Combinator { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: fmt::Write, |
| { |
| match *self { |
| Combinator::Child => dest.delim('>', true), |
| Combinator::Descendant => dest.write_str(" "), |
| Combinator::NextSibling => dest.delim('+', true), |
| Combinator::LaterSibling => dest.delim('~', true), |
| Combinator::Deep => dest.write_str(" /deep/ "), |
| Combinator::DeepDescendant => { |
| dest.whitespace()?; |
| dest.write_str(">>>")?; |
| dest.whitespace() |
| } |
| Combinator::PseudoElement | Combinator::Part | Combinator::SlotAssignment => Ok(()), |
| } |
| } |
| } |
| |
| // Copied from the selectors crate and modified to override to_css implementation. |
| impl<'a, 'i> ToCss for Selector<'i> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: fmt::Write, |
| { |
| serialize_selector(self, dest, dest.context(), false) |
| } |
| } |
| |
| fn serialize_selector<'a, 'i, W>( |
| selector: &Selector<'i>, |
| dest: &mut Printer<W>, |
| context: Option<&StyleContext>, |
| mut is_relative: bool, |
| ) -> Result<(), PrinterError> |
| where |
| W: fmt::Write, |
| { |
| use parcel_selectors::parser::*; |
| // Compound selectors invert the order of their contents, so we need to |
| // undo that during serialization. |
| // |
| // This two-iterator strategy involves walking over the selector twice. |
| // We could do something more clever, but selector serialization probably |
| // isn't hot enough to justify it, and the stringification likely |
| // dominates anyway. |
| // |
| // NB: A parse-order iterator is a Rev<>, which doesn't expose as_slice(), |
| // which we need for |split|. So we split by combinators on a match-order |
| // sequence and then reverse. |
| |
| let mut combinators = selector.iter_raw_match_order().rev().filter_map(|x| x.as_combinator()); |
| let compound_selectors = selector.iter_raw_match_order().as_slice().split(|x| x.is_combinator()).rev(); |
| let should_compile_nesting = should_compile!(dest.targets.current, Nesting); |
| |
| let mut first = true; |
| let mut combinators_exhausted = false; |
| for mut compound in compound_selectors { |
| debug_assert!(!combinators_exhausted); |
| |
| // Skip implicit :scope in relative selectors (e.g. :has(:scope > foo) -> :has(> foo)) |
| if is_relative && matches!(compound.get(0), Some(Component::Scope)) { |
| if let Some(combinator) = combinators.next() { |
| combinator.to_css(dest)?; |
| } |
| compound = &compound[1..]; |
| is_relative = false; |
| } |
| |
| // https://drafts.csswg.org/cssom/#serializing-selectors |
| if compound.is_empty() { |
| continue; |
| } |
| |
| let has_leading_nesting = first && matches!(compound[0], Component::Nesting); |
| let first_index = if has_leading_nesting { 1 } else { 0 }; |
| first = false; |
| |
| // 1. If there is only one simple selector in the compound selectors |
| // which is a universal selector, append the result of |
| // serializing the universal selector to s. |
| // |
| // Check if `!compound.empty()` first--this can happen if we have |
| // something like `... > ::before`, because we store `>` and `::` |
| // both as combinators internally. |
| // |
| // If we are in this case, after we have serialized the universal |
| // selector, we skip Step 2 and continue with the algorithm. |
| let (can_elide_namespace, first_non_namespace) = match compound.get(first_index) { |
| Some(Component::ExplicitAnyNamespace) |
| | Some(Component::ExplicitNoNamespace) |
| | Some(Component::Namespace(..)) => (false, first_index + 1), |
| Some(Component::DefaultNamespace(..)) => (true, first_index + 1), |
| _ => (true, first_index), |
| }; |
| let mut perform_step_2 = true; |
| let next_combinator = combinators.next(); |
| if first_non_namespace == compound.len() - 1 { |
| match (next_combinator, &compound[first_non_namespace]) { |
| // We have to be careful here, because if there is a |
| // pseudo element "combinator" there isn't really just |
| // the one simple selector. Technically this compound |
| // selector contains the pseudo element selector as well |
| // -- Combinator::PseudoElement, just like |
| // Combinator::SlotAssignment, don't exist in the |
| // spec. |
| (Some(Combinator::PseudoElement), _) | (Some(Combinator::SlotAssignment), _) => (), |
| (_, &Component::ExplicitUniversalType) => { |
| // Iterate over everything so we serialize the namespace |
| // too. |
| let mut iter = compound.iter(); |
| let swap_nesting = has_leading_nesting && should_compile_nesting; |
| if swap_nesting { |
| // Swap nesting and type selector (e.g. &div -> div&). |
| iter.next(); |
| } |
| |
| for simple in iter { |
| serialize_component(simple, dest, context)?; |
| } |
| |
| if swap_nesting { |
| serialize_nesting(dest, context, false)?; |
| } |
| |
| // Skip step 2, which is an "otherwise". |
| perform_step_2 = false; |
| } |
| _ => (), |
| } |
| } |
| |
| // 2. Otherwise, for each simple selector in the compound selectors |
| // that is not a universal selector of which the namespace prefix |
| // maps to a namespace that is not the default namespace |
| // serialize the simple selector and append the result to s. |
| // |
| // See https://github.com/w3c/csswg-drafts/issues/1606, which is |
| // proposing to change this to match up with the behavior asserted |
| // in cssom/serialize-namespaced-type-selectors.html, which the |
| // following code tries to match. |
| if perform_step_2 { |
| let mut iter = compound.iter(); |
| if has_leading_nesting && should_compile_nesting && is_type_selector(compound.get(first_non_namespace)) { |
| // Swap nesting and type selector (e.g. &div -> div&). |
| // This ensures that the compiled selector is valid. e.g. (div.foo is valid, .foodiv is not). |
| let nesting = iter.next().unwrap(); |
| let local = iter.next().unwrap(); |
| serialize_component(local, dest, context)?; |
| |
| // Also check the next item in case of namespaces. |
| if first_non_namespace > first_index { |
| let local = iter.next().unwrap(); |
| serialize_component(local, dest, context)?; |
| } |
| |
| serialize_component(nesting, dest, context)?; |
| } else if has_leading_nesting && should_compile_nesting { |
| // Nesting selector may serialize differently if it is leading, due to type selectors. |
| iter.next(); |
| serialize_nesting(dest, context, true)?; |
| } |
| |
| for simple in iter { |
| if let Component::ExplicitUniversalType = *simple { |
| // Can't have a namespace followed by a pseudo-element |
| // selector followed by a universal selector in the same |
| // compound selector, so we don't have to worry about the |
| // real namespace being in a different `compound`. |
| if can_elide_namespace { |
| continue; |
| } |
| } |
| serialize_component(simple, dest, context)?; |
| } |
| } |
| |
| // 3. If this is not the last part of the chain of the selector |
| // append a single SPACE (U+0020), followed by the combinator |
| // ">", "+", "~", ">>", "||", as appropriate, followed by another |
| // single SPACE (U+0020) if the combinator was not whitespace, to |
| // s. |
| match next_combinator { |
| Some(c) => c.to_css(dest)?, |
| None => combinators_exhausted = true, |
| }; |
| |
| // 4. If this is the last part of the chain of the selector and |
| // there is a pseudo-element, append "::" followed by the name of |
| // the pseudo-element, to s. |
| // |
| // (we handle this above) |
| } |
| |
| Ok(()) |
| } |
| |
| fn serialize_component<'a, 'i, W>( |
| component: &Component, |
| dest: &mut Printer<W>, |
| context: Option<&StyleContext>, |
| ) -> Result<(), PrinterError> |
| where |
| W: fmt::Write, |
| { |
| match component { |
| Component::Combinator(ref c) => c.to_css(dest), |
| Component::AttributeInNoNamespace { |
| ref local_name, |
| operator, |
| ref value, |
| case_sensitivity, |
| .. |
| } => { |
| dest.write_char('[')?; |
| cssparser::ToCss::to_css(local_name, dest)?; |
| cssparser::ToCss::to_css(operator, dest)?; |
| |
| if dest.minify { |
| // Serialize as both an identifier and a string and choose the shorter one. |
| let mut id = String::new(); |
| serialize_identifier(&value, &mut id)?; |
| |
| let s = value.to_css_string(Default::default())?; |
| |
| if id.len() > 0 && id.len() < s.len() { |
| dest.write_str(&id)?; |
| } else { |
| dest.write_str(&s)?; |
| } |
| } else { |
| value.to_css(dest)?; |
| } |
| |
| match case_sensitivity { |
| parcel_selectors::attr::ParsedCaseSensitivity::CaseSensitive |
| | parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {} |
| parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?, |
| parcel_selectors::attr::ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?, |
| } |
| dest.write_char(']') |
| } |
| Component::Is(ref list) |
| | Component::Where(ref list) |
| | Component::Negation(ref list) |
| | Component::Any(_, ref list) => { |
| match *component { |
| Component::Where(..) => dest.write_str(":where(")?, |
| Component::Is(ref selectors) => { |
| // If there's only one simple selector, serialize it directly. |
| if should_unwrap_is(selectors) { |
| serialize_selector(selectors.first().unwrap(), dest, context, false)?; |
| return Ok(()); |
| } |
| |
| let vp = dest.vendor_prefix; |
| if vp.intersects(VendorPrefix::WebKit | VendorPrefix::Moz) { |
| dest.write_char(':')?; |
| vp.to_css(dest)?; |
| dest.write_str("any(")?; |
| } else { |
| dest.write_str(":is(")?; |
| } |
| } |
| Component::Negation(_) => { |
| dest.write_str(":not(")?; |
| } |
| Component::Any(prefix, ..) => { |
| let vp = dest.vendor_prefix.or(prefix); |
| if vp.intersects(VendorPrefix::WebKit | VendorPrefix::Moz) { |
| dest.write_char(':')?; |
| vp.to_css(dest)?; |
| dest.write_str("any(")?; |
| } else { |
| dest.write_str(":is(")?; |
| } |
| } |
| _ => unreachable!(), |
| } |
| serialize_selector_list(list.iter(), dest, context, false)?; |
| dest.write_str(")") |
| } |
| Component::Has(ref list) => { |
| dest.write_str(":has(")?; |
| serialize_selector_list(list.iter(), dest, context, true)?; |
| dest.write_str(")") |
| } |
| Component::NonTSPseudoClass(pseudo) => serialize_pseudo_class(pseudo, dest, context), |
| Component::PseudoElement(pseudo) => serialize_pseudo_element(pseudo, dest, context), |
| Component::Nesting => serialize_nesting(dest, context, false), |
| Component::Class(ref class) => { |
| dest.write_char('.')?; |
| dest.write_ident(&class.0, true) |
| } |
| Component::ID(ref id) => { |
| dest.write_char('#')?; |
| dest.write_ident(&id.0, true) |
| } |
| Component::Host(selector) => { |
| dest.write_str(":host")?; |
| if let Some(ref selector) = *selector { |
| dest.write_char('(')?; |
| selector.to_css(dest)?; |
| dest.write_char(')')?; |
| } |
| Ok(()) |
| } |
| Component::Slotted(ref selector) => { |
| dest.write_str("::slotted(")?; |
| selector.to_css(dest)?; |
| dest.write_char(')') |
| } |
| Component::NthOf(ref nth_of_data) => { |
| let nth_data = nth_of_data.nth_data(); |
| nth_data.write_start(dest, true)?; |
| nth_data.write_affine(dest)?; |
| dest.write_str(" of ")?; |
| serialize_selector_list(nth_of_data.selectors().iter(), dest, context, true)?; |
| dest.write_char(')') |
| } |
| _ => { |
| cssparser::ToCss::to_css(component, dest)?; |
| Ok(()) |
| } |
| } |
| } |
| |
| fn should_unwrap_is<'i>(selectors: &Box<[Selector<'i>]>) -> bool { |
| if selectors.len() == 1 { |
| let first = selectors.first().unwrap(); |
| if !has_type_selector(first) && is_simple(first) { |
| return true; |
| } |
| } |
| |
| false |
| } |
| |
| fn serialize_nesting<W>( |
| dest: &mut Printer<W>, |
| context: Option<&StyleContext>, |
| first: bool, |
| ) -> Result<(), PrinterError> |
| where |
| W: fmt::Write, |
| { |
| if let Some(ctx) = context { |
| // If there's only one simple selector, just serialize it directly. |
| // Otherwise, use an :is() pseudo class. |
| // Type selectors are only allowed at the start of a compound selector, |
| // so use :is() if that is not the case. |
| if ctx.selectors.0.len() == 1 |
| && (first || (!has_type_selector(&ctx.selectors.0[0]) && is_simple(&ctx.selectors.0[0]))) |
| { |
| serialize_selector(ctx.selectors.0.first().unwrap(), dest, ctx.parent, false) |
| } else { |
| dest.write_str(":is(")?; |
| serialize_selector_list(ctx.selectors.0.iter(), dest, ctx.parent, false)?; |
| dest.write_char(')') |
| } |
| } else { |
| // If there is no context, we are at the root if nesting is supported. This is equivalent to :scope. |
| // Otherwise, if nesting is supported, serialize the nesting selector directly. |
| if should_compile!(dest.targets.current, Nesting) { |
| dest.write_str(":scope") |
| } else { |
| dest.write_char('&') |
| } |
| } |
| } |
| |
| #[inline] |
| fn has_type_selector(selector: &Selector) -> bool { |
| // For input:checked the component vector is |
| // [input, :checked] so we have to check it using matching order. |
| // |
| // This both happens for input:checked and is(input:checked) |
| let mut iter = selector.iter_raw_match_order(); |
| let first = iter.next(); |
| |
| if is_namespace(first) { |
| is_type_selector(iter.next()) |
| } else { |
| is_type_selector(first) |
| } |
| } |
| |
| #[inline] |
| fn is_simple(selector: &Selector) -> bool { |
| !selector.iter_raw_match_order().any(|component| component.is_combinator()) |
| } |
| |
| #[inline] |
| fn is_type_selector(component: Option<&Component>) -> bool { |
| matches!( |
| component, |
| Some(Component::LocalName(_)) | Some(Component::ExplicitUniversalType) |
| ) |
| } |
| |
| #[inline] |
| fn is_namespace(component: Option<&Component>) -> bool { |
| matches!( |
| component, |
| Some(Component::ExplicitAnyNamespace) |
| | Some(Component::ExplicitNoNamespace) |
| | Some(Component::Namespace(..)) |
| | Some(Component::DefaultNamespace(_)) |
| ) |
| } |
| |
| fn serialize_selector_list<'a, 'i: 'a, I, W>( |
| iter: I, |
| dest: &mut Printer<W>, |
| context: Option<&StyleContext>, |
| is_relative: bool, |
| ) -> Result<(), PrinterError> |
| where |
| I: Iterator<Item = &'a Selector<'i>>, |
| W: fmt::Write, |
| { |
| let mut first = true; |
| for selector in iter { |
| if !first { |
| dest.delim(',', false)?; |
| } |
| first = false; |
| serialize_selector(selector, dest, context, is_relative)?; |
| } |
| Ok(()) |
| } |
| |
| pub(crate) fn is_compatible(selectors: &[Selector], targets: Targets) -> bool { |
| for selector in selectors { |
| let iter = selector.iter_raw_match_order(); |
| for component in iter { |
| let feature = match component { |
| Component::ID(_) | Component::Class(_) | Component::LocalName(_) => continue, |
| |
| Component::ExplicitAnyNamespace |
| | Component::ExplicitNoNamespace |
| | Component::DefaultNamespace(_) |
| | Component::Namespace(_, _) => Feature::Namespaces, |
| |
| Component::ExplicitUniversalType => Feature::Selectors2, |
| |
| Component::AttributeInNoNamespaceExists { .. } => Feature::Selectors2, |
| Component::AttributeInNoNamespace { |
| operator, |
| case_sensitivity, |
| .. |
| } => { |
| if *case_sensitivity != ParsedCaseSensitivity::CaseSensitive { |
| Feature::CaseInsensitive |
| } else { |
| match operator { |
| AttrSelectorOperator::Equal | AttrSelectorOperator::Includes | AttrSelectorOperator::DashMatch => { |
| Feature::Selectors2 |
| } |
| AttrSelectorOperator::Prefix | AttrSelectorOperator::Substring | AttrSelectorOperator::Suffix => { |
| Feature::Selectors3 |
| } |
| } |
| } |
| } |
| Component::AttributeOther(attr) => match attr.operation { |
| ParsedAttrSelectorOperation::Exists => Feature::Selectors2, |
| ParsedAttrSelectorOperation::WithValue { |
| operator, |
| case_sensitivity, |
| .. |
| } => { |
| if case_sensitivity != ParsedCaseSensitivity::CaseSensitive { |
| Feature::CaseInsensitive |
| } else { |
| match operator { |
| AttrSelectorOperator::Equal | AttrSelectorOperator::Includes | AttrSelectorOperator::DashMatch => { |
| Feature::Selectors2 |
| } |
| AttrSelectorOperator::Prefix | AttrSelectorOperator::Substring | AttrSelectorOperator::Suffix => { |
| Feature::Selectors3 |
| } |
| } |
| } |
| } |
| }, |
| |
| Component::Empty | Component::Root => Feature::Selectors3, |
| Component::Negation(selectors) => { |
| // :not() selector list is not forgiving. |
| if !targets.is_compatible(Feature::Selectors3) || !is_compatible(&*selectors, targets) { |
| return false; |
| } |
| continue; |
| } |
| |
| Component::Nth(data) => match data.ty { |
| NthType::Child if data.a == 0 && data.b == 1 => Feature::Selectors2, |
| NthType::Col | NthType::LastCol => return false, |
| _ => Feature::Selectors3, |
| }, |
| Component::NthOf(n) => { |
| if !targets.is_compatible(Feature::NthChildOf) || !is_compatible(n.selectors(), targets) { |
| return false; |
| } |
| continue; |
| } |
| |
| // These support forgiving selector lists, so no need to check nested selectors. |
| Component::Is(selectors) => { |
| // ... except if we are going to unwrap them. |
| if should_unwrap_is(selectors) && is_compatible(selectors, targets) { |
| continue; |
| } |
| |
| Feature::IsSelector |
| } |
| Component::Where(_) | Component::Nesting => Feature::IsSelector, |
| Component::Any(..) => return false, |
| Component::Has(selectors) => { |
| if !targets.is_compatible(Feature::HasSelector) || !is_compatible(&*selectors, targets) { |
| return false; |
| } |
| continue; |
| } |
| |
| Component::Scope | Component::Host(_) | Component::Slotted(_) => Feature::Shadowdomv1, |
| |
| Component::Part(_) => Feature::PartPseudo, |
| |
| Component::NonTSPseudoClass(pseudo) => { |
| match pseudo { |
| PseudoClass::Link |
| | PseudoClass::Visited |
| | PseudoClass::Active |
| | PseudoClass::Hover |
| | PseudoClass::Focus |
| | PseudoClass::Lang { languages: _ } => Feature::Selectors2, |
| |
| PseudoClass::Checked | PseudoClass::Disabled | PseudoClass::Enabled | PseudoClass::Target => { |
| Feature::Selectors3 |
| } |
| |
| PseudoClass::AnyLink(prefix) if *prefix == VendorPrefix::None => Feature::AnyLink, |
| PseudoClass::Indeterminate => Feature::IndeterminatePseudo, |
| |
| PseudoClass::Fullscreen(prefix) if *prefix == VendorPrefix::None => Feature::Fullscreen, |
| |
| PseudoClass::FocusVisible => Feature::FocusVisible, |
| PseudoClass::FocusWithin => Feature::FocusWithin, |
| PseudoClass::Default => Feature::DefaultPseudo, |
| PseudoClass::Dir { direction: _ } => Feature::DirSelector, |
| PseudoClass::Optional => Feature::OptionalPseudo, |
| PseudoClass::PlaceholderShown(prefix) if *prefix == VendorPrefix::None => Feature::PlaceholderShown, |
| |
| PseudoClass::ReadOnly(prefix) | PseudoClass::ReadWrite(prefix) if *prefix == VendorPrefix::None => { |
| Feature::ReadOnlyWrite |
| } |
| |
| PseudoClass::Valid | PseudoClass::Invalid | PseudoClass::Required => Feature::FormValidation, |
| |
| PseudoClass::InRange | PseudoClass::OutOfRange => Feature::InOutOfRange, |
| |
| PseudoClass::Autofill(prefix) if *prefix == VendorPrefix::None => Feature::Autofill, |
| |
| // Experimental, no browser support. |
| PseudoClass::Current |
| | PseudoClass::Past |
| | PseudoClass::Future |
| | PseudoClass::Playing |
| | PseudoClass::Paused |
| | PseudoClass::Seeking |
| | PseudoClass::Stalled |
| | PseudoClass::Buffering |
| | PseudoClass::Muted |
| | PseudoClass::VolumeLocked |
| | PseudoClass::TargetWithin |
| | PseudoClass::LocalLink |
| | PseudoClass::Blank |
| | PseudoClass::UserInvalid |
| | PseudoClass::UserValid |
| | PseudoClass::Defined |
| | PseudoClass::ActiveViewTransition |
| | PseudoClass::ActiveViewTransitionType { .. } => return false, |
| |
| PseudoClass::Custom { .. } | _ => return false, |
| } |
| } |
| |
| Component::PseudoElement(pseudo) => match pseudo { |
| PseudoElement::After | PseudoElement::Before => Feature::Gencontent, |
| PseudoElement::FirstLine => Feature::FirstLine, |
| PseudoElement::FirstLetter => Feature::FirstLetter, |
| PseudoElement::DetailsContent => Feature::DetailsContent, |
| PseudoElement::TargetText => Feature::TargetText, |
| PseudoElement::Selection(prefix) if *prefix == VendorPrefix::None => Feature::Selection, |
| PseudoElement::Placeholder(prefix) if *prefix == VendorPrefix::None => Feature::Placeholder, |
| PseudoElement::Marker => Feature::MarkerPseudo, |
| PseudoElement::Backdrop(prefix) if *prefix == VendorPrefix::None => Feature::Dialog, |
| PseudoElement::Cue => Feature::Cue, |
| PseudoElement::CueFunction { selector: _ } => Feature::CueFunction, |
| PseudoElement::ViewTransition |
| | PseudoElement::ViewTransitionNew { .. } |
| | PseudoElement::ViewTransitionOld { .. } |
| | PseudoElement::ViewTransitionGroup { .. } |
| | PseudoElement::ViewTransitionImagePair { .. } => Feature::ViewTransition, |
| PseudoElement::PickerFunction { identifier: _ } => Feature::Picker, |
| PseudoElement::PickerIcon => Feature::PickerIcon, |
| PseudoElement::Checkmark => Feature::Checkmark, |
| PseudoElement::Custom { name: _ } | _ => return false, |
| }, |
| |
| Component::Combinator(combinator) => match combinator { |
| Combinator::Child | Combinator::NextSibling => Feature::Selectors2, |
| Combinator::LaterSibling => Feature::Selectors3, |
| _ => continue, |
| }, |
| }; |
| |
| if !targets.is_compatible(feature) { |
| return false; |
| } |
| } |
| } |
| |
| true |
| } |
| |
| /// Returns whether two selector lists are equivalent, i.e. the same minus any vendor prefix differences. |
| pub(crate) fn is_equivalent<'i>(selectors: &[Selector<'i>], other: &[Selector<'i>]) -> bool { |
| if selectors.len() != other.len() { |
| return false; |
| } |
| |
| for (i, a) in selectors.iter().enumerate() { |
| let b = &other[i]; |
| if a.len() != b.len() { |
| return false; |
| } |
| |
| for (a, b) in a.iter_raw_match_order().zip(b.iter_raw_match_order()) { |
| let is_equivalent = match (a, b) { |
| (Component::NonTSPseudoClass(a_ps), Component::NonTSPseudoClass(b_ps)) => a_ps.is_equivalent(b_ps), |
| (Component::PseudoElement(a_pe), Component::PseudoElement(b_pe)) => a_pe.is_equivalent(b_pe), |
| (Component::Any(_, a), Component::Is(b)) |
| | (Component::Is(a), Component::Any(_, b)) |
| | (Component::Any(_, a), Component::Any(_, b)) |
| | (Component::Is(a), Component::Is(b)) => is_equivalent(&*a, &*b), |
| (a, b) => a == b, |
| }; |
| |
| if !is_equivalent { |
| return false; |
| } |
| } |
| } |
| |
| true |
| } |
| |
| /// Returns the vendor prefix (if any) used in the given selector list. |
| /// If multiple vendor prefixes are seen, this is invalid, and an empty result is returned. |
| pub(crate) fn get_prefix(selectors: &SelectorList) -> VendorPrefix { |
| let mut prefix = VendorPrefix::empty(); |
| for selector in &selectors.0 { |
| for component in selector.iter_raw_match_order() { |
| let p = match component { |
| // Return none rather than empty for these so that we call downlevel_selectors. |
| Component::NonTSPseudoClass(PseudoClass::Lang { .. }) |
| | Component::NonTSPseudoClass(PseudoClass::Dir { .. }) |
| | Component::Is(..) |
| | Component::Where(..) |
| | Component::Has(..) |
| | Component::Negation(..) => VendorPrefix::None, |
| Component::Any(prefix, _) => *prefix, |
| Component::NonTSPseudoClass(pc) => pc.get_prefix(), |
| Component::PseudoElement(pe) => pe.get_prefix(), |
| _ => VendorPrefix::empty(), |
| }; |
| |
| if !p.is_empty() { |
| // Allow none to be mixed with a prefix. |
| let prefix_without_none = prefix - VendorPrefix::None; |
| if prefix_without_none.is_empty() || prefix_without_none == p { |
| prefix |= p; |
| } else { |
| return VendorPrefix::empty(); |
| } |
| } |
| } |
| } |
| |
| prefix |
| } |
| |
| const RTL_LANGS: &[&str] = &[ |
| "ae", "ar", "arc", "bcc", "bqi", "ckb", "dv", "fa", "glk", "he", "ku", "mzn", "nqo", "pnb", "ps", "sd", "ug", |
| "ur", "yi", |
| ]; |
| |
| /// Downlevels the given selectors to be compatible with the given browser targets. |
| /// Returns the necessary vendor prefixes. |
| pub(crate) fn downlevel_selectors(selectors: &mut [Selector], targets: Targets) -> VendorPrefix { |
| let mut necessary_prefixes = VendorPrefix::empty(); |
| for selector in selectors { |
| for component in selector.iter_mut_raw_match_order() { |
| necessary_prefixes |= downlevel_component(component, targets); |
| } |
| } |
| |
| necessary_prefixes |
| } |
| |
| fn downlevel_component<'i>(component: &mut Component<'i>, targets: Targets) -> VendorPrefix { |
| match component { |
| Component::NonTSPseudoClass(pc) => { |
| match pc { |
| PseudoClass::Dir { direction: dir } => { |
| if should_compile!(targets, DirSelector) { |
| *component = downlevel_dir(*dir, targets); |
| downlevel_component(component, targets) |
| } else { |
| VendorPrefix::empty() |
| } |
| } |
| PseudoClass::Lang { languages: langs } => { |
| // :lang() with multiple languages is not supported everywhere. |
| // compile this to :is(:lang(a), :lang(b)) etc. |
| if langs.len() > 1 && should_compile!(targets, LangSelectorList) { |
| *component = Component::Is(lang_list_to_selectors(&langs)); |
| downlevel_component(component, targets) |
| } else { |
| VendorPrefix::empty() |
| } |
| } |
| _ => pc.get_necessary_prefixes(targets), |
| } |
| } |
| Component::PseudoElement(pe) => pe.get_necessary_prefixes(targets), |
| Component::Is(selectors) => { |
| let mut necessary_prefixes = downlevel_selectors(&mut **selectors, targets); |
| |
| // Convert :is to :-webkit-any/:-moz-any if needed. |
| // All selectors must be simple, no combinators are supported. |
| if should_compile!(targets, IsSelector) |
| && !should_unwrap_is(selectors) |
| && selectors.iter().all(|selector| !selector.has_combinator()) |
| { |
| necessary_prefixes |= targets.prefixes(VendorPrefix::None, crate::prefixes::Feature::AnyPseudo) |
| } else { |
| necessary_prefixes |= VendorPrefix::None |
| } |
| |
| necessary_prefixes |
| } |
| Component::Negation(selectors) => { |
| let mut necessary_prefixes = downlevel_selectors(&mut **selectors, targets); |
| |
| // Downlevel :not(.a, .b) -> :not(:is(.a, .b)) if not list is unsupported. |
| // We need to use :is() / :-webkit-any() rather than :not(.a):not(.b) to ensure the specificity is equivalent. |
| // https://drafts.csswg.org/selectors/#specificity-rules |
| if selectors.len() > 1 && should_compile!(targets, NotSelectorList) { |
| *component = |
| Component::Negation(vec![Selector::from(Component::Is(selectors.clone()))].into_boxed_slice()); |
| |
| if should_compile!(targets, IsSelector) { |
| necessary_prefixes |= targets.prefixes(VendorPrefix::None, crate::prefixes::Feature::AnyPseudo) |
| } else { |
| necessary_prefixes |= VendorPrefix::None |
| } |
| } |
| |
| necessary_prefixes |
| } |
| Component::Where(selectors) | Component::Any(_, selectors) | Component::Has(selectors) => { |
| downlevel_selectors(&mut **selectors, targets) |
| } |
| _ => VendorPrefix::empty(), |
| } |
| } |
| |
| fn lang_list_to_selectors<'i>(langs: &Vec<CowArcStr<'i>>) -> Box<[Selector<'i>]> { |
| langs |
| .iter() |
| .map(|lang| { |
| Selector::from(Component::NonTSPseudoClass(PseudoClass::Lang { |
| languages: vec![lang.clone()], |
| })) |
| }) |
| .collect::<Vec<Selector>>() |
| .into_boxed_slice() |
| } |
| |
| fn downlevel_dir<'i>(dir: Direction, targets: Targets) -> Component<'i> { |
| // Convert :dir to :lang. If supported, use a list of languages in a single :lang, |
| // otherwise, use :is/:not, which may be further downleveled to e.g. :-webkit-any. |
| let langs = RTL_LANGS.iter().map(|lang| (*lang).into()).collect(); |
| if !should_compile!(targets, LangSelectorList) { |
| let c = Component::NonTSPseudoClass(PseudoClass::Lang { languages: langs }); |
| if dir == Direction::Ltr { |
| Component::Negation(vec![Selector::from(c)].into_boxed_slice()) |
| } else { |
| c |
| } |
| } else { |
| if dir == Direction::Ltr { |
| Component::Negation(lang_list_to_selectors(&langs)) |
| } else { |
| Component::Is(lang_list_to_selectors(&langs)) |
| } |
| } |
| } |
| |
| /// Determines whether a selector list contains only unused selectors. |
| /// A selector is considered unused if it contains a class or id component that exists in the set of unused symbols. |
| pub(crate) fn is_unused( |
| selectors: &mut std::slice::Iter<Selector>, |
| unused_symbols: &HashSet<String>, |
| parent_is_unused: bool, |
| ) -> bool { |
| if unused_symbols.is_empty() { |
| return false; |
| } |
| |
| selectors.all(|selector| { |
| for component in selector.iter_raw_match_order() { |
| match component { |
| Component::Class(name) | Component::ID(name) => { |
| if unused_symbols.contains(&name.0.to_string()) { |
| return true; |
| } |
| } |
| Component::Is(is) | Component::Where(is) | Component::Any(_, is) => { |
| if is_unused(&mut is.iter(), unused_symbols, parent_is_unused) { |
| return true; |
| } |
| } |
| Component::Nesting => { |
| if parent_is_unused { |
| return true; |
| } |
| } |
| _ => {} |
| } |
| } |
| |
| false |
| }) |
| } |
| |
| /// Returns whether the selector has any class or id components. |
| pub(crate) fn is_pure_css_modules_selector(selector: &Selector) -> bool { |
| use parcel_selectors::parser::Component; |
| selector.iter_raw_match_order().any(|c| match c { |
| Component::Class(_) | Component::ID(_) => true, |
| Component::Is(s) | Component::Where(s) | Component::Has(s) | Component::Any(_, s) | Component::Negation(s) => { |
| s.iter().any(is_pure_css_modules_selector) |
| } |
| Component::NthOf(nth) => nth.selectors().iter().any(is_pure_css_modules_selector), |
| Component::Slotted(s) => is_pure_css_modules_selector(&s), |
| Component::Host(s) => s.as_ref().map(is_pure_css_modules_selector).unwrap_or(false), |
| Component::NonTSPseudoClass(pc) => match pc { |
| PseudoClass::Local { selector } => is_pure_css_modules_selector(&*selector), |
| _ => false, |
| }, |
| _ => false, |
| }) |
| } |
| |
| #[cfg(feature = "visitor")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "visitor")))] |
| impl<'i, T: Visit<'i, T, V>, V: ?Sized + Visitor<'i, T>> Visit<'i, T, V> for SelectorList<'i> { |
| const CHILD_TYPES: VisitTypes = VisitTypes::SELECTORS; |
| |
| fn visit(&mut self, visitor: &mut V) -> Result<(), V::Error> { |
| if visitor.visit_types().contains(VisitTypes::SELECTORS) { |
| visitor.visit_selector_list(self) |
| } else { |
| self.visit_children(visitor) |
| } |
| } |
| |
| fn visit_children(&mut self, visitor: &mut V) -> Result<(), V::Error> { |
| self.0.iter_mut().try_for_each(|selector| Visit::visit(selector, visitor)) |
| } |
| } |
| |
| #[cfg(feature = "visitor")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "visitor")))] |
| impl<'i, T: Visit<'i, T, V>, V: ?Sized + Visitor<'i, T>> Visit<'i, T, V> for Selector<'i> { |
| const CHILD_TYPES: VisitTypes = VisitTypes::SELECTORS; |
| |
| fn visit(&mut self, visitor: &mut V) -> Result<(), V::Error> { |
| visitor.visit_selector(self) |
| } |
| |
| fn visit_children(&mut self, _visitor: &mut V) -> Result<(), V::Error> { |
| Ok(()) |
| } |
| } |
| |
| impl<'i> ParseWithOptions<'i> for Selector<'i> { |
| fn parse_with_options<'t>( |
| input: &mut Parser<'i, 't>, |
| options: &ParserOptions<'_, 'i>, |
| ) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| Selector::parse( |
| &SelectorParser { |
| is_nesting_allowed: true, |
| options: &options, |
| }, |
| input, |
| ) |
| } |
| } |
| |
| impl<'i> ParseWithOptions<'i> for SelectorList<'i> { |
| fn parse_with_options<'t>( |
| input: &mut Parser<'i, 't>, |
| options: &ParserOptions<'_, 'i>, |
| ) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| SelectorList::parse( |
| &SelectorParser { |
| is_nesting_allowed: true, |
| options: &options, |
| }, |
| input, |
| parcel_selectors::parser::ParseErrorRecovery::DiscardList, |
| parcel_selectors::parser::NestingRequirement::None, |
| ) |
| } |
| } |