diff --git a/napi/src/transformer.rs b/napi/src/transformer.rs index ad20c611..e95b60ef 100644 --- a/napi/src/transformer.rs +++ b/napi/src/transformer.rs @@ -312,6 +312,7 @@ impl<'i> Visitor<'i, AtRule<'i>> for JsVisitor { CssRule::Nesting(..) => "nesting", CssRule::Viewport(..) => "viewport", CssRule::StartingStyle(..) => "starting-style", + CssRule::ViewTransition(..) => "view-transition", CssRule::Unknown(v) => { let name = v.name.as_ref(); if let Some(visit) = rule_map.custom(stage, "unknown", name) { diff --git a/node/ast.d.ts b/node/ast.d.ts index 19ab2c1f..cf393c79 100644 --- a/node/ast.d.ts +++ b/node/ast.d.ts @@ -1,4 +1,4 @@ -/* tslint:disable */ +/* eslint-disable */ /** * This file was automatically generated by json-schema-to-typescript. * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, @@ -89,6 +89,10 @@ export type Rule = | { type: "starting-style"; value: StartingStyleRule; } +| { + type: "view-transition"; + value: ViewTransitionRule; + } | { type: "ignored"; } @@ -2342,6 +2346,12 @@ export type PropertyId = | { property: "view-transition-name"; } + | { + property: "view-transition-class"; + } + | { + property: "view-transition-group"; + } | { property: "color-scheme"; } @@ -3819,7 +3829,15 @@ export type Declaration = } | { property: "view-transition-name"; - value: String; + value: ViewTransitionName; + } + | { + property: "view-transition-class"; + value: NoneOrCustomIdentList; + } + | { + property: "view-transition-group"; + value: ViewTransitionGroup; } | { property: "color-scheme"; @@ -6406,6 +6424,21 @@ export type ContainerNameList = type: "names"; value: String[]; }; +/** + * A value for the [view-transition-name](https://drafts.csswg.org/css-view-transitions-1/#view-transition-name-prop) property. + */ +export type ViewTransitionName = + "none" | "auto" | String; +/** + * The `none` keyword, or a space-separated list of custom idents. + */ +export type NoneOrCustomIdentList = + "none" | String[]; +/** + * A value for the [view-transition-group](https://drafts.csswg.org/css-view-transitions-2/#view-transition-group-prop) property. + */ +export type ViewTransitionGroup = + "normal" | "contain" | "nearest" | String; /** * A [CSS-wide keyword](https://drafts.csswg.org/css-cascade-5/#defaulting-keywords). */ @@ -6733,6 +6766,16 @@ export type PseudoClass = kind: "autofill"; vendorPrefix: VendorPrefix; } + | { + kind: "active-view-transition"; + } + | { + kind: "active-view-transition-type"; + /** + * A view transition type. + */ + type: String[]; + } | { kind: "local"; /** @@ -6864,28 +6907,28 @@ export type PseudoElement = /** * A part name selector. */ - partName: ViewTransitionPartName; + part: ViewTransitionPartSelector; } | { kind: "view-transition-image-pair"; /** * A part name selector. */ - partName: ViewTransitionPartName; + part: ViewTransitionPartSelector; } | { kind: "view-transition-old"; /** * A part name selector. */ - partName: ViewTransitionPartName; + part: ViewTransitionPartSelector; } | { kind: "view-transition-new"; /** * A part name selector. */ - partName: ViewTransitionPartName; + part: ViewTransitionPartSelector; } | { kind: "custom"; @@ -7413,6 +7456,28 @@ export type StyleQuery = | { operator: Operator; type: "operation"; }; +/** + * A property within a `@view-transition` rule. + * + * See [ViewTransitionRule](ViewTransitionRule). + */ +export type ViewTransitionProperty = + | { + property: "navigation"; + value: Navigation; + } + | { + property: "types"; + value: NoneOrCustomIdentList; + } + | { + property: "custom"; + value: CustomProperty; + }; +/** + * A value for the [navigation](https://drafts.csswg.org/css-view-transitions-2/#view-transition-navigation-descriptor) property in a `@view-transition` rule. + */ +export type Navigation = "none" | "auto"; export type DefaultAtRule = null; /** @@ -9126,6 +9191,19 @@ export interface AttrOperation { operator: AttrSelectorOperator; value: string; } +/** + * A [view transition part selector](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#typedef-pt-name-selector). + */ +export interface ViewTransitionPartSelector { + /** + * A list of view transition classes. + */ + classes: String[]; + /** + * The view transition part name. + */ + name?: ViewTransitionPartName | null; +} /** * A [@keyframes](https://drafts.csswg.org/css-animations/#keyframes) rule. */ @@ -9545,6 +9623,19 @@ export interface StartingStyleRule { */ rules: Rule[]; } +/** + * A [@view-transition](https://drafts.csswg.org/css-view-transitions-2/#view-transition-rule) rule. + */ +export interface ViewTransitionRule { + /** + * The location of the rule in the source file. + */ + loc: Location2; + /** + * Declarations in the `@view-transition` rule. + */ + properties: ViewTransitionProperty[]; +} /** * An unknown at-rule, stored as raw tokens. */ diff --git a/scripts/build-ast.js b/scripts/build-ast.js index de0e7f20..883aa442 100644 --- a/scripts/build-ast.js +++ b/scripts/build-ast.js @@ -75,6 +75,27 @@ compileFromFile('node/ast.json', { if (types[2].type === 'TSTypeLiteral' && types[2].members[0].key.name === 'timelinerange') { path.get('typeAnnotation.types.2').replaceWith(path.node.typeAnnotation.types[2].members[0].typeAnnotation.typeAnnotation); } + } else if ( + path.node.id.name === 'NoneOrCustomIdentList' && + path.node.typeAnnotation.type === 'TSUnionType' && + path.node.typeAnnotation.types[1].type === 'TSTypeLiteral' && + path.node.typeAnnotation.types[1].members[0].key.name === 'idents' + ) { + path.get('typeAnnotation.types.1').replaceWith(path.node.typeAnnotation.types[1].members[0].typeAnnotation.typeAnnotation); + } else if ( + path.node.id.name === 'ViewTransitionGroup' && + path.node.typeAnnotation.type === 'TSUnionType' && + path.node.typeAnnotation.types[3].type === 'TSTypeLiteral' && + path.node.typeAnnotation.types[3].members[0].key.name === 'custom' + ) { + path.get('typeAnnotation.types.3').replaceWith(path.node.typeAnnotation.types[3].members[0].typeAnnotation.typeAnnotation); + } else if ( + path.node.id.name === 'ViewTransitionName' && + path.node.typeAnnotation.type === 'TSUnionType' && + path.node.typeAnnotation.types[2].type === 'TSTypeLiteral' && + path.node.typeAnnotation.types[2].members[0].key.name === 'custom' + ) { + path.get('typeAnnotation.types.2').replaceWith(path.node.typeAnnotation.types[2].members[0].typeAnnotation.typeAnnotation); } } }); diff --git a/scripts/build-prefixes.js b/scripts/build-prefixes.js index 9ffa67c2..47a42692 100644 --- a/scripts/build-prefixes.js +++ b/scripts/build-prefixes.js @@ -331,6 +331,7 @@ let mdnFeatures = { lightDark: mdn.css.types.color['light-dark'].__compat.support, accentSystemColor: mdn.css.types.color['system-color'].accentcolor_accentcolortext.__compat.support, animationTimelineShorthand: mdn.css.properties.animation['animation-timeline_included'].__compat.support, + viewTransition: mdn.css.selectors['view-transition'].__compat.support, }; for (let key in mdn.css.types.length) { diff --git a/src/compat.rs b/src/compat.rs index 911b2c86..6f360bb1 100644 --- a/src/compat.rs +++ b/src/compat.rs @@ -212,6 +212,7 @@ pub enum Feature { VbUnit, VhUnit, ViUnit, + ViewTransition, ViewportPercentageUnitsDynamic, ViewportPercentageUnitsLarge, ViewportPercentageUnitsSmall, @@ -3464,6 +3465,46 @@ impl Feature { return false; } } + Feature::ViewTransition => { + if let Some(version) = browsers.chrome { + if version < 7143424 { + return false; + } + } + if let Some(version) = browsers.edge { + if version < 7143424 { + return false; + } + } + if let Some(version) = browsers.opera { + if version < 4849664 { + return false; + } + } + if let Some(version) = browsers.safari { + if version < 1179648 { + return false; + } + } + if let Some(version) = browsers.ios_saf { + if version < 1179648 { + return false; + } + } + if let Some(version) = browsers.samsung { + if version < 1376256 { + return false; + } + } + if let Some(version) = browsers.android { + if version < 7143424 { + return false; + } + } + if browsers.firefox.is_some() || browsers.ie.is_some() { + return false; + } + } Feature::QUnit => { if let Some(version) = browsers.chrome { if version < 4128768 { diff --git a/src/lib.rs b/src/lib.rs index fbba61af..54c2587d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -182,6 +182,7 @@ mod tests { expected_exports: CssModuleExports, expected_references: CssModuleReferences, config: crate::css_modules::Config<'i>, + minify: bool, ) { let mut stylesheet = StyleSheet::parse( &source, @@ -193,7 +194,12 @@ mod tests { ) .unwrap(); stylesheet.minify(MinifyOptions::default()).unwrap(); - let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); + let res = stylesheet + .to_css(PrinterOptions { + minify, + ..Default::default() + }) + .unwrap(); assert_eq!(res.code, expected); assert_eq!(res.exports.unwrap(), expected_exports); assert_eq!(res.references.unwrap(), expected_references); @@ -6856,6 +6862,19 @@ mod tests { ":root::view-transition {position: fixed}", ":root::view-transition{position:fixed}", ); + minify_test( + ":root:active-view-transition {position: fixed}", + ":root:active-view-transition{position:fixed}", + ); + minify_test( + ":root:active-view-transition-type(slide-in) {position: fixed}", + ":root:active-view-transition-type(slide-in){position:fixed}", + ); + minify_test( + ":root:active-view-transition-type(slide-in, reverse) {position: fixed}", + ":root:active-view-transition-type(slide-in,reverse){position:fixed}", + ); + for name in &[ "view-transition-group", "view-transition-image-pair", @@ -6866,14 +6885,42 @@ mod tests { &format!(":root::{}(*) {{position: fixed}}", name), &format!(":root::{}(*){{position:fixed}}", name), ); + minify_test( + &format!(":root::{}(*.class) {{position: fixed}}", name), + &format!(":root::{}(*.class){{position:fixed}}", name), + ); + minify_test( + &format!(":root::{}(*.class.class) {{position: fixed}}", name), + &format!(":root::{}(*.class.class){{position:fixed}}", name), + ); minify_test( &format!(":root::{}(foo) {{position: fixed}}", name), &format!(":root::{}(foo){{position:fixed}}", name), ); + minify_test( + &format!(":root::{}(foo.class) {{position: fixed}}", name), + &format!(":root::{}(foo.class){{position:fixed}}", name), + ); + minify_test( + &format!(":root::{}(foo.bar.baz) {{position: fixed}}", name), + &format!(":root::{}(foo.bar.baz){{position:fixed}}", name), + ); minify_test( &format!(":root::{}(foo):only-child {{position: fixed}}", name), &format!(":root::{}(foo):only-child{{position:fixed}}", name), ); + minify_test( + &format!(":root::{}(foo.bar.baz):only-child {{position: fixed}}", name), + &format!(":root::{}(foo.bar.baz):only-child{{position:fixed}}", name), + ); + minify_test( + &format!(":root::{}(.foo) {{position: fixed}}", name), + &format!(":root::{}(.foo){{position:fixed}}", name), + ); + minify_test( + &format!(":root::{}(.foo.bar) {{position: fixed}}", name), + &format!(":root::{}(.foo.bar){{position:fixed}}", name), + ); error_test( &format!(":root::{}(foo):first-child {{position: fixed}}", name), ParserError::SelectorError(SelectorError::InvalidPseudoClassAfterPseudoElement), @@ -6882,6 +6929,30 @@ mod tests { &format!(":root::{}(foo)::before {{position: fixed}}", name), ParserError::SelectorError(SelectorError::InvalidState), ); + error_test( + &format!(":root::{}(*.*) {{position: fixed}}", name), + ParserError::SelectorError(SelectorError::InvalidState), + ); + error_test( + &format!(":root::{}(*. cls) {{position: fixed}}", name), + ParserError::SelectorError(SelectorError::InvalidState), + ); + error_test( + &format!(":root::{}(foo .bar) {{position: fixed}}", name), + ParserError::SelectorError(SelectorError::InvalidState), + ); + error_test( + &format!(":root::{}(*.cls. c) {{position: fixed}}", name), + ParserError::SelectorError(SelectorError::InvalidState), + ); + error_test( + &format!(":root::{}(*.cls>cls) {{position: fixed}}", name), + ParserError::SelectorError(SelectorError::InvalidState), + ); + error_test( + &format!(":root::{}(*.cls.foo.*) {{position: fixed}}", name), + ParserError::SelectorError(SelectorError::InvalidState), + ); } minify_test(".foo ::deep .bar {width: 20px}", ".foo ::deep .bar{width:20px}"); @@ -23860,6 +23931,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -23906,6 +23978,7 @@ mod tests { // custom_idents: false, ..Default::default() }, + false, ); css_modules_test( @@ -23951,6 +24024,7 @@ mod tests { custom_idents: false, ..Default::default() }, + false, ); #[cfg(feature = "grid")] @@ -23995,6 +24069,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); #[cfg(feature = "grid")] @@ -24033,6 +24108,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); #[cfg(feature = "grid")] @@ -24073,6 +24149,7 @@ mod tests { grid: false, ..Default::default() }, + false, ); css_modules_test( @@ -24089,6 +24166,7 @@ mod tests { map! {}, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -24123,6 +24201,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); // :global(:local(.hi)) { @@ -24155,6 +24234,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -24184,6 +24264,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -24221,6 +24302,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -24240,6 +24322,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -24259,6 +24342,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -24278,6 +24362,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -24297,6 +24382,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -24327,6 +24413,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -24348,6 +24435,7 @@ mod tests { pattern: crate::css_modules::Pattern::parse("test-[hash]-[local]").unwrap(), ..Default::default() }, + false, ); let stylesheet = StyleSheet::parse( @@ -24409,6 +24497,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( @@ -24478,6 +24567,7 @@ mod tests { dashed_idents: true, ..Default::default() }, + false, ); css_modules_test( @@ -24497,6 +24587,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( r#" @@ -24514,6 +24605,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( r#" @@ -24531,6 +24623,7 @@ mod tests { }, HashMap::new(), Default::default(), + false, ); css_modules_test( r#" @@ -24551,6 +24644,7 @@ mod tests { animation: false, ..Default::default() }, + false, ); css_modules_test( r#" @@ -24569,6 +24663,7 @@ mod tests { }, HashMap::new(), crate::css_modules::Config { ..Default::default() }, + false, ); css_modules_test( @@ -24591,6 +24686,7 @@ mod tests { pattern: crate::css_modules::Pattern::parse("[content-hash]-[local]").unwrap(), ..Default::default() }, + false, ); css_modules_test( @@ -24616,6 +24712,7 @@ mod tests { }, HashMap::new(), crate::css_modules::Config { ..Default::default() }, + false, ); css_modules_test( @@ -24643,8 +24740,142 @@ mod tests { container: false, ..Default::default() }, + false, + ); + + css_modules_test( + ".foo { view-transition-name: bar }", + ".EgL3uq_foo{view-transition-name:EgL3uq_bar}", + map! { + "foo" => "EgL3uq_foo", + "bar" => "EgL3uq_bar" + }, + HashMap::new(), + Default::default(), + true, + ); + css_modules_test( + ".foo { view-transition-name: none }", + ".EgL3uq_foo{view-transition-name:none}", + map! { + "foo" => "EgL3uq_foo" + }, + HashMap::new(), + Default::default(), + true, + ); + css_modules_test( + ".foo { view-transition-name: auto }", + ".EgL3uq_foo{view-transition-name:auto}", + map! { + "foo" => "EgL3uq_foo" + }, + HashMap::new(), + Default::default(), + true, + ); + + css_modules_test( + ".foo { view-transition-class: bar baz qux }", + ".EgL3uq_foo{view-transition-class:EgL3uq_bar EgL3uq_baz EgL3uq_qux}", + map! { + "foo" => "EgL3uq_foo", + "bar" => "EgL3uq_bar", + "baz" => "EgL3uq_baz", + "qux" => "EgL3uq_qux" + }, + HashMap::new(), + Default::default(), + true, + ); + + css_modules_test( + ".foo { view-transition-group: contain }", + ".EgL3uq_foo{view-transition-group:contain}", + map! { + "foo" => "EgL3uq_foo" + }, + HashMap::new(), + Default::default(), + true, + ); + css_modules_test( + ".foo { view-transition-group: bar }", + ".EgL3uq_foo{view-transition-group:EgL3uq_bar}", + map! { + "foo" => "EgL3uq_foo", + "bar" => "EgL3uq_bar" + }, + HashMap::new(), + Default::default(), + true, + ); + + css_modules_test( + "@view-transition { types: foo bar baz }", + "@view-transition{types:EgL3uq_foo EgL3uq_bar EgL3uq_baz}", + map! { + "foo" => "EgL3uq_foo", + "bar" => "EgL3uq_bar", + "baz" => "EgL3uq_baz" + }, + HashMap::new(), + Default::default(), + true, + ); + + css_modules_test( + ":root:active-view-transition-type(foo, bar) { color: red }", + ":root:active-view-transition-type(EgL3uq_foo,EgL3uq_bar){color:red}", + map! { + "foo" => "EgL3uq_foo", + "bar" => "EgL3uq_bar" + }, + HashMap::new(), + Default::default(), + true, ); + for name in &[ + "view-transition-group", + "view-transition-image-pair", + "view-transition-new", + "view-transition-old", + ] { + css_modules_test( + &format!(":root::{}(foo) {{position: fixed}}", name), + &format!(":root::{}(EgL3uq_foo){{position:fixed}}", name), + map! { + "foo" => "EgL3uq_foo" + }, + HashMap::new(), + Default::default(), + true, + ); + css_modules_test( + &format!(":root::{}(.bar) {{position: fixed}}", name), + &format!(":root::{}(.EgL3uq_bar){{position:fixed}}", name), + map! { + "bar" => "EgL3uq_bar" + }, + HashMap::new(), + Default::default(), + true, + ); + css_modules_test( + &format!(":root::{}(foo.bar.baz) {{position: fixed}}", name), + &format!(":root::{}(EgL3uq_foo.EgL3uq_bar.EgL3uq_baz){{position:fixed}}", name), + map! { + "foo" => "EgL3uq_foo", + "bar" => "EgL3uq_bar", + "baz" => "EgL3uq_baz" + }, + HashMap::new(), + Default::default(), + true, + ); + } + // Stable hashes between project roots. fn test_project_root(project_root: &str, filename: &str, hash: &str) { let stylesheet = StyleSheet::parse( @@ -28143,6 +28374,7 @@ mod tests { dashed_idents: true, ..Default::default() }, + false, ); } @@ -28490,4 +28722,24 @@ mod tests { ".foo{--bar:currentcolor;--foo:1.1em;all:unset}", ); } + + #[test] + fn test_view_transition() { + minify_test( + "@view-transition { navigation: auto }", + "@view-transition{navigation:auto}", + ); + minify_test( + "@view-transition { navigation: auto; types: none; }", + "@view-transition{navigation:auto;types:none}", + ); + minify_test( + "@view-transition { navigation: auto; types: foo bar; }", + "@view-transition{navigation:auto;types:foo bar}", + ); + minify_test( + "@layer { @view-transition { navigation: auto; types: foo bar; } }", + "@layer{@view-transition{navigation:auto;types:foo bar}}", + ); + } } diff --git a/src/parser.rs b/src/parser.rs index 080753be..a40ee5b1 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -9,6 +9,7 @@ use crate::rules::layer::{LayerBlockRule, LayerStatementRule}; use crate::rules::property::PropertyRule; use crate::rules::scope::ScopeRule; use crate::rules::starting_style::StartingStyleRule; +use crate::rules::view_transition::ViewTransitionRule; use crate::rules::viewport::ViewportRule; use crate::rules::{ @@ -214,6 +215,8 @@ pub enum AtRulePrelude<'i, T> { StartingStyle, /// A @scope rule prelude. Scope(Option>, Option>), + /// A @view-transition rule prelude. + ViewTransition, /// An unknown prelude. Unknown(CowArcStr<'i>, TokenList<'i>), /// A custom prelude. @@ -249,7 +252,8 @@ impl<'i, T> AtRulePrelude<'i, T> { | Self::Import(..) | Self::CustomMedia(..) | Self::Viewport(..) - | Self::Charset => false, + | Self::Charset + | Self::ViewTransition => false, } } } @@ -669,6 +673,9 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne AtRulePrelude::Scope(scope_start, scope_end) }, + "view-transition" => { + AtRulePrelude::ViewTransition + }, "nest" if self.is_in_style_rule => { self.options.warn(input.new_custom_error(ParserError::DeprecatedNestRule)); let selector_parser = SelectorParser { @@ -833,6 +840,13 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for Ne self.rules.0.push(CssRule::StartingStyle(StartingStyleRule { rules, loc })); Ok(()) } + AtRulePrelude::ViewTransition => { + self + .rules + .0 + .push(CssRule::ViewTransition(ViewTransitionRule::parse(input, loc)?)); + Ok(()) + } AtRulePrelude::Nest(selectors) => { let (declarations, rules) = self.parse_nested(input, true)?; self.rules.0.push(CssRule::Nesting(NestingRule { diff --git a/src/properties/mod.rs b/src/properties/mod.rs index 7c5f4071..00667a30 100644 --- a/src/properties/mod.rs +++ b/src/properties/mod.rs @@ -133,7 +133,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::CustomIdent, ident::DashedIdentReference, image::*, + alpha::*, color::*, easing::EasingFunction, ident::DashedIdentReference, ident::NoneOrCustomIdentList, image::*, length::*, position::*, rect::*, shape::FillRule, size::Size2D, time::Time, }; use crate::vendor_prefix::VendorPrefix; @@ -1638,7 +1638,10 @@ define_properties! { "container": Container(Container<'i>) shorthand: true, // https://w3c.github.io/csswg-drafts/css-view-transitions-1/ - "view-transition-name": ViewTransitionName(CustomIdent<'i>), + "view-transition-name": ViewTransitionName(ViewTransitionName<'i>), + // https://drafts.csswg.org/css-view-transitions-2/ + "view-transition-class": ViewTransitionClass(NoneOrCustomIdentList<'i>), + "view-transition-group": ViewTransitionGroup(ViewTransitionGroup<'i>), // https://drafts.csswg.org/css-color-adjust/ "color-scheme": ColorScheme(ColorScheme), diff --git a/src/properties/transition.rs b/src/properties/transition.rs index be95d5ab..8d6a6629 100644 --- a/src/properties/transition.rs +++ b/src/properties/transition.rs @@ -10,6 +10,7 @@ use crate::prefixes::Feature; use crate::printer::Printer; use crate::properties::masking::get_webkit_mask_property; use crate::traits::{Parse, PropertyHandler, Shorthand, ToCss, Zero}; +use crate::values::ident::CustomIdent; use crate::values::{easing::EasingFunction, time::Time}; use crate::vendor_prefix::VendorPrefix; #[cfg(feature = "visitor")] @@ -106,6 +107,50 @@ impl<'i> ToCss for Transition<'i> { } } +/// A value for the [view-transition-name](https://drafts.csswg.org/css-view-transitions-1/#view-transition-name-prop) property. +#[derive(Debug, Clone, PartialEq, Default, Parse, ToCss)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[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 ViewTransitionName<'i> { + /// The element will not participate independently in a view transition. + #[default] + None, + /// The `auto` keyword. + Auto, + /// A custom name. + #[cfg_attr(feature = "serde", serde(borrow, untagged))] + Custom(CustomIdent<'i>), +} + +/// A value for the [view-transition-group](https://drafts.csswg.org/css-view-transitions-2/#view-transition-group-prop) property. +#[derive(Debug, Clone, PartialEq, Default, Parse, ToCss)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[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 ViewTransitionGroup<'i> { + /// The `normal` keyword. + #[default] + Normal, + /// The `contain` keyword. + Contain, + /// The `nearest` keyword. + Nearest, + /// A custom group. + #[cfg_attr(feature = "serde", serde(borrow, untagged))] + Custom(CustomIdent<'i>), +} + #[derive(Default)] pub(crate) struct TransitionHandler<'i> { properties: Option<(SmallVec<[PropertyId<'i>; 1]>, VendorPrefix)>, diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 4655a6e6..8d986f4e 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -55,6 +55,7 @@ pub mod starting_style; pub mod style; pub mod supports; pub mod unknown; +pub mod view_transition; pub mod viewport; use self::font_palette_values::FontPaletteValuesRule; @@ -97,6 +98,7 @@ use std::hash::{BuildHasherDefault, Hasher}; use style::StyleRule; use supports::SupportsRule; use unknown::UnknownAtRule; +use view_transition::ViewTransitionRule; use viewport::ViewportRule; #[derive(Clone)] @@ -174,6 +176,8 @@ pub enum CssRule<'i, R = DefaultAtRule> { Scope(ScopeRule<'i, R>), /// A `@starting-style` rule. StartingStyle(StartingStyleRule<'i, R>), + /// A `@view-transition` rule. + ViewTransition(ViewTransitionRule<'i>), /// A placeholder for a rule that was removed. Ignored, /// An unknown at-rule. @@ -318,6 +322,10 @@ impl<'i, 'de: 'i, R: serde::Deserialize<'de>> serde::Deserialize<'de> for CssRul let rule = StartingStyleRule::deserialize(deserializer)?; Ok(CssRule::StartingStyle(rule)) } + "view-transition" => { + let rule = ViewTransitionRule::deserialize(deserializer)?; + Ok(CssRule::ViewTransition(rule)) + } "ignored" => Ok(CssRule::Ignored), "unknown" => { let rule = UnknownAtRule::deserialize(deserializer)?; @@ -358,6 +366,7 @@ impl<'a, 'i, T: ToCss> ToCss for CssRule<'i, T> { CssRule::StartingStyle(rule) => rule.to_css(dest), CssRule::Container(container) => container.to_css(dest), CssRule::Scope(scope) => scope.to_css(dest), + CssRule::ViewTransition(rule) => rule.to_css(dest), CssRule::Unknown(unknown) => unknown.to_css(dest), CssRule::Custom(rule) => rule.to_css(dest).map_err(|_| PrinterError { kind: PrinterErrorKind::FmtError, diff --git a/src/rules/view_transition.rs b/src/rules/view_transition.rs new file mode 100644 index 00000000..fac6ec65 --- /dev/null +++ b/src/rules/view_transition.rs @@ -0,0 +1,196 @@ +//! The `@view-transition` rule. + +use super::Location; +use crate::error::{ParserError, PrinterError}; +use crate::printer::Printer; +use crate::properties::custom::CustomProperty; +use crate::stylesheet::ParserOptions; +use crate::traits::{Parse, ToCss}; +use crate::values::ident::NoneOrCustomIdentList; +#[cfg(feature = "visitor")] +use crate::visitor::Visit; +use cssparser::*; + +/// A [@view-transition](https://drafts.csswg.org/css-view-transitions-2/#view-transition-rule) rule. +#[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +pub struct ViewTransitionRule<'i> { + /// Declarations in the `@view-transition` rule. + #[cfg_attr(feature = "serde", serde(borrow))] + pub properties: Vec>, + /// The location of the rule in the source file. + #[cfg_attr(feature = "visitor", skip_visit)] + pub loc: Location, +} + +/// A property within a `@view-transition` rule. +/// +/// See [ViewTransitionRule](ViewTransitionRule). +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "property", content = "value", rename_all = "kebab-case") +)] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +pub enum ViewTransitionProperty<'i> { + /// The `navigation` property. + Navigation(Navigation), + /// The `types` property. + #[cfg_attr(feature = "serde", serde(borrow))] + Types(NoneOrCustomIdentList<'i>), + /// An unknown or unsupported property. + Custom(CustomProperty<'i>), +} + +/// A value for the [navigation](https://drafts.csswg.org/css-view-transitions-2/#view-transition-navigation-descriptor) +/// property in a `@view-transition` rule. +#[derive(Debug, Clone, PartialEq, Default, Parse, ToCss)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "kebab-case") +)] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +pub enum Navigation { + /// There will be no transition. + #[default] + None, + /// The transition will be enabled if the navigation is same-origin. + Auto, +} + +pub(crate) struct ViewTransitionDeclarationParser; + +impl<'i> cssparser::DeclarationParser<'i> for ViewTransitionDeclarationParser { + type Declaration = ViewTransitionProperty<'i>; + type Error = ParserError<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut cssparser::Parser<'i, 't>, + ) -> Result> { + let state = input.state(); + match_ignore_ascii_case! { &name, + "navigation" => { + // https://drafts.csswg.org/css-view-transitions-2/#view-transition-navigation-descriptor + if let Ok(navigation) = Navigation::parse(input) { + return Ok(ViewTransitionProperty::Navigation(navigation)); + } + }, + "types" => { + // https://drafts.csswg.org/css-view-transitions-2/#types-cross-doc + if let Ok(types) = NoneOrCustomIdentList::parse(input) { + return Ok(ViewTransitionProperty::Types(types)); + } + }, + _ => return Err(input.new_custom_error(ParserError::InvalidDeclaration)) + } + + input.reset(&state); + return Ok(ViewTransitionProperty::Custom(CustomProperty::parse( + name.into(), + input, + &ParserOptions::default(), + )?)); + } +} + +/// Default methods reject all at rules. +impl<'i> AtRuleParser<'i> for ViewTransitionDeclarationParser { + type Prelude = (); + type AtRule = ViewTransitionProperty<'i>; + type Error = ParserError<'i>; +} + +impl<'i> QualifiedRuleParser<'i> for ViewTransitionDeclarationParser { + type Prelude = (); + type QualifiedRule = ViewTransitionProperty<'i>; + type Error = ParserError<'i>; +} + +impl<'i> RuleBodyItemParser<'i, ViewTransitionProperty<'i>, ParserError<'i>> for ViewTransitionDeclarationParser { + fn parse_qualified(&self) -> bool { + false + } + + fn parse_declarations(&self) -> bool { + true + } +} + +impl<'i> ViewTransitionRule<'i> { + pub(crate) fn parse<'t>( + input: &mut Parser<'i, 't>, + loc: Location, + ) -> Result>> { + let mut decl_parser = ViewTransitionDeclarationParser; + let mut parser = RuleBodyParser::new(input, &mut decl_parser); + let mut properties = vec![]; + while let Some(decl) = parser.next() { + if let Ok(decl) = decl { + properties.push(decl); + } + } + + Ok(ViewTransitionRule { properties, loc }) + } +} + +impl<'i> ToCss for ViewTransitionRule<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + #[cfg(feature = "sourcemap")] + dest.add_mapping(self.loc); + dest.write_str("@view-transition")?; + dest.whitespace()?; + dest.write_char('{')?; + dest.indent(); + let len = self.properties.len(); + for (i, prop) in self.properties.iter().enumerate() { + dest.newline()?; + prop.to_css(dest)?; + if i != len - 1 || !dest.minify { + dest.write_char(';')?; + } + } + dest.dedent(); + dest.newline()?; + dest.write_char('}') + } +} + +impl<'i> ToCss for ViewTransitionProperty<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + macro_rules! property { + ($prop: literal, $value: expr) => {{ + dest.write_str($prop)?; + dest.delim(':', false)?; + $value.to_css(dest) + }}; + } + + match self { + ViewTransitionProperty::Navigation(f) => property!("navigation", f), + ViewTransitionProperty::Types(t) => property!("types", t), + ViewTransitionProperty::Custom(custom) => { + dest.write_str(custom.name.as_ref())?; + dest.delim(':', false)?; + custom.value.to_css(dest, true) + } + } + } +} diff --git a/src/selector.rs b/src/selector.rs index 73311c8a..86507b58 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -1,7 +1,7 @@ //! CSS selectors. use crate::compat::Feature; -use crate::error::{ParserError, PrinterError}; +use crate::error::{ParserError, PrinterError, SelectorError}; use crate::parser::ParserFlags; use crate::printer::Printer; use crate::properties::custom::TokenList; @@ -21,6 +21,7 @@ use parcel_selectors::{ attr::{AttrSelectorOperator, ParsedAttrSelectorOperation, ParsedCaseSensitivity}, parser::SelectorImpl, }; +use smallvec::SmallVec; use std::collections::HashSet; use std::fmt; @@ -177,6 +178,9 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, "-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), @@ -221,6 +225,11 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, 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)?) }, _ => { @@ -303,10 +312,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)? }, + "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()))); @@ -507,6 +516,15 @@ pub enum PseudoClass<'i> { #[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 { @@ -763,6 +781,13 @@ where // 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); @@ -902,25 +927,25 @@ pub enum PseudoElement<'i> { #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] ViewTransitionGroup { /// A part name selector. - part_name: ViewTransitionPartName<'i>, + 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_name: ViewTransitionPartName<'i>, + 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_name: ViewTransitionPartName<'i>, + 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_name: ViewTransitionPartName<'i>, + part: ViewTransitionPartSelector<'i>, }, /// An unknown pseudo element. Custom { @@ -965,44 +990,17 @@ pub enum WebKitScrollbarPseudoElement { /// 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, /// + #[cfg_attr(feature = "serde", serde(borrow, untagged))] 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> { @@ -1041,6 +1039,55 @@ impl<'i> ToCss for ViewTransitionPartName<'i> { } } +/// 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>, + /// A list of view transition classes. + classes: Vec>, +} + +impl<'i> Parse<'i> for ViewTransitionPartSelector<'i> { + fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { + 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(&self, dest: &mut Printer) -> 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(&self, dest: &mut W) -> std::fmt::Result where @@ -1138,24 +1185,24 @@ where }) } ViewTransition => dest.write_str("::view-transition"), - ViewTransitionGroup { part_name } => { + ViewTransitionGroup { part } => { dest.write_str("::view-transition-group(")?; - part_name.to_css(dest)?; + part.to_css(dest)?; dest.write_char(')') } - ViewTransitionImagePair { part_name } => { + ViewTransitionImagePair { part } => { dest.write_str("::view-transition-image-pair(")?; - part_name.to_css(dest)?; + part.to_css(dest)?; dest.write_char(')') } - ViewTransitionOld { part_name } => { + ViewTransitionOld { part } => { dest.write_str("::view-transition-old(")?; - part_name.to_css(dest)?; + part.to_css(dest)?; dest.write_char(')') } - ViewTransitionNew { part_name } => { + ViewTransitionNew { part } => { dest.write_str("::view-transition-new(")?; - part_name.to_css(dest)?; + part.to_css(dest)?; dest.write_char(')') } Custom { name: val } => { @@ -1836,7 +1883,9 @@ pub(crate) fn is_compatible(selectors: &[Selector], targets: Targets) -> bool { | PseudoClass::Blank | PseudoClass::UserInvalid | PseudoClass::UserValid - | PseudoClass::Defined => return false, + | PseudoClass::Defined + | PseudoClass::ActiveViewTransition + | PseudoClass::ActiveViewTransitionType { .. } => return false, PseudoClass::Custom { .. } | _ => return false, } @@ -1852,6 +1901,11 @@ pub(crate) fn is_compatible(selectors: &[Selector], targets: Targets) -> bool { 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::Custom { name: _ } | _ => return false, }, diff --git a/src/values/ident.rs b/src/values/ident.rs index 173d3a08..be850f8b 100644 --- a/src/values/ident.rs +++ b/src/values/ident.rs @@ -75,6 +75,69 @@ impl<'i> CustomIdent<'i> { /// A list of CSS [``](https://www.w3.org/TR/css-values-4/#custom-idents) values. pub type CustomIdentList<'i> = SmallVec<[CustomIdent<'i>; 1]>; +/// The `none` keyword, or a space-separated list of custom idents. +#[derive(Debug, Clone, PartialEq, Default)] +#[cfg_attr(feature = "visitor", derive(Visit))] +#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "kebab-case") +)] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +pub enum NoneOrCustomIdentList<'i> { + /// None. + #[default] + None, + /// A list of idents. + #[cfg_attr(feature = "serde", serde(borrow, untagged))] + Idents(SmallVec<[CustomIdent<'i>; 1]>), +} + +impl<'i> Parse<'i> for NoneOrCustomIdentList<'i> { + fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { + let mut types = SmallVec::new(); + loop { + if let Ok(ident) = input.try_parse(CustomIdent::parse) { + if ident == "none" { + if types.is_empty() { + return Ok(NoneOrCustomIdentList::None); + } else { + return Err(input.new_custom_error(ParserError::InvalidValue)); + } + } + + types.push(ident); + } else { + return Ok(NoneOrCustomIdentList::Idents(types)); + } + } + } +} + +impl<'i> ToCss for NoneOrCustomIdentList<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + match self { + NoneOrCustomIdentList::None => dest.write_str("none"), + NoneOrCustomIdentList::Idents(types) => { + let mut first = true; + for ident in types { + if !first { + dest.write_char(' ')?; + } else { + first = false; + } + ident.to_css(dest)?; + } + Ok(()) + } + } + } +} + /// A CSS [``](https://www.w3.org/TR/css-values-4/#dashed-idents) declaration. /// /// Dashed idents are used in cases where an identifier can be either author defined _or_ CSS-defined.