From c2c5065ac4f590c6a868d7d55ad7104757f7d1b5 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Tue, 18 Apr 2023 12:53:06 -0400 Subject: [PATCH] Add support for the view transition pseudo elements Fixes #443 --- node/ast.d.ts | 39 ++++++++++++ selectors/parser.rs | 17 +++++ src/lib.rs | 32 ++++++++++ src/properties/mod.rs | 5 +- src/selector.rs | 142 +++++++++++++++++++++++++++++++++++++++++- 5 files changed, 233 insertions(+), 2 deletions(-) diff --git a/node/ast.d.ts b/node/ast.d.ts index 855860df..1df89bf7 100644 --- a/node/ast.d.ts +++ b/node/ast.d.ts @@ -2222,6 +2222,9 @@ export type PropertyId = | { property: "container"; } + | { + property: "view-transition-name"; + } | { property: "all"; } @@ -3657,6 +3660,10 @@ export type Declaration = property: "container"; value: Container; } + | { + property: "view-transition-name"; + value: String; + } | { property: "unparsed"; value: UnparsedProperty; @@ -6507,6 +6514,37 @@ export type PseudoElement = */ selector: Selector; } + | { + kind: "view-transition"; + } + | { + kind: "view-transition-group"; + /** + * A part name selector. + */ + partName: ViewTransitionPartName; + } + | { + kind: "view-transition-image-pair"; + /** + * A part name selector. + */ + partName: ViewTransitionPartName; + } + | { + kind: "view-transition-old"; + /** + * A part name selector. + */ + partName: ViewTransitionPartName; + } + | { + kind: "view-transition-new"; + /** + * A part name selector. + */ + partName: ViewTransitionPartName; + } | { kind: "custom"; /** @@ -6536,6 +6574,7 @@ export type WebKitScrollbarPseudoElement = | "thumb" | "corner" | "resizer"; +export type ViewTransitionPartName = string; export type Selector = SelectorComponent[]; export type SelectorList = Selector[]; /** diff --git a/selectors/parser.rs b/selectors/parser.rs index f09200fe..e154c734 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -40,6 +40,10 @@ pub trait PseudoElement<'i>: Sized + ToCss { fn is_webkit_scrollbar(&self) -> bool { false } + + fn is_view_transition(&self) -> bool { + false + } } /// A trait that represents a pseudo-class. @@ -126,6 +130,7 @@ bitflags! { const AFTER_NESTING = 1 << 7; const AFTER_WEBKIT_SCROLLBAR = 1 << 8; + const AFTER_VIEW_TRANSITION = 1 << 9; } } @@ -2601,6 +2606,9 @@ where if p.is_webkit_scrollbar() { state.insert(SelectorParsingState::AFTER_WEBKIT_SCROLLBAR); } + if p.is_view_transition() { + state.insert(SelectorParsingState::AFTER_VIEW_TRANSITION); + } builder.push_combinator(Combinator::PseudoElement); builder.push_simple_selector(Component::PseudoElement(p)); } @@ -2916,6 +2924,15 @@ where } } + // The view-transition pseudo elements accept the :only-child pseudo class. + // https://w3c.github.io/csswg-drafts/css-view-transitions-1/#pseudo-root + if state.intersects(SelectorParsingState::AFTER_VIEW_TRANSITION) { + match_ignore_ascii_case! { &name, + "only-child" => return Ok(Component::Nth(NthSelectorData::only(/* of_type = */ false))), + _ => {} + } + } + let pseudo_class = P::parse_non_ts_pseudo_class(parser, location, name)?; if state.intersects(SelectorParsingState::AFTER_WEBKIT_SCROLLBAR) { if !pseudo_class.is_valid_after_webkit_scrollbar() { diff --git a/src/lib.rs b/src/lib.rs index ea5dbddb..7511e5da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5764,6 +5764,38 @@ mod tests { minify_test("a:is(:is(.foo)) { color: yellow }", "a.foo{color:#ff0}"); minify_test(":host(:hover) {color: red}", ":host(:hover){color:red}"); minify_test("::slotted(:hover) {color: red}", "::slotted(:hover){color:red}"); + + minify_test( + ":root::view-transition {position: fixed}", + ":root::view-transition{position:fixed}", + ); + for name in &[ + "view-transition-group", + "view-transition-image-pair", + "view-transition-new", + "view-transition-old", + ] { + minify_test( + &format!(":root::{}(*) {{position: fixed}}", name), + &format!(":root::{}(*){{position:fixed}}", name), + ); + minify_test( + &format!(":root::{}(foo) {{position: fixed}}", name), + &format!(":root::{}(foo){{position:fixed}}", name), + ); + minify_test( + &format!(":root::{}(foo):only-child {{position: fixed}}", name), + &format!(":root::{}(foo):only-child{{position:fixed}}", name), + ); + error_test( + &format!(":root::{}(foo):first-child {{position: fixed}}", name), + ParserError::SelectorError(SelectorError::InvalidPseudoClassAfterPseudoElement), + ); + error_test( + &format!(":root::{}(foo)::before {{position: fixed}}", name), + ParserError::SelectorError(SelectorError::InvalidState), + ); + } } #[test] diff --git a/src/properties/mod.rs b/src/properties/mod.rs index ec80aa7e..8be1b363 100644 --- a/src/properties/mod.rs +++ b/src/properties/mod.rs @@ -132,7 +132,7 @@ use crate::traits::{Parse, ParseWithOptions, Shorthand, ToCss}; use crate::values::number::{CSSInteger, CSSNumber}; use crate::values::string::CowArcStr; use crate::values::{ - alpha::*, color::*, easing::EasingFunction, ident::DashedIdentReference, image::*, length::*, position::*, + alpha::*, color::*, easing::EasingFunction, ident::DashedIdentReference, ident::CustomIdent, image::*, length::*, position::*, rect::*, shape::FillRule, size::Size2D, time::Time, }; use crate::vendor_prefix::VendorPrefix; @@ -1579,6 +1579,9 @@ define_properties! { "container-type": ContainerType(ContainerType), "container-name": ContainerName(ContainerNameList<'i>), "container": Container(Container<'i>) shorthand: true, + + // https://w3c.github.io/csswg-drafts/css-view-transitions-1/ + "view-transition-name": ViewTransitionName(CustomIdent<'i>), } impl<'i, T: smallvec::Array, V: Parse<'i>> Parse<'i> for SmallVec { diff --git a/src/selector.rs b/src/selector.rs index f63e7ac3..181335bc 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -8,7 +8,7 @@ use crate::rules::StyleContext; use crate::stylesheet::{ParserOptions, PrinterOptions}; use crate::targets::Browsers; use crate::traits::{Parse, ParseWithOptions, ToCss}; -use crate::values::ident::Ident; +use crate::values::ident::{CustomIdent, Ident}; use crate::values::string::CSSString; use crate::vendor_prefix::VendorPrefix; #[cfg(feature = "visitor")] @@ -246,6 +246,8 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, "-webkit-scrollbar-corner" => WebKitScrollbar(WebKitScrollbarPseudoElement::Corner), "-webkit-resizer" => WebKitScrollbar(WebKitScrollbarPseudoElement::Resizer), + "view-transition" => ViewTransition, + _ => { if !name.starts_with('-') { self.options.warn(loc.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name.clone()))); @@ -266,6 +268,10 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, 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)?) }, + "view-transition-group" => ViewTransitionGroup { part_name: ViewTransitionPartName::parse(arguments)? }, + "view-transition-image-pair" => ViewTransitionImagePair { part_name: ViewTransitionPartName::parse(arguments)? }, + "view-transition-old" => ViewTransitionOld { part_name: ViewTransitionPartName::parse(arguments)? }, + "view-transition-new" => ViewTransitionNew { part_name: ViewTransitionPartName::parse(arguments)? }, _ => { if !name.starts_with('-') { self.options.warn(arguments.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name.clone()))); @@ -819,6 +825,32 @@ pub enum PseudoElement<'i> { /// The selector argument. selector: Box>, }, + /// 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_name: ViewTransitionPartName<'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_name: ViewTransitionPartName<'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_name: ViewTransitionPartName<'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_name: ViewTransitionPartName<'i>, + }, /// An unknown pseudo element. Custom { /// The name of the pseudo element. @@ -859,6 +891,83 @@ pub enum WebKitScrollbarPseudoElement { 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)] +pub enum ViewTransitionPartName<'i> { + /// * + All, + /// + Name(CustomIdent<'i>), +} + +#[cfg(feature = "serde")] +#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] +impl<'i> serde::Serialize for ViewTransitionPartName<'i> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + ViewTransitionPartName::All => serializer.serialize_str("*"), + ViewTransitionPartName::Name(name) => serializer.serialize_str(&name.0), + } + } +} + +#[cfg(feature = "serde")] +#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] +impl<'i, 'de: 'i> serde::Deserialize<'de> for ViewTransitionPartName<'i> { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = CowArcStr::deserialize(deserializer)?; + if s == "*" { + Ok(ViewTransitionPartName::All) + } else { + Ok(ViewTransitionPartName::Name(CustomIdent(s))) + } + } +} + +#[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>> { + 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(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + match self { + ViewTransitionPartName::All => dest.write_char('*'), + ViewTransitionPartName::Name(name) => name.to_css(dest), + } + } +} + impl<'i> cssparser::ToCss for PseudoElement<'i> { fn to_css(&self, _: &mut W) -> std::fmt::Result where @@ -952,6 +1061,27 @@ where Resizer => "::-webkit-resizer", }) } + ViewTransition => dest.write_str("::view-transition"), + ViewTransitionGroup { part_name } => { + dest.write_str("::view-transition-group(")?; + part_name.to_css(dest)?; + dest.write_char(')') + } + ViewTransitionImagePair { part_name } => { + dest.write_str("::view-transition-image-pair(")?; + part_name.to_css(dest)?; + dest.write_char(')') + } + ViewTransitionOld { part_name } => { + dest.write_str("::view-transition-old(")?; + part_name.to_css(dest)?; + dest.write_char(')') + } + ViewTransitionNew { part_name } => { + dest.write_str("::view-transition-new(")?; + part_name.to_css(dest)?; + dest.write_char(')') + } Custom { name: val } => { dest.write_str("::")?; return dest.write_str(val); @@ -991,6 +1121,16 @@ impl<'i> parcel_selectors::parser::PseudoElement<'i> for PseudoElement<'i> { 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 { .. } + ) + } } impl<'i> PseudoElement<'i> {