Skip to content

Commit

Permalink
Add support for the view transition pseudo elements
Browse files Browse the repository at this point in the history
Fixes #443
  • Loading branch information
devongovett committed Apr 18, 2023
1 parent f333cd9 commit c2c5065
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 2 deletions.
39 changes: 39 additions & 0 deletions node/ast.d.ts
Expand Up @@ -2222,6 +2222,9 @@ export type PropertyId =
| {
property: "container";
}
| {
property: "view-transition-name";
}
| {
property: "all";
}
Expand Down Expand Up @@ -3657,6 +3660,10 @@ export type Declaration =
property: "container";
value: Container;
}
| {
property: "view-transition-name";
value: String;
}
| {
property: "unparsed";
value: UnparsedProperty;
Expand Down Expand Up @@ -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";
/**
Expand Down Expand Up @@ -6536,6 +6574,7 @@ export type WebKitScrollbarPseudoElement =
| "thumb"
| "corner"
| "resizer";
export type ViewTransitionPartName = string;
export type Selector = SelectorComponent[];
export type SelectorList = Selector[];
/**
Expand Down
17 changes: 17 additions & 0 deletions selectors/parser.rs
Expand Up @@ -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.
Expand Down Expand Up @@ -126,6 +130,7 @@ bitflags! {
const AFTER_NESTING = 1 << 7;

const AFTER_WEBKIT_SCROLLBAR = 1 << 8;
const AFTER_VIEW_TRANSITION = 1 << 9;
}
}

Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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() {
Expand Down
32 changes: 32 additions & 0 deletions src/lib.rs
Expand Up @@ -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]
Expand Down
5 changes: 4 additions & 1 deletion src/properties/mod.rs
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Item = V>, V: Parse<'i>> Parse<'i> for SmallVec<T> {
Expand Down
142 changes: 141 additions & 1 deletion src/selector.rs
Expand Up @@ -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")]
Expand Down Expand Up @@ -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())));
Expand All @@ -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())));
Expand Down Expand Up @@ -819,6 +825,32 @@ pub enum PseudoElement<'i> {
/// 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_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.
Expand Down Expand Up @@ -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,
/// <custom-ident>
Name(CustomIdent<'i>),
}

#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
impl<'i> serde::Serialize for ViewTransitionPartName<'i> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
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<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),
}
}
}

impl<'i> cssparser::ToCss for PseudoElement<'i> {
fn to_css<W>(&self, _: &mut W) -> std::fmt::Result
where
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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> {
Expand Down

0 comments on commit c2c5065

Please sign in to comment.