| //! CSS gradient values. |
| |
| use super::angle::{Angle, AnglePercentage}; |
| use super::color::{ColorFallbackKind, CssColor}; |
| use super::length::{Length, LengthPercentage}; |
| use super::number::CSSNumber; |
| use super::percentage::{DimensionPercentage, NumberOrPercentage, Percentage}; |
| use super::position::{HorizontalPositionKeyword, VerticalPositionKeyword}; |
| use super::position::{Position, PositionComponent}; |
| use crate::compat; |
| use crate::error::{ParserError, PrinterError}; |
| use crate::macros::enum_property; |
| use crate::prefixes::Feature; |
| use crate::printer::Printer; |
| use crate::targets::{should_compile, Browsers, Targets}; |
| use crate::traits::{IsCompatible, Parse, ToCss, TrySign, Zero}; |
| use crate::vendor_prefix::VendorPrefix; |
| #[cfg(feature = "visitor")] |
| use crate::visitor::Visit; |
| use cssparser::*; |
| use std::f32::consts::PI; |
| |
| #[cfg(feature = "serde")] |
| use crate::serialization::ValueWrapper; |
| |
| /// A CSS [`<gradient>`](https://www.w3.org/TR/css-images-3/#gradients) value. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum Gradient { |
| /// A `linear-gradient()`, and its vendor prefix. |
| Linear(LinearGradient), |
| /// A `repeating-linear-gradient()`, and its vendor prefix. |
| RepeatingLinear(LinearGradient), |
| /// A `radial-gradient()`, and its vendor prefix. |
| Radial(RadialGradient), |
| /// A `repeating-radial-gradient`, and its vendor prefix. |
| RepeatingRadial(RadialGradient), |
| /// A `conic-gradient()`. |
| Conic(ConicGradient), |
| /// A `repeating-conic-gradient()`. |
| RepeatingConic(ConicGradient), |
| /// A legacy `-webkit-gradient()`. |
| #[cfg_attr(feature = "serde", serde(rename = "webkit-gradient"))] |
| WebKitGradient(WebKitGradient), |
| } |
| |
| impl Gradient { |
| /// Returns the vendor prefix of the gradient. |
| pub fn get_vendor_prefix(&self) -> VendorPrefix { |
| match self { |
| Gradient::Linear(LinearGradient { vendor_prefix, .. }) |
| | Gradient::RepeatingLinear(LinearGradient { vendor_prefix, .. }) |
| | Gradient::Radial(RadialGradient { vendor_prefix, .. }) |
| | Gradient::RepeatingRadial(RadialGradient { vendor_prefix, .. }) => *vendor_prefix, |
| Gradient::WebKitGradient(_) => VendorPrefix::WebKit, |
| _ => VendorPrefix::None, |
| } |
| } |
| |
| /// Returns the vendor prefixes needed for the given browser targets. |
| pub fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix { |
| macro_rules! get_prefixes { |
| ($feature: ident, $prefix: expr) => { |
| targets.prefixes($prefix, Feature::$feature) |
| }; |
| } |
| |
| match self { |
| Gradient::Linear(linear) => get_prefixes!(LinearGradient, linear.vendor_prefix), |
| Gradient::RepeatingLinear(linear) => get_prefixes!(RepeatingLinearGradient, linear.vendor_prefix), |
| Gradient::Radial(radial) => get_prefixes!(RadialGradient, radial.vendor_prefix), |
| Gradient::RepeatingRadial(radial) => get_prefixes!(RepeatingRadialGradient, radial.vendor_prefix), |
| _ => VendorPrefix::None, |
| } |
| } |
| |
| /// Returns a copy of the gradient with the given vendor prefix. |
| pub fn get_prefixed(&self, prefix: VendorPrefix) -> Gradient { |
| match self { |
| Gradient::Linear(linear) => { |
| let mut new_linear = linear.clone(); |
| let needs_legacy_direction = linear.vendor_prefix == VendorPrefix::None && prefix != VendorPrefix::None; |
| if needs_legacy_direction { |
| new_linear.direction = convert_to_legacy_direction(&new_linear.direction); |
| } |
| new_linear.vendor_prefix = prefix; |
| Gradient::Linear(new_linear) |
| } |
| Gradient::RepeatingLinear(linear) => { |
| let mut new_linear = linear.clone(); |
| let needs_legacy_direction = linear.vendor_prefix == VendorPrefix::None && prefix != VendorPrefix::None; |
| if needs_legacy_direction { |
| new_linear.direction = convert_to_legacy_direction(&new_linear.direction); |
| } |
| new_linear.vendor_prefix = prefix; |
| Gradient::RepeatingLinear(new_linear) |
| } |
| Gradient::Radial(radial) => Gradient::Radial(RadialGradient { |
| vendor_prefix: prefix, |
| ..radial.clone() |
| }), |
| Gradient::RepeatingRadial(radial) => Gradient::RepeatingRadial(RadialGradient { |
| vendor_prefix: prefix, |
| ..radial.clone() |
| }), |
| _ => self.clone(), |
| } |
| } |
| |
| /// Attempts to convert the gradient to the legacy `-webkit-gradient()` syntax. |
| /// |
| /// Returns an error in case the conversion is not possible. |
| pub fn get_legacy_webkit(&self) -> Result<Gradient, ()> { |
| Ok(Gradient::WebKitGradient(WebKitGradient::from_standard(self)?)) |
| } |
| |
| /// Returns the color fallback types needed for the given browser targets. |
| pub fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind { |
| match self { |
| Gradient::Linear(LinearGradient { items, .. }) |
| | Gradient::Radial(RadialGradient { items, .. }) |
| | Gradient::RepeatingLinear(LinearGradient { items, .. }) |
| | Gradient::RepeatingRadial(RadialGradient { items, .. }) => { |
| let mut fallbacks = ColorFallbackKind::empty(); |
| for item in items { |
| fallbacks |= item.get_necessary_fallbacks(targets) |
| } |
| fallbacks |
| } |
| Gradient::Conic(ConicGradient { items, .. }) | Gradient::RepeatingConic(ConicGradient { items, .. }) => { |
| let mut fallbacks = ColorFallbackKind::empty(); |
| for item in items { |
| fallbacks |= item.get_necessary_fallbacks(targets) |
| } |
| fallbacks |
| } |
| Gradient::WebKitGradient(..) => ColorFallbackKind::empty(), |
| } |
| } |
| |
| /// Returns a fallback gradient for the given color fallback type. |
| pub fn get_fallback(&self, kind: ColorFallbackKind) -> Gradient { |
| match self { |
| Gradient::Linear(g) => Gradient::Linear(g.get_fallback(kind)), |
| Gradient::RepeatingLinear(g) => Gradient::RepeatingLinear(g.get_fallback(kind)), |
| Gradient::Radial(g) => Gradient::Radial(g.get_fallback(kind)), |
| Gradient::RepeatingRadial(g) => Gradient::RepeatingRadial(g.get_fallback(kind)), |
| Gradient::Conic(g) => Gradient::Conic(g.get_fallback(kind)), |
| Gradient::RepeatingConic(g) => Gradient::RepeatingConic(g.get_fallback(kind)), |
| Gradient::WebKitGradient(g) => Gradient::WebKitGradient(g.get_fallback(kind)), |
| } |
| } |
| } |
| |
| impl<'i> Parse<'i> for Gradient { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let location = input.current_source_location(); |
| let func = input.expect_function()?.clone(); |
| input.parse_nested_block(|input| { |
| match_ignore_ascii_case! { &func, |
| "linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::None)?)), |
| "repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::None)?)), |
| "radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::None)?)), |
| "repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::None)?)), |
| "conic-gradient" => Ok(Gradient::Conic(ConicGradient::parse(input)?)), |
| "repeating-conic-gradient" => Ok(Gradient::RepeatingConic(ConicGradient::parse(input)?)), |
| "-webkit-linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::WebKit)?)), |
| "-webkit-repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::WebKit)?)), |
| "-webkit-radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::WebKit)?)), |
| "-webkit-repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::WebKit)?)), |
| "-moz-linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::Moz)?)), |
| "-moz-repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::Moz)?)), |
| "-moz-radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::Moz)?)), |
| "-moz-repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::Moz)?)), |
| "-o-linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::O)?)), |
| "-o-repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::O)?)), |
| "-o-radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::O)?)), |
| "-o-repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::O)?)), |
| "-webkit-gradient" => Ok(Gradient::WebKitGradient(WebKitGradient::parse(input)?)), |
| _ => Err(location.new_unexpected_token_error(cssparser::Token::Ident(func.clone()))) |
| } |
| }) |
| } |
| } |
| |
| impl ToCss for Gradient { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| let (f, prefix) = match self { |
| Gradient::Linear(g) => ("linear-gradient(", Some(g.vendor_prefix)), |
| Gradient::RepeatingLinear(g) => ("repeating-linear-gradient(", Some(g.vendor_prefix)), |
| Gradient::Radial(g) => ("radial-gradient(", Some(g.vendor_prefix)), |
| Gradient::RepeatingRadial(g) => ("repeating-radial-gradient(", Some(g.vendor_prefix)), |
| Gradient::Conic(_) => ("conic-gradient(", None), |
| Gradient::RepeatingConic(_) => ("repeating-conic-gradient(", None), |
| Gradient::WebKitGradient(_) => ("-webkit-gradient(", None), |
| }; |
| |
| if let Some(prefix) = prefix { |
| prefix.to_css(dest)?; |
| } |
| |
| dest.write_str(f)?; |
| |
| match self { |
| Gradient::Linear(linear) | Gradient::RepeatingLinear(linear) => { |
| linear.to_css(dest, linear.vendor_prefix != VendorPrefix::None)? |
| } |
| Gradient::Radial(radial) | Gradient::RepeatingRadial(radial) => radial.to_css(dest)?, |
| Gradient::Conic(conic) | Gradient::RepeatingConic(conic) => conic.to_css(dest)?, |
| Gradient::WebKitGradient(g) => g.to_css(dest)?, |
| } |
| |
| dest.write_char(')') |
| } |
| } |
| |
| /// A CSS [`linear-gradient()`](https://www.w3.org/TR/css-images-3/#linear-gradients) or `repeating-linear-gradient()`. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(rename_all = "camelCase") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub struct LinearGradient { |
| /// The vendor prefixes for the gradient. |
| pub vendor_prefix: VendorPrefix, |
| /// The direction of the gradient. |
| pub direction: LineDirection, |
| /// The color stops and transition hints for the gradient. |
| pub items: Vec<GradientItem<LengthPercentage>>, |
| } |
| |
| impl LinearGradient { |
| fn parse<'i, 't>( |
| input: &mut Parser<'i, 't>, |
| vendor_prefix: VendorPrefix, |
| ) -> Result<LinearGradient, ParseError<'i, ParserError<'i>>> { |
| let direction = if let Ok(direction) = |
| input.try_parse(|input| LineDirection::parse(input, vendor_prefix != VendorPrefix::None)) |
| { |
| input.expect_comma()?; |
| direction |
| } else { |
| LineDirection::Vertical(VerticalPositionKeyword::Bottom) |
| }; |
| let items = parse_items(input)?; |
| Ok(LinearGradient { |
| direction, |
| items, |
| vendor_prefix, |
| }) |
| } |
| |
| fn to_css<W>(&self, dest: &mut Printer<W>, is_prefixed: bool) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| let angle = match &self.direction { |
| LineDirection::Vertical(VerticalPositionKeyword::Bottom) => 180.0, |
| LineDirection::Vertical(VerticalPositionKeyword::Top) => 0.0, |
| LineDirection::Angle(angle) => angle.to_degrees(), |
| _ => -1.0, |
| }; |
| |
| // We can omit `to bottom` or `180deg` because it is the default. |
| if angle == 180.0 { |
| serialize_items(&self.items, dest) |
| |
| // If we have `to top` or `0deg`, and all of the positions and hints are percentages, |
| // we can flip the gradient the other direction and omit the direction. |
| } else if angle == 0.0 |
| && dest.minify |
| && self.items.iter().all(|item| { |
| matches!( |
| item, |
| GradientItem::Hint(LengthPercentage::Percentage(_)) |
| | GradientItem::ColorStop(ColorStop { |
| position: None | Some(LengthPercentage::Percentage(_)), |
| .. |
| }) |
| ) |
| }) |
| { |
| let items: Vec<GradientItem<LengthPercentage>> = self |
| .items |
| .iter() |
| .rev() |
| .map(|item| { |
| // Flip percentages. |
| match item { |
| GradientItem::Hint(LengthPercentage::Percentage(p)) => { |
| GradientItem::Hint(LengthPercentage::Percentage(Percentage(1.0 - p.0))) |
| } |
| GradientItem::ColorStop(ColorStop { color, position }) => GradientItem::ColorStop(ColorStop { |
| color: color.clone(), |
| position: position.clone().map(|p| match p { |
| LengthPercentage::Percentage(p) => LengthPercentage::Percentage(Percentage(1.0 - p.0)), |
| _ => unreachable!(), |
| }), |
| }), |
| _ => unreachable!(), |
| } |
| }) |
| .collect(); |
| serialize_items(&items, dest) |
| } else { |
| if self.direction != LineDirection::Vertical(VerticalPositionKeyword::Bottom) |
| && self.direction != LineDirection::Angle(Angle::Deg(180.0)) |
| { |
| self.direction.to_css(dest, is_prefixed)?; |
| dest.delim(',', false)?; |
| } |
| |
| serialize_items(&self.items, dest) |
| } |
| } |
| |
| fn get_fallback(&self, kind: ColorFallbackKind) -> LinearGradient { |
| LinearGradient { |
| direction: self.direction.clone(), |
| items: self.items.iter().map(|item| item.get_fallback(kind)).collect(), |
| vendor_prefix: self.vendor_prefix, |
| } |
| } |
| } |
| |
| impl IsCompatible for LinearGradient { |
| fn is_compatible(&self, browsers: Browsers) -> bool { |
| self.items.iter().all(|item| item.is_compatible(browsers)) |
| } |
| } |
| |
| /// A CSS [`radial-gradient()`](https://www.w3.org/TR/css-images-3/#radial-gradients) or `repeating-radial-gradient()`. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(rename_all = "camelCase") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub struct RadialGradient { |
| /// The vendor prefixes for the gradient. |
| pub vendor_prefix: VendorPrefix, |
| /// The shape of the gradient. |
| pub shape: EndingShape, |
| /// The position of the gradient. |
| pub position: Position, |
| /// The color stops and transition hints for the gradient. |
| pub items: Vec<GradientItem<LengthPercentage>>, |
| } |
| |
| impl<'i> RadialGradient { |
| fn parse<'t>( |
| input: &mut Parser<'i, 't>, |
| vendor_prefix: VendorPrefix, |
| ) -> Result<RadialGradient, ParseError<'i, ParserError<'i>>> { |
| let shape = input.try_parse(EndingShape::parse).ok(); |
| let position = input |
| .try_parse(|input| { |
| input.expect_ident_matching("at")?; |
| Position::parse(input) |
| }) |
| .ok(); |
| |
| if shape.is_some() || position.is_some() { |
| input.expect_comma()?; |
| } |
| |
| let items = parse_items(input)?; |
| Ok(RadialGradient { |
| shape: shape.unwrap_or_default(), |
| position: position.unwrap_or(Position::center()), |
| items, |
| vendor_prefix, |
| }) |
| } |
| } |
| |
| impl ToCss for RadialGradient { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| if self.shape != EndingShape::default() { |
| self.shape.to_css(dest)?; |
| if self.position.is_center() { |
| dest.delim(',', false)?; |
| } else { |
| dest.write_char(' ')?; |
| } |
| } |
| |
| if !self.position.is_center() { |
| dest.write_str("at ")?; |
| self.position.to_css(dest)?; |
| dest.delim(',', false)?; |
| } |
| |
| serialize_items(&self.items, dest) |
| } |
| } |
| |
| impl RadialGradient { |
| fn get_fallback(&self, kind: ColorFallbackKind) -> RadialGradient { |
| RadialGradient { |
| shape: self.shape.clone(), |
| position: self.position.clone(), |
| items: self.items.iter().map(|item| item.get_fallback(kind)).collect(), |
| vendor_prefix: self.vendor_prefix, |
| } |
| } |
| } |
| |
| impl IsCompatible for RadialGradient { |
| fn is_compatible(&self, browsers: Browsers) -> bool { |
| self.items.iter().all(|item| item.is_compatible(browsers)) |
| } |
| } |
| |
| /// The direction of a CSS `linear-gradient()`. |
| /// |
| /// See [LinearGradient](LinearGradient). |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| pub enum LineDirection { |
| /// An angle. |
| #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Angle>"))] |
| Angle(Angle), |
| /// A horizontal position keyword, e.g. `left` or `right. |
| #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<HorizontalPositionKeyword>"))] |
| Horizontal(HorizontalPositionKeyword), |
| /// A vertical posision keyword, e.g. `top` or `bottom`. |
| #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<VerticalPositionKeyword>"))] |
| Vertical(VerticalPositionKeyword), |
| /// A corner, e.g. `bottom left` or `top right`. |
| Corner { |
| /// A horizontal position keyword, e.g. `left` or `right. |
| horizontal: HorizontalPositionKeyword, |
| /// A vertical posision keyword, e.g. `top` or `bottom`. |
| vertical: VerticalPositionKeyword, |
| }, |
| } |
| |
| impl LineDirection { |
| fn parse<'i, 't>( |
| input: &mut Parser<'i, 't>, |
| is_prefixed: bool, |
| ) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| // Spec allows unitless zero angles for gradients. |
| // https://w3c.github.io/csswg-drafts/css-images-3/#linear-gradient-syntax |
| if let Ok(angle) = input.try_parse(Angle::parse_with_unitless_zero) { |
| return Ok(LineDirection::Angle(angle)); |
| } |
| |
| if !is_prefixed { |
| input.expect_ident_matching("to")?; |
| } |
| |
| if let Ok(x) = input.try_parse(HorizontalPositionKeyword::parse) { |
| if let Ok(y) = input.try_parse(VerticalPositionKeyword::parse) { |
| return Ok(LineDirection::Corner { |
| horizontal: x, |
| vertical: y, |
| }); |
| } |
| return Ok(LineDirection::Horizontal(x)); |
| } |
| |
| let y = VerticalPositionKeyword::parse(input)?; |
| if let Ok(x) = input.try_parse(HorizontalPositionKeyword::parse) { |
| return Ok(LineDirection::Corner { |
| horizontal: x, |
| vertical: y, |
| }); |
| } |
| Ok(LineDirection::Vertical(y)) |
| } |
| |
| fn to_css<W>(&self, dest: &mut Printer<W>, is_prefixed: bool) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| match self { |
| LineDirection::Angle(angle) => angle.to_css(dest), |
| LineDirection::Horizontal(k) => { |
| if dest.minify { |
| match k { |
| HorizontalPositionKeyword::Left => dest.write_str("270deg"), |
| HorizontalPositionKeyword::Right => dest.write_str("90deg"), |
| } |
| } else { |
| if !is_prefixed { |
| dest.write_str("to ")?; |
| } |
| k.to_css(dest) |
| } |
| } |
| LineDirection::Vertical(k) => { |
| if dest.minify { |
| match k { |
| VerticalPositionKeyword::Top => dest.write_str("0deg"), |
| VerticalPositionKeyword::Bottom => dest.write_str("180deg"), |
| } |
| } else { |
| if !is_prefixed { |
| dest.write_str("to ")?; |
| } |
| k.to_css(dest) |
| } |
| } |
| LineDirection::Corner { horizontal, vertical } => { |
| if !is_prefixed { |
| dest.write_str("to ")?; |
| } |
| vertical.to_css(dest)?; |
| dest.write_char(' ')?; |
| horizontal.to_css(dest) |
| } |
| } |
| } |
| } |
| |
| /// Converts a standard gradient direction to its legacy vendor-prefixed form. |
| /// |
| /// Inverts keyword-based directions (e.g., `to bottom` → `top`) for compatibility |
| /// with legacy prefixed syntaxes. |
| /// |
| /// See: https://github.com/parcel-bundler/lightningcss/issues/918 |
| fn convert_to_legacy_direction(direction: &LineDirection) -> LineDirection { |
| match direction { |
| LineDirection::Horizontal(HorizontalPositionKeyword::Left) => { |
| LineDirection::Horizontal(HorizontalPositionKeyword::Right) |
| } |
| LineDirection::Horizontal(HorizontalPositionKeyword::Right) => { |
| LineDirection::Horizontal(HorizontalPositionKeyword::Left) |
| } |
| LineDirection::Vertical(VerticalPositionKeyword::Top) => { |
| LineDirection::Vertical(VerticalPositionKeyword::Bottom) |
| } |
| LineDirection::Vertical(VerticalPositionKeyword::Bottom) => { |
| LineDirection::Vertical(VerticalPositionKeyword::Top) |
| } |
| LineDirection::Corner { horizontal, vertical } => LineDirection::Corner { |
| horizontal: match horizontal { |
| HorizontalPositionKeyword::Left => HorizontalPositionKeyword::Right, |
| HorizontalPositionKeyword::Right => HorizontalPositionKeyword::Left, |
| }, |
| vertical: match vertical { |
| VerticalPositionKeyword::Top => VerticalPositionKeyword::Bottom, |
| VerticalPositionKeyword::Bottom => VerticalPositionKeyword::Top, |
| }, |
| }, |
| LineDirection::Angle(angle) => { |
| let angle = angle.clone(); |
| let deg = match angle { |
| Angle::Deg(n) => convert_to_legacy_degree(n), |
| Angle::Rad(n) => { |
| let n = n / (2.0 * PI) * 360.0; |
| convert_to_legacy_degree(n) |
| } |
| Angle::Grad(n) => { |
| let n = n / 400.0 * 360.0; |
| convert_to_legacy_degree(n) |
| } |
| Angle::Turn(n) => { |
| let n = n * 360.0; |
| convert_to_legacy_degree(n) |
| } |
| }; |
| LineDirection::Angle(Angle::Deg(deg)) |
| } |
| } |
| } |
| |
| fn convert_to_legacy_degree(degree: f32) -> f32 { |
| // Add 90 degrees |
| let n = (450.0 - degree).abs() % 360.0; |
| // Round the number to 3 decimal places |
| (n * 1000.0).round() / 1000.0 |
| } |
| |
| /// A `radial-gradient()` [ending shape](https://www.w3.org/TR/css-images-3/#valdef-radial-gradient-ending-shape). |
| /// |
| /// See [RadialGradient](RadialGradient). |
| #[derive(Debug, Clone, PartialEq, Parse, ToCss)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", content = "value", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| pub enum EndingShape { |
| // Note: Ellipse::parse MUST run before Circle::parse for this to be correct. |
| /// An ellipse. |
| Ellipse(Ellipse), |
| /// A circle. |
| Circle(Circle), |
| } |
| |
| impl Default for EndingShape { |
| fn default() -> EndingShape { |
| EndingShape::Ellipse(Ellipse::Extent(ShapeExtent::FarthestCorner)) |
| } |
| } |
| |
| /// A circle ending shape for a `radial-gradient()`. |
| /// |
| /// See [RadialGradient](RadialGradient). |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", content = "value", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| pub enum Circle { |
| /// A circle with a specified radius. |
| Radius(Length), |
| /// A shape extent keyword. |
| Extent(ShapeExtent), |
| } |
| |
| impl<'i> Parse<'i> for Circle { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| if let Ok(extent) = input.try_parse(ShapeExtent::parse) { |
| // The `circle` keyword is required. If it's not there, then it's an ellipse. |
| input.expect_ident_matching("circle")?; |
| return Ok(Circle::Extent(extent)); |
| } |
| |
| if let Ok(length) = input.try_parse(Length::parse) { |
| // The `circle` keyword is optional if there is only a single length. |
| // We are assuming here that Ellipse::parse ran first. |
| let _ = input.try_parse(|input| input.expect_ident_matching("circle")); |
| return Ok(Circle::Radius(length)); |
| } |
| |
| if input.try_parse(|input| input.expect_ident_matching("circle")).is_ok() { |
| if let Ok(extent) = input.try_parse(ShapeExtent::parse) { |
| return Ok(Circle::Extent(extent)); |
| } |
| |
| if let Ok(length) = input.try_parse(Length::parse) { |
| return Ok(Circle::Radius(length)); |
| } |
| |
| // If only the `circle` keyword was given, default to `farthest-corner`. |
| return Ok(Circle::Extent(ShapeExtent::FarthestCorner)); |
| } |
| |
| return Err(input.new_error_for_next_token()); |
| } |
| } |
| |
| impl ToCss for Circle { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| match self { |
| Circle::Radius(r) => r.to_css(dest), |
| Circle::Extent(extent) => { |
| dest.write_str("circle")?; |
| if *extent != ShapeExtent::FarthestCorner { |
| dest.write_char(' ')?; |
| extent.to_css(dest)?; |
| } |
| Ok(()) |
| } |
| } |
| } |
| } |
| |
| /// An ellipse ending shape for a `radial-gradient()`. |
| /// |
| /// See [RadialGradient](RadialGradient). |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| pub enum Ellipse { |
| /// An ellipse with a specified horizontal and vertical radius. |
| Size { |
| /// The x-radius of the ellipse. |
| x: LengthPercentage, |
| /// The y-radius of the ellipse. |
| y: LengthPercentage, |
| }, |
| /// A shape extent keyword. |
| #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<ShapeExtent>"))] |
| Extent(ShapeExtent), |
| } |
| |
| impl<'i> Parse<'i> for Ellipse { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| if let Ok(extent) = input.try_parse(ShapeExtent::parse) { |
| // The `ellipse` keyword is optional, but only if the `circle` keyword is not present. |
| // If it is, then we'll re-parse as a circle. |
| if input.try_parse(|input| input.expect_ident_matching("circle")).is_ok() { |
| return Err(input.new_error_for_next_token()); |
| } |
| let _ = input.try_parse(|input| input.expect_ident_matching("ellipse")); |
| return Ok(Ellipse::Extent(extent)); |
| } |
| |
| if let Ok(x) = input.try_parse(LengthPercentage::parse) { |
| let y = LengthPercentage::parse(input)?; |
| // The `ellipse` keyword is optional if there are two lengths. |
| let _ = input.try_parse(|input| input.expect_ident_matching("ellipse")); |
| return Ok(Ellipse::Size { x, y }); |
| } |
| |
| if input.try_parse(|input| input.expect_ident_matching("ellipse")).is_ok() { |
| if let Ok(extent) = input.try_parse(ShapeExtent::parse) { |
| return Ok(Ellipse::Extent(extent)); |
| } |
| |
| if let Ok(x) = input.try_parse(LengthPercentage::parse) { |
| let y = LengthPercentage::parse(input)?; |
| return Ok(Ellipse::Size { x, y }); |
| } |
| |
| // Assume `farthest-corner` if only the `ellipse` keyword is present. |
| return Ok(Ellipse::Extent(ShapeExtent::FarthestCorner)); |
| } |
| |
| return Err(input.new_error_for_next_token()); |
| } |
| } |
| |
| impl ToCss for Ellipse { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| // The `ellipse` keyword is optional, so we don't emit it. |
| match self { |
| Ellipse::Size { x, y } => { |
| x.to_css(dest)?; |
| dest.write_char(' ')?; |
| y.to_css(dest) |
| } |
| Ellipse::Extent(extent) => extent.to_css(dest), |
| } |
| } |
| } |
| |
| enum_property! { |
| /// A shape extent for a `radial-gradient()`. |
| /// |
| /// See [RadialGradient](RadialGradient). |
| pub enum ShapeExtent { |
| /// The closest side of the box to the gradient's center. |
| ClosestSide, |
| /// The farthest side of the box from the gradient's center. |
| FarthestSide, |
| /// The closest cornder of the box to the gradient's center. |
| ClosestCorner, |
| /// The farthest corner of the box from the gradient's center. |
| FarthestCorner, |
| } |
| } |
| |
| /// A CSS [`conic-gradient()`](https://www.w3.org/TR/css-images-4/#conic-gradients) or `repeating-conic-gradient()`. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[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 ConicGradient { |
| /// The angle of the gradient. |
| pub angle: Angle, |
| /// The position of the gradient. |
| pub position: Position, |
| /// The color stops and transition hints for the gradient. |
| pub items: Vec<GradientItem<AnglePercentage>>, |
| } |
| |
| impl ConicGradient { |
| fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let angle = input.try_parse(|input| { |
| input.expect_ident_matching("from")?; |
| // Spec allows unitless zero angles for gradients. |
| // https://w3c.github.io/csswg-drafts/css-images-4/#valdef-conic-gradient-angle |
| Angle::parse_with_unitless_zero(input) |
| }); |
| |
| let position = input.try_parse(|input| { |
| input.expect_ident_matching("at")?; |
| Position::parse(input) |
| }); |
| |
| if angle.is_ok() || position.is_ok() { |
| input.expect_comma()?; |
| } |
| |
| let items = parse_items(input)?; |
| Ok(ConicGradient { |
| angle: angle.unwrap_or(Angle::Deg(0.0)), |
| position: position.unwrap_or(Position::center()), |
| items, |
| }) |
| } |
| } |
| |
| impl ToCss for ConicGradient { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| if !self.angle.is_zero() { |
| dest.write_str("from ")?; |
| self.angle.to_css(dest)?; |
| |
| if self.position.is_center() { |
| dest.delim(',', false)?; |
| } else { |
| dest.write_char(' ')?; |
| } |
| } |
| |
| if !self.position.is_center() { |
| dest.write_str("at ")?; |
| self.position.to_css(dest)?; |
| dest.delim(',', false)?; |
| } |
| |
| serialize_items(&self.items, dest) |
| } |
| } |
| |
| impl ConicGradient { |
| fn get_fallback(&self, kind: ColorFallbackKind) -> ConicGradient { |
| ConicGradient { |
| angle: self.angle.clone(), |
| position: self.position.clone(), |
| items: self.items.iter().map(|item| item.get_fallback(kind)).collect(), |
| } |
| } |
| } |
| |
| impl IsCompatible for ConicGradient { |
| fn is_compatible(&self, browsers: Browsers) -> bool { |
| self.items.iter().all(|item| item.is_compatible(browsers)) |
| } |
| } |
| |
| /// A [`<color-stop>`](https://www.w3.org/TR/css-images-4/#color-stop-syntax) within a gradient. |
| /// |
| /// This type is generic, and may be either a [LengthPercentage](super::length::LengthPercentage) |
| /// or [Angle](super::angle::Angle) depending on what type of gradient it is within. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| pub struct ColorStop<D> { |
| /// The color of the color stop. |
| pub color: CssColor, |
| /// The position of the color stop. |
| pub position: Option<D>, |
| } |
| |
| impl<'i, D: Parse<'i>> Parse<'i> for ColorStop<D> { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let color = CssColor::parse(input)?; |
| let position = input.try_parse(D::parse).ok(); |
| Ok(ColorStop { color, position }) |
| } |
| } |
| |
| impl<D: ToCss> ToCss for ColorStop<D> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| self.color.to_css(dest)?; |
| if let Some(position) = &self.position { |
| dest.write_char(' ')?; |
| position.to_css(dest)?; |
| } |
| Ok(()) |
| } |
| } |
| |
| /// Either a color stop or interpolation hint within a gradient. |
| /// |
| /// This type is generic, and items may be either a [LengthPercentage](super::length::LengthPercentage) |
| /// or [Angle](super::angle::Angle) depending on what type of gradient it is within. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| pub enum GradientItem<D> { |
| /// A color stop. |
| ColorStop(ColorStop<D>), |
| /// A color interpolation hint. |
| #[cfg_attr( |
| feature = "serde", |
| serde( |
| bound(serialize = "D: serde::Serialize", deserialize = "D: serde::Deserialize<'de>"), |
| with = "ValueWrapper::<D>" |
| ) |
| )] |
| Hint(D), |
| } |
| |
| impl<D: ToCss> ToCss for GradientItem<D> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| match self { |
| GradientItem::ColorStop(stop) => stop.to_css(dest), |
| GradientItem::Hint(hint) => hint.to_css(dest), |
| } |
| } |
| } |
| |
| impl<D: Clone> GradientItem<D> { |
| /// Returns the color fallback types needed for the given browser targets. |
| pub fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind { |
| match self { |
| GradientItem::ColorStop(stop) => stop.color.get_necessary_fallbacks(targets), |
| GradientItem::Hint(..) => ColorFallbackKind::empty(), |
| } |
| } |
| |
| /// Returns a fallback gradient item for the given color fallback type. |
| pub fn get_fallback(&self, kind: ColorFallbackKind) -> GradientItem<D> { |
| match self { |
| GradientItem::ColorStop(stop) => GradientItem::ColorStop(ColorStop { |
| color: stop.color.get_fallback(kind), |
| position: stop.position.clone(), |
| }), |
| GradientItem::Hint(..) => self.clone(), |
| } |
| } |
| } |
| |
| impl<D> IsCompatible for GradientItem<D> { |
| fn is_compatible(&self, browsers: Browsers) -> bool { |
| match self { |
| GradientItem::ColorStop(c) => c.color.is_compatible(browsers), |
| GradientItem::Hint(..) => compat::Feature::GradientInterpolationHints.is_compatible(browsers), |
| } |
| } |
| } |
| |
| fn parse_items<'i, 't, D: Parse<'i>>( |
| input: &mut Parser<'i, 't>, |
| ) -> Result<Vec<GradientItem<D>>, ParseError<'i, ParserError<'i>>> { |
| let mut items = Vec::new(); |
| let mut seen_stop = false; |
| |
| loop { |
| input.parse_until_before(Delimiter::Comma, |input| { |
| if seen_stop { |
| if let Ok(hint) = input.try_parse(D::parse) { |
| seen_stop = false; |
| items.push(GradientItem::Hint(hint)); |
| return Ok(()); |
| } |
| } |
| |
| let stop = ColorStop::parse(input)?; |
| |
| if let Ok(position) = input.try_parse(D::parse) { |
| let color = stop.color.clone(); |
| items.push(GradientItem::ColorStop(stop)); |
| |
| items.push(GradientItem::ColorStop(ColorStop { |
| color, |
| position: Some(position), |
| })) |
| } else { |
| items.push(GradientItem::ColorStop(stop)); |
| } |
| |
| seen_stop = true; |
| Ok(()) |
| })?; |
| |
| match input.next() { |
| Err(_) => break, |
| Ok(Token::Comma) => continue, |
| _ => unreachable!(), |
| } |
| } |
| |
| Ok(items) |
| } |
| |
| fn serialize_items< |
| D: ToCss + std::cmp::PartialEq<D> + std::ops::Mul<f32, Output = D> + TrySign + Clone + std::fmt::Debug, |
| W, |
| >( |
| items: &Vec<GradientItem<DimensionPercentage<D>>>, |
| dest: &mut Printer<W>, |
| ) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| let mut first = true; |
| let mut last: Option<&GradientItem<DimensionPercentage<D>>> = None; |
| for item in items { |
| // Skip useless hints |
| if *item == GradientItem::Hint(DimensionPercentage::Percentage(Percentage(0.5))) { |
| continue; |
| } |
| |
| // Use double position stop if the last stop is the same color and all targets support it. |
| if let Some(prev) = last { |
| if !should_compile!(dest.targets.current, DoublePositionGradients) { |
| match (prev, item) { |
| ( |
| GradientItem::ColorStop(ColorStop { |
| position: Some(_), |
| color: ca, |
| }), |
| GradientItem::ColorStop(ColorStop { |
| position: Some(p), |
| color: cb, |
| }), |
| ) if ca == cb => { |
| dest.write_char(' ')?; |
| p.to_css(dest)?; |
| last = None; |
| continue; |
| } |
| _ => {} |
| } |
| } |
| } |
| |
| if first { |
| first = false; |
| } else { |
| dest.delim(',', false)?; |
| } |
| item.to_css(dest)?; |
| last = Some(item) |
| } |
| Ok(()) |
| } |
| |
| /// A legacy `-webkit-gradient()`. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "kind", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum WebKitGradient { |
| /// A linear `-webkit-gradient()`. |
| Linear { |
| /// The starting point of the gradient. |
| from: WebKitGradientPoint, |
| /// The ending point of the gradient. |
| to: WebKitGradientPoint, |
| /// The color stops in the gradient. |
| stops: Vec<WebKitColorStop>, |
| }, |
| /// A radial `-webkit-gradient()`. |
| Radial { |
| /// The starting point of the gradient. |
| from: WebKitGradientPoint, |
| /// The starting radius of the gradient. |
| r0: CSSNumber, |
| /// The ending point of the gradient. |
| to: WebKitGradientPoint, |
| /// The ending radius of the gradient. |
| r1: CSSNumber, |
| /// The color stops in the gradient. |
| stops: Vec<WebKitColorStop>, |
| }, |
| } |
| |
| impl<'i> Parse<'i> for WebKitGradient { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let location = input.current_source_location(); |
| let ident = input.expect_ident_cloned()?; |
| input.expect_comma()?; |
| |
| match_ignore_ascii_case! { &ident, |
| "linear" => { |
| let from = WebKitGradientPoint::parse(input)?; |
| input.expect_comma()?; |
| let to = WebKitGradientPoint::parse(input)?; |
| input.expect_comma()?; |
| let stops = input.parse_comma_separated(WebKitColorStop::parse)?; |
| Ok(WebKitGradient::Linear { |
| from, |
| to, |
| stops |
| }) |
| }, |
| "radial" => { |
| let from = WebKitGradientPoint::parse(input)?; |
| input.expect_comma()?; |
| let r0 = CSSNumber::parse(input)?; |
| input.expect_comma()?; |
| let to = WebKitGradientPoint::parse(input)?; |
| input.expect_comma()?; |
| let r1 = CSSNumber::parse(input)?; |
| input.expect_comma()?; |
| let stops = input.parse_comma_separated(WebKitColorStop::parse)?; |
| Ok(WebKitGradient::Radial { |
| from, |
| r0, |
| to, |
| r1, |
| stops |
| }) |
| }, |
| _ => Err(location.new_unexpected_token_error(cssparser::Token::Ident(ident.clone()))) |
| } |
| } |
| } |
| |
| impl ToCss for WebKitGradient { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| match self { |
| WebKitGradient::Linear { from, to, stops } => { |
| dest.write_str("linear")?; |
| dest.delim(',', false)?; |
| from.to_css(dest)?; |
| dest.delim(',', false)?; |
| to.to_css(dest)?; |
| for stop in stops { |
| dest.delim(',', false)?; |
| stop.to_css(dest)?; |
| } |
| Ok(()) |
| } |
| WebKitGradient::Radial { |
| from, |
| r0, |
| to, |
| r1, |
| stops, |
| } => { |
| dest.write_str("radial")?; |
| dest.delim(',', false)?; |
| from.to_css(dest)?; |
| dest.delim(',', false)?; |
| r0.to_css(dest)?; |
| dest.delim(',', false)?; |
| to.to_css(dest)?; |
| dest.delim(',', false)?; |
| r1.to_css(dest)?; |
| for stop in stops { |
| dest.delim(',', false)?; |
| stop.to_css(dest)?; |
| } |
| Ok(()) |
| } |
| } |
| } |
| } |
| |
| impl WebKitGradient { |
| fn get_fallback(&self, kind: ColorFallbackKind) -> WebKitGradient { |
| let stops = match self { |
| WebKitGradient::Linear { stops, .. } => stops, |
| WebKitGradient::Radial { stops, .. } => stops, |
| }; |
| |
| let stops = stops.iter().map(|stop| stop.get_fallback(kind)).collect(); |
| |
| match self { |
| WebKitGradient::Linear { from, to, .. } => WebKitGradient::Linear { |
| from: from.clone(), |
| to: to.clone(), |
| stops, |
| }, |
| WebKitGradient::Radial { from, r0, to, r1, .. } => WebKitGradient::Radial { |
| from: from.clone(), |
| r0: *r0, |
| to: to.clone(), |
| r1: *r1, |
| stops, |
| }, |
| } |
| } |
| } |
| |
| /// A color stop within a legacy `-webkit-gradient()`. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| pub struct WebKitColorStop { |
| /// The color of the color stop. |
| pub color: CssColor, |
| /// The position of the color stop. |
| pub position: CSSNumber, |
| } |
| |
| impl<'i> Parse<'i> for WebKitColorStop { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let location = input.current_source_location(); |
| let function = input.expect_function()?.clone(); |
| input.parse_nested_block(|input| { |
| let position = match_ignore_ascii_case! { &function, |
| "color-stop" => { |
| let p = NumberOrPercentage::parse(input)?; |
| input.expect_comma()?; |
| (&p).into() |
| }, |
| "from" => 0.0, |
| "to" => 1.0, |
| _ => return Err(location.new_unexpected_token_error(cssparser::Token::Ident(function.clone()))) |
| }; |
| let color = CssColor::parse(input)?; |
| Ok(WebKitColorStop { color, position }) |
| }) |
| } |
| } |
| |
| impl ToCss for WebKitColorStop { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| if self.position == 0.0 { |
| dest.write_str("from(")?; |
| self.color.to_css(dest)?; |
| } else if self.position == 1.0 { |
| dest.write_str("to(")?; |
| self.color.to_css(dest)?; |
| } else { |
| dest.write_str("color-stop(")?; |
| self.position.to_css(dest)?; |
| dest.delim(',', false)?; |
| self.color.to_css(dest)?; |
| } |
| dest.write_char(')') |
| } |
| } |
| |
| impl WebKitColorStop { |
| fn get_fallback(&self, kind: ColorFallbackKind) -> WebKitColorStop { |
| WebKitColorStop { |
| color: self.color.get_fallback(kind), |
| position: self.position, |
| } |
| } |
| } |
| |
| /// An x/y position within a legacy `-webkit-gradient()`. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[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 WebKitGradientPoint { |
| /// The x-position. |
| pub x: WebKitGradientPointComponent<HorizontalPositionKeyword>, |
| /// The y-position. |
| pub y: WebKitGradientPointComponent<VerticalPositionKeyword>, |
| } |
| |
| impl<'i> Parse<'i> for WebKitGradientPoint { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let x = WebKitGradientPointComponent::parse(input)?; |
| let y = WebKitGradientPointComponent::parse(input)?; |
| Ok(WebKitGradientPoint { x, y }) |
| } |
| } |
| |
| impl ToCss for WebKitGradientPoint { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| self.x.to_css(dest)?; |
| dest.write_char(' ')?; |
| self.y.to_css(dest) |
| } |
| } |
| |
| /// A keyword or number within a [WebKitGradientPoint](WebKitGradientPoint). |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", content = "value", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum WebKitGradientPointComponent<S> { |
| /// The `center` keyword. |
| Center, |
| /// A number or percentage. |
| Number(NumberOrPercentage), |
| /// A side keyword. |
| Side(S), |
| } |
| |
| impl<'i, S: Parse<'i>> Parse<'i> for WebKitGradientPointComponent<S> { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| if input.try_parse(|i| i.expect_ident_matching("center")).is_ok() { |
| return Ok(WebKitGradientPointComponent::Center); |
| } |
| |
| if let Ok(lp) = input.try_parse(NumberOrPercentage::parse) { |
| return Ok(WebKitGradientPointComponent::Number(lp)); |
| } |
| |
| let keyword = S::parse(input)?; |
| Ok(WebKitGradientPointComponent::Side(keyword)) |
| } |
| } |
| |
| impl<S: ToCss + Clone + Into<LengthPercentage>> ToCss for WebKitGradientPointComponent<S> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| use WebKitGradientPointComponent::*; |
| match &self { |
| Center => { |
| if dest.minify { |
| dest.write_str("50%") |
| } else { |
| dest.write_str("center") |
| } |
| } |
| Number(lp) => { |
| if matches!(lp, NumberOrPercentage::Percentage(Percentage(p)) if *p == 0.0) { |
| dest.write_char('0') |
| } else { |
| lp.to_css(dest) |
| } |
| } |
| Side(s) => { |
| if dest.minify { |
| let lp: LengthPercentage = s.clone().into(); |
| lp.to_css(dest)?; |
| } else { |
| s.to_css(dest)?; |
| } |
| Ok(()) |
| } |
| } |
| } |
| } |
| |
| impl<S: Clone> WebKitGradientPointComponent<S> { |
| /// Attempts to convert a standard position to a webkit gradient point. |
| fn from_position(pos: &PositionComponent<S>) -> Result<WebKitGradientPointComponent<S>, ()> { |
| match pos { |
| PositionComponent::Center => Ok(WebKitGradientPointComponent::Center), |
| PositionComponent::Length(len) => { |
| Ok(WebKitGradientPointComponent::Number(match len { |
| LengthPercentage::Percentage(p) => NumberOrPercentage::Percentage(p.clone()), |
| LengthPercentage::Dimension(d) => { |
| // Webkit gradient points can only be specified in pixels. |
| if let Some(px) = d.to_px() { |
| NumberOrPercentage::Number(px) |
| } else { |
| return Err(()); |
| } |
| } |
| _ => return Err(()), |
| })) |
| } |
| PositionComponent::Side { side, offset } => { |
| if offset.is_some() { |
| return Err(()); |
| } |
| Ok(WebKitGradientPointComponent::Side(side.clone())) |
| } |
| } |
| } |
| } |
| |
| impl WebKitGradient { |
| /// Attempts to convert a standard gradient to a legacy -webkit-gradient() |
| pub fn from_standard(gradient: &Gradient) -> Result<WebKitGradient, ()> { |
| match gradient { |
| Gradient::Linear(linear) => { |
| // Convert from line direction to a from and to point, if possible. |
| let (from, to) = match &linear.direction { |
| LineDirection::Horizontal(horizontal) => match horizontal { |
| HorizontalPositionKeyword::Left => ((1.0, 0.0), (0.0, 0.0)), |
| HorizontalPositionKeyword::Right => ((0.0, 0.0), (1.0, 0.0)), |
| }, |
| LineDirection::Vertical(vertical) => match vertical { |
| VerticalPositionKeyword::Top => ((0.0, 1.0), (0.0, 0.0)), |
| VerticalPositionKeyword::Bottom => ((0.0, 0.0), (0.0, 1.0)), |
| }, |
| LineDirection::Corner { horizontal, vertical } => match (horizontal, vertical) { |
| (HorizontalPositionKeyword::Left, VerticalPositionKeyword::Top) => ((1.0, 1.0), (0.0, 0.0)), |
| (HorizontalPositionKeyword::Left, VerticalPositionKeyword::Bottom) => ((1.0, 0.0), (0.0, 1.0)), |
| (HorizontalPositionKeyword::Right, VerticalPositionKeyword::Top) => ((0.0, 1.0), (1.0, 0.0)), |
| (HorizontalPositionKeyword::Right, VerticalPositionKeyword::Bottom) => ((0.0, 0.0), (1.0, 1.0)), |
| }, |
| LineDirection::Angle(angle) => { |
| let degrees = angle.to_degrees(); |
| if degrees == 0.0 { |
| ((0.0, 1.0), (0.0, 0.0)) |
| } else if degrees == 90.0 { |
| ((0.0, 0.0), (1.0, 0.0)) |
| } else if degrees == 180.0 { |
| ((0.0, 0.0), (0.0, 1.0)) |
| } else if degrees == 270.0 { |
| ((1.0, 0.0), (0.0, 0.0)) |
| } else { |
| return Err(()); |
| } |
| } |
| }; |
| |
| Ok(WebKitGradient::Linear { |
| from: WebKitGradientPoint { |
| x: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(from.0))), |
| y: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(from.1))), |
| }, |
| to: WebKitGradientPoint { |
| x: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(to.0))), |
| y: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(to.1))), |
| }, |
| stops: convert_stops_to_webkit(&linear.items)?, |
| }) |
| } |
| Gradient::Radial(radial) => { |
| // Webkit radial gradients are always circles, not ellipses, and must be specified in pixels. |
| let radius = match &radial.shape { |
| EndingShape::Circle(Circle::Radius(radius)) => { |
| if let Some(r) = radius.to_px() { |
| r |
| } else { |
| return Err(()); |
| } |
| } |
| _ => return Err(()), |
| }; |
| |
| let x = WebKitGradientPointComponent::from_position(&radial.position.x)?; |
| let y = WebKitGradientPointComponent::from_position(&radial.position.y)?; |
| let point = WebKitGradientPoint { x, y }; |
| Ok(WebKitGradient::Radial { |
| from: point.clone(), |
| r0: 0.0, |
| to: point, |
| r1: radius, |
| stops: convert_stops_to_webkit(&radial.items)?, |
| }) |
| } |
| _ => Err(()), |
| } |
| } |
| } |
| |
| fn convert_stops_to_webkit(items: &Vec<GradientItem<LengthPercentage>>) -> Result<Vec<WebKitColorStop>, ()> { |
| let mut stops = Vec::with_capacity(items.len()); |
| for (i, item) in items.iter().enumerate() { |
| match item { |
| GradientItem::ColorStop(stop) => { |
| // webkit stops must always be percentage based, not length based. |
| let position = if let Some(pos) = &stop.position { |
| if let LengthPercentage::Percentage(position) = pos { |
| position.0 |
| } else { |
| return Err(()); |
| } |
| } else if i == 0 { |
| 0.0 |
| } else if i == items.len() - 1 { |
| 1.0 |
| } else { |
| return Err(()); |
| }; |
| |
| stops.push(WebKitColorStop { |
| color: stop.color.clone(), |
| position, |
| }) |
| } |
| _ => return Err(()), |
| } |
| } |
| |
| Ok(stops) |
| } |