use crate::css_parser as css;
use crate::css_parser::{CssResult as Result, PrintErr, Printer, Token};
use crate::values::number::{CSSInteger, CSSIntegerFns, CSSNumber, CSSNumberFns};
#[derive(Clone, PartialEq)]
pub enum EasingFunction {
Linear,
Ease,
EaseIn,
EaseOut,
EaseInOut,
CubicBezier(CubicBezier),
Steps(Steps),
}
#[derive(Clone, PartialEq)]
pub struct CubicBezier {
pub x1: CSSNumber,
pub y1: CSSNumber,
pub x2: CSSNumber,
pub y2: CSSNumber,
}
#[derive(Clone, PartialEq, Eq, Default)]
pub struct Steps {
pub count: CSSInteger,
pub position: StepPosition,
}
#[derive(Clone, Copy)]
enum EasingKeyword {
Linear,
Ease,
EaseIn,
EaseOut,
EaseInOut,
StepStart,
StepEnd,
}
fn easing_map_get_any_case(ident: &[u8]) -> Option<EasingKeyword> {
let len = ident.len();
if len < 4 || len > 11 {
return None;
}
let (buf, _) = bun_core::strings::ascii_lowercase_buf::<11>(ident)?;
let lower = &buf[..len];
match len {
4 if lower == b"ease" => Some(EasingKeyword::Ease),
6 if lower == b"linear" => Some(EasingKeyword::Linear),
7 if lower == b"ease-in" => Some(EasingKeyword::EaseIn),
8 => match lower {
b"ease-out" => Some(EasingKeyword::EaseOut),
b"step-end" => Some(EasingKeyword::StepEnd),
_ => None,
},
10 if lower == b"step-start" => Some(EasingKeyword::StepStart),
11 if lower == b"ease-in-out" => Some(EasingKeyword::EaseInOut),
_ => None,
}
}
impl EasingFunction {
pub fn parse(input: &mut css::Parser) -> Result<EasingFunction> {
let location = input.current_source_location();
let tok = input.next()?.clone();
if let Token::Ident(ident) = tok {
let keyword = if let Some(e) = easing_map_get_any_case(ident) {
match e {
EasingKeyword::Linear => EasingFunction::Linear,
EasingKeyword::Ease => EasingFunction::Ease,
EasingKeyword::EaseIn => EasingFunction::EaseIn,
EasingKeyword::EaseOut => EasingFunction::EaseOut,
EasingKeyword::EaseInOut => EasingFunction::EaseInOut,
EasingKeyword::StepStart => EasingFunction::Steps(Steps {
count: 1,
position: StepPosition::Start,
}),
EasingKeyword::StepEnd => EasingFunction::Steps(Steps {
count: 1,
position: StepPosition::End,
}),
}
} else {
return Err(location.new_unexpected_token_error(Token::Ident(ident)));
};
return Ok(keyword);
}
let Token::Function(function) = tok else {
return Err(location.new_unexpected_token_error(tok));
};
input.parse_nested_block(move |i| {
crate::match_ignore_ascii_case! { function, {
b"cubic-bezier" => {
let x1 = CSSNumberFns::parse(i)?;
i.expect_comma()?;
let y1 = CSSNumberFns::parse(i)?;
i.expect_comma()?;
let x2 = CSSNumberFns::parse(i)?;
i.expect_comma()?;
let y2 = CSSNumberFns::parse(i)?;
Ok(EasingFunction::CubicBezier(CubicBezier { x1, y1, x2, y2 }))
},
b"steps" => {
let count = CSSIntegerFns::parse(i)?;
let position = i
.try_parse(|p| {
p.expect_comma()?;
StepPosition::parse(p)
})
.unwrap_or_default();
Ok(EasingFunction::Steps(Steps { count, position }))
},
_ => Err(location.new_unexpected_token_error(Token::Ident(function))),
}}
})
}
pub fn to_css(&self, dest: &mut Printer) -> core::result::Result<(), PrintErr> {
match self {
EasingFunction::Linear => dest.write_str("linear"),
EasingFunction::Ease => dest.write_str("ease"),
EasingFunction::EaseIn => dest.write_str("ease-in"),
EasingFunction::EaseOut => dest.write_str("ease-out"),
EasingFunction::EaseInOut => dest.write_str("ease-in-out"),
_ => {
if self.is_ease() {
return dest.write_str("ease");
} else if matches!(self, EasingFunction::CubicBezier(cb) if *cb == CubicBezier {
x1: 0.42,
y1: 0.0,
x2: 1.0,
y2: 1.0,
}) {
return dest.write_str("ease-in");
} else if matches!(self, EasingFunction::CubicBezier(cb) if *cb == CubicBezier {
x1: 0.0,
y1: 0.0,
x2: 0.58,
y2: 1.0,
}) {
return dest.write_str("ease-out");
} else if matches!(self, EasingFunction::CubicBezier(cb) if *cb == CubicBezier {
x1: 0.42,
y1: 0.0,
x2: 0.58,
y2: 1.0,
}) {
return dest.write_str("ease-in-out");
}
match self {
EasingFunction::CubicBezier(cb) => {
dest.write_str("cubic-bezier(")?;
CSSNumberFns::to_css(cb.x1, dest)?;
dest.write_char(b',')?;
CSSNumberFns::to_css(cb.y1, dest)?;
dest.write_char(b',')?;
CSSNumberFns::to_css(cb.x2, dest)?;
dest.write_char(b',')?;
CSSNumberFns::to_css(cb.y2, dest)?;
dest.write_char(b')')
}
EasingFunction::Steps(steps) => {
if steps.count == 1 && steps.position == StepPosition::Start {
return dest.write_str("step-start");
}
if steps.count == 1 && steps.position == StepPosition::End {
return dest.write_str("step-end");
}
dest.write_fmt(format_args!("steps({}", steps.count))?;
dest.delim(b',', false)?;
steps.position.to_css(dest)?;
dest.write_char(b')')
}
EasingFunction::Linear
| EasingFunction::Ease
| EasingFunction::EaseIn
| EasingFunction::EaseOut
| EasingFunction::EaseInOut => unreachable!(),
}
}
}
}
pub fn is_ident(s: &[u8]) -> bool {
easing_map_get_any_case(s).is_some()
}
pub fn is_ease(&self) -> bool {
matches!(self, EasingFunction::Ease)
|| matches!(self, EasingFunction::CubicBezier(cb) if *cb == CubicBezier {
x1: 0.25,
y1: 0.1,
x2: 0.25,
y2: 1.0,
})
}
}
#[derive(Clone, Copy, PartialEq, Eq, Default, strum::IntoStaticStr)]
pub enum StepPosition {
#[strum(serialize = "start")]
Start,
#[strum(serialize = "end")]
#[default]
End,
#[strum(serialize = "jump-none")]
JumpNone,
#[strum(serialize = "jump-both")]
JumpBoth,
}
#[derive(Clone, Copy)]
enum StepPositionKeyword {
Start,
End,
JumpNone,
JumpBoth,
JumpStart,
JumpEnd,
}
fn step_position_map_get_any_case(ident: &[u8]) -> Option<StepPositionKeyword> {
let (buf, len) = bun_core::strings::ascii_lowercase_buf::<10>(ident)?;
let key = &buf[..len];
match len {
3 if key == b"end" => Some(StepPositionKeyword::End),
5 if key == b"start" => Some(StepPositionKeyword::Start),
8 if key == b"jump-end" => Some(StepPositionKeyword::JumpEnd),
9 => match key {
b"jump-none" => Some(StepPositionKeyword::JumpNone),
b"jump-both" => Some(StepPositionKeyword::JumpBoth),
_ => None,
},
10 if key == b"jump-start" => Some(StepPositionKeyword::JumpStart),
_ => None,
}
}
impl StepPosition {
pub fn to_css(self, dest: &mut Printer) -> core::result::Result<(), PrintErr> {
dest.write_str(<&'static str>::from(self))
}
pub fn parse(input: &mut css::Parser) -> Result<StepPosition> {
let location = input.current_source_location();
let tok = input.next()?.clone();
let Token::Ident(ident) = tok else {
return Err(location.new_unexpected_token_error(tok));
};
let keyword = if let Some(e) = step_position_map_get_any_case(ident) {
match e {
StepPositionKeyword::Start => StepPosition::Start,
StepPositionKeyword::End => StepPosition::End,
StepPositionKeyword::JumpStart => StepPosition::Start,
StepPositionKeyword::JumpEnd => StepPosition::End,
StepPositionKeyword::JumpNone => StepPosition::JumpNone,
StepPositionKeyword::JumpBoth => StepPosition::JumpBoth,
}
} else {
return Err(location.new_unexpected_token_error(Token::Ident(ident)));
};
Ok(keyword)
}
}