Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/css/css_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,7 @@ pub enum AtRulePrelude<T> {
condition: ContainerCondition,
},
StartingStyle,
ViewTransition,
Nest(SelectorList),
Scope {
scope_start: Option<SelectorList>,
Expand Down Expand Up @@ -1632,6 +1633,7 @@ mod rule_parsers {
};
break 'brk AtRulePrelude::Scope { scope_start, scope_end };
},
b"view-transition" => break 'brk AtRulePrelude::ViewTransition,
b"nest" => if this.is_in_style_rule {
this.options.warn(&input.new_custom_error(ParserError::deprecated_nest_rule));
let mut selector_parser = selector_parser::SelectorParser {
Expand Down Expand Up @@ -1857,6 +1859,11 @@ mod rule_parsers {
));
Ok(())
}
AtRulePrelude::ViewTransition => {
let rule = css_rules::view_transition::ViewTransitionRule::parse(input, loc)?;
this.rules.v.push(CssRule::ViewTransition(rule));
Ok(())
}
AtRulePrelude::Nest(selectors) => {
let (declarations, rules) = this.parse_nested(input, true)?;
this.rules
Expand Down
5 changes: 4 additions & 1 deletion src/css/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub mod starting_style;
pub mod style;
pub mod supports;
pub mod unknown;
pub mod view_transition;
pub mod viewport;

// ─── CssRule / CssRuleList ─────────────────────────────────────────────────
Expand All @@ -36,7 +37,7 @@ pub mod viewport;
// lifetime-free here.

// ─── CssRule variant table ────────────────────────────────────────────────
// Single source of truth for the 20 typed at-rule payloads. Adding a new
// Single source of truth for the 21 typed at-rule payloads. Adding a new
// at-rule = one line here; the enum variant + `to_css` arm + `deep_clone`
// arm are generated. `Unknown`/`Custom`/`Ignored` stay a fixed tail because
// their `to_css` arms are special-cased (see the note on `Custom`).
Expand Down Expand Up @@ -133,6 +134,8 @@ css_rule_variants! {
Scope(scope::ScopeRule<R>),
/// A `@starting-style` rule.
StartingStyle(starting_style::StartingStyleRule<R>),
/// A `@view-transition` rule.
ViewTransition(view_transition::ViewTransitionRule),
}

// SAFETY: the CSS AST contains `SmallList<T, N>` (raw `*mut T`) and
Expand Down
210 changes: 210 additions & 0 deletions src/css/rules/view_transition.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
use crate as css;
use crate::css_rules::Location;
use crate::css_values::ident::NoneOrCustomIdentList;
use crate::generics::DeepClone as _;
use crate::{PrintErr, Printer};

use super::ArrayList;

/// A [@view-transition](https://drafts.csswg.org/css-view-transitions-2/#view-transition-rule) rule.
pub struct ViewTransitionRule {
/// Declarations in the `@view-transition` rule.
pub(crate) properties: ArrayList<ViewTransitionProperty>,
/// The location of the rule in the source file.
pub(crate) loc: Location,
}

impl ViewTransitionRule {
pub(crate) fn parse(input: &mut css::Parser, loc: Location) -> css::Result<ViewTransitionRule> {
let mut decl_parser = ViewTransitionDeclarationParser {};
let mut parser = css::css_parser::RuleBodyParser::new(input, &mut decl_parser);
let mut properties: ArrayList<ViewTransitionProperty> = ArrayList::new();
while let Some(result) = parser.next() {
if let Ok(decl) = result {
properties.push(decl);
}
}

Ok(ViewTransitionRule { properties, loc })
}

pub(crate) fn to_css(&self, dest: &mut Printer) -> Result<(), PrintErr> {
dest.write_str("@view-transition")?;
dest.whitespace()?;
dest.write_char(b'{')?;
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(b';')?;
}
}
dest.dedent();
dest.newline()?;
dest.write_char(b'}')
}

pub(crate) fn deep_clone(&self, bump: &bun_alloc::Arena) -> Self {
Self {
properties: self.properties.iter().map(|p| p.deep_clone(bump)).collect(),
loc: self.loc,
}
}
}

/// A property within a `@view-transition` rule.
pub enum ViewTransitionProperty {
/// The `navigation` property.
Navigation(Navigation),
/// The `types` property. Script sets the same names, so a CSS module keeps them as written.
Types(NoneOrCustomIdentList),
/// An unknown or unsupported property.
Custom(crate::css_properties::custom::CustomProperty),
}

impl ViewTransitionProperty {
fn to_css(&self, dest: &mut Printer) -> Result<(), PrintErr> {
match self {
ViewTransitionProperty::Navigation(navigation) => {
dest.write_str("navigation")?;
dest.delim(b':', false)?;
navigation.to_css(dest)
}
ViewTransitionProperty::Types(types) => {
dest.write_str("types")?;
dest.delim(b':', false)?;
types.to_css_with_options(dest, false)
}
ViewTransitionProperty::Custom(custom) => {
custom.name.to_css(dest)?;
dest.delim(b':', false)?;
custom.value.to_css(dest, true)
}
}
}

fn deep_clone(&self, bump: &bun_alloc::Arena) -> Self {
match self {
Self::Navigation(navigation) => Self::Navigation(*navigation),
Self::Types(types) => Self::Types(types.deep_clone(bump)),
Self::Custom(custom) => Self::Custom(custom.deep_clone(bump)),
}
}
}

/// A value for the [navigation](https://drafts.csswg.org/css-view-transitions-2/#view-transition-navigation-descriptor) descriptor.
#[derive(Clone, Copy, PartialEq, Eq, css::DefineEnumProperty)]
pub enum Navigation {
/// There will be no transition.
None,
/// The transition will be enabled if the navigation is same-origin.
Auto,
}

pub(crate) struct ViewTransitionDeclarationParser {}

const _: () = {
use crate::css_properties::custom::{CustomProperty, CustomPropertyName};
use css::css_parser::{
AtRuleParser, DeclarationParser, QualifiedRuleParser, RuleBodyItemParser,
};
use css::{BasicParseErrorKind, Maybe, Parser, ParserOptions, ParserState, Result};

impl DeclarationParser for ViewTransitionDeclarationParser {
type Declaration = ViewTransitionProperty;

fn parse_value(
_this: &mut Self,
name: &[u8],
input: &mut Parser,
) -> Result<Self::Declaration> {
// As in `@font-face`, what does not parse is kept as `Custom`. lightningcss drops it.
let state = input.state();
crate::match_ignore_ascii_case! { name, {
b"navigation" => if let Ok(navigation) = Navigation::parse(input) {
if input.expect_exhausted().is_ok() {
return Ok(ViewTransitionProperty::Navigation(navigation));
}
},
b"types" => if let Ok(types) = NoneOrCustomIdentList::parse(input) {
if input.expect_exhausted().is_ok() {
return Ok(ViewTransitionProperty::Types(types));
}
},
_ => {},
}}

input.reset(&state);
let opts = ParserOptions::default(None);
Ok(ViewTransitionProperty::Custom(CustomProperty::parse(
CustomPropertyName::from_str(name),
input,
&opts,
)?))
}
}

impl RuleBodyItemParser for ViewTransitionDeclarationParser {
fn parse_qualified(_this: &Self) -> bool {
false
}

fn parse_declarations(_this: &Self) -> bool {
true
}
}

impl AtRuleParser for ViewTransitionDeclarationParser {
type Prelude = ();
type AtRule = ViewTransitionProperty;

fn parse_prelude(
_this: &mut Self,
name: &[u8],
input: &mut Parser,
) -> Result<Self::Prelude> {
Err(
input.new_error(BasicParseErrorKind::at_rule_invalid(std::ptr::from_ref::<
[u8],
>(name))),
)
}

fn parse_block(
_this: &mut Self,
_prelude: Self::Prelude,
_start: &ParserState,
input: &mut Parser,
) -> Result<Self::AtRule> {
Err(input.new_error(BasicParseErrorKind::at_rule_body_invalid))
}

fn rule_without_block(
_this: &mut Self,
_prelude: Self::Prelude,
_start: &ParserState,
) -> Maybe<Self::AtRule, ()> {
Err(())
}
}

impl QualifiedRuleParser for ViewTransitionDeclarationParser {
type Prelude = ();
type QualifiedRule = ViewTransitionProperty;

fn parse_prelude(_this: &mut Self, input: &mut Parser) -> Result<Self::Prelude> {
Err(input.new_error(BasicParseErrorKind::qualified_rule_invalid))
}

fn parse_block(
_this: &mut Self,
_prelude: Self::Prelude,
_start: &ParserState,
input: &mut Parser,
) -> Result<Self::QualifiedRule> {
Err(input.new_error(BasicParseErrorKind::qualified_rule_invalid))
}
}
};
14 changes: 14 additions & 0 deletions src/css/selectors/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,14 @@ pub enum PseudoClass {
/// The [:autofill](https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill) pseudo class.
Autofill(css::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 {
/// Plain idents: script sets the same names, so a CSS module does not rename them.
types: Vec<Ident>,
},

// CSS modules
/// The CSS modules :local() pseudo class.
Local {
Expand Down Expand Up @@ -1363,6 +1371,10 @@ impl<'a> SelectorParser<'a> {
b"dir" => PseudoClass::Dir {
direction: Direction::parse(parser)?,
},
// The spec says `<custom-ident>#`, but Blink takes any ident, so `default` must build.
b"active-view-transition-type" => PseudoClass::ActiveViewTransitionType {
types: parser.parse_comma_separated(Ident::parse)?,
},
b"local" if self.options.css_modules.is_some() => PseudoClass::Local {
selector: Box::new(Selector::parse(self, parser)?),
},
Expand Down Expand Up @@ -1514,6 +1526,8 @@ fn lookup_non_ts_pseudo_class(name: &[u8]) -> Option<PseudoClass> {
b"autofill" => P::Autofill(VP::NONE),
b"-webkit-autofill" => P::Autofill(VP::WEBKIT),
b"-o-autofill" => P::Autofill(VP::O),
// https://drafts.csswg.org/css-view-transitions-2/#pseudo-classes-for-selective-vt
b"active-view-transition" => P::ActiveViewTransition,
// https://webkit.org/blog/363/styling-scrollbars/
b"horizontal" => P::WebkitScrollbar(WS::Horizontal),
b"vertical" => P::WebkitScrollbar(WS::Vertical),
Expand Down
8 changes: 8 additions & 0 deletions src/css/selectors/selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1107,6 +1107,14 @@ pub(crate) mod serialize {
// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill
PseudoClass::Autofill(prefix) => write_prefixed(dest, *prefix, b"autofill")?,

// https://drafts.csswg.org/css-view-transitions-2/#pseudo-classes-for-selective-vt
PseudoClass::ActiveViewTransition => dest.write_str(b":active-view-transition")?,
PseudoClass::ActiveViewTransitionType { types } => {
dest.write_str(b":active-view-transition-type(")?;
dest.write_comma_separated(types.iter(), |d, ty| ty.to_css(d))?;
dest.write_char(b')')?;
}

PseudoClass::Local { selector } => serialize_selector(selector, dest, context, false)?,
PseudoClass::Global { selector } => {
let css_module = dest.css_module.take();
Expand Down
11 changes: 10 additions & 1 deletion src/css/values/ident.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,12 +495,21 @@ impl NoneOrCustomIdentList {
}

pub fn to_css(&self, dest: &mut Printer) -> Result<(), PrintErr> {
self.to_css_with_options(dest, true)
}

/// Write the list to CSS. See [`CustomIdent::to_css_with_options`].
pub(crate) fn to_css_with_options(
&self,
dest: &mut Printer,
enabled_css_modules: bool,
) -> Result<(), PrintErr> {
match self {
Self::None => dest.write_str(b"none"),
Self::Idents(idents) => dest.write_separated(
idents.slice(),
|d| d.write_char(b' '),
|d, ident| ident.to_css(d),
|d, ident| ident.to_css_with_options(d, enabled_css_modules),
),
}
}
Expand Down
Loading
Loading