Skip to main content

lightningcss/values/
gradient.rs

1//! CSS gradient values.
2
3use super::angle::{Angle, AnglePercentage};
4use super::color::{ColorFallbackKind, CssColor};
5use super::length::{Length, LengthPercentage};
6use super::number::CSSNumber;
7use super::percentage::{DimensionPercentage, NumberOrPercentage, Percentage};
8use super::position::{HorizontalPositionKeyword, VerticalPositionKeyword};
9use super::position::{Position, PositionComponent};
10use crate::compat;
11use crate::error::{ParserError, PrinterError};
12use crate::macros::enum_property;
13use crate::prefixes::Feature;
14use crate::printer::Printer;
15use crate::targets::{should_compile, Browsers, Targets};
16use crate::traits::{IsCompatible, Parse, ToCss, TrySign, Zero};
17use crate::vendor_prefix::VendorPrefix;
18#[cfg(feature = "visitor")]
19use crate::visitor::Visit;
20use cssparser::*;
21use std::f32::consts::PI;
22
23#[cfg(feature = "serde")]
24use crate::serialization::ValueWrapper;
25
26/// A CSS [`<gradient>`](https://www.w3.org/TR/css-images-3/#gradients) value.
27#[derive(Debug, Clone, PartialEq)]
28#[cfg_attr(feature = "visitor", derive(Visit))]
29#[cfg_attr(
30  feature = "serde",
31  derive(serde::Serialize, serde::Deserialize),
32  serde(tag = "type", rename_all = "kebab-case")
33)]
34#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
35#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
36pub enum Gradient {
37  /// A `linear-gradient()`, and its vendor prefix.
38  Linear(LinearGradient),
39  /// A `repeating-linear-gradient()`, and its vendor prefix.
40  RepeatingLinear(LinearGradient),
41  /// A `radial-gradient()`, and its vendor prefix.
42  Radial(RadialGradient),
43  /// A `repeating-radial-gradient`, and its vendor prefix.
44  RepeatingRadial(RadialGradient),
45  /// A `conic-gradient()`.
46  Conic(ConicGradient),
47  /// A `repeating-conic-gradient()`.
48  RepeatingConic(ConicGradient),
49  /// A legacy `-webkit-gradient()`.
50  #[cfg_attr(feature = "serde", serde(rename = "webkit-gradient"))]
51  WebKitGradient(WebKitGradient),
52}
53
54impl Gradient {
55  /// Returns the vendor prefix of the gradient.
56  pub fn get_vendor_prefix(&self) -> VendorPrefix {
57    match self {
58      Gradient::Linear(LinearGradient { vendor_prefix, .. })
59      | Gradient::RepeatingLinear(LinearGradient { vendor_prefix, .. })
60      | Gradient::Radial(RadialGradient { vendor_prefix, .. })
61      | Gradient::RepeatingRadial(RadialGradient { vendor_prefix, .. }) => *vendor_prefix,
62      Gradient::WebKitGradient(_) => VendorPrefix::WebKit,
63      _ => VendorPrefix::None,
64    }
65  }
66
67  /// Returns the vendor prefixes needed for the given browser targets.
68  pub fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
69    macro_rules! get_prefixes {
70      ($feature: ident, $prefix: expr) => {
71        targets.prefixes($prefix, Feature::$feature)
72      };
73    }
74
75    match self {
76      Gradient::Linear(linear) => get_prefixes!(LinearGradient, linear.vendor_prefix),
77      Gradient::RepeatingLinear(linear) => get_prefixes!(RepeatingLinearGradient, linear.vendor_prefix),
78      Gradient::Radial(radial) => get_prefixes!(RadialGradient, radial.vendor_prefix),
79      Gradient::RepeatingRadial(radial) => get_prefixes!(RepeatingRadialGradient, radial.vendor_prefix),
80      _ => VendorPrefix::None,
81    }
82  }
83
84  /// Returns a copy of the gradient with the given vendor prefix.
85  pub fn get_prefixed(&self, prefix: VendorPrefix) -> Gradient {
86    match self {
87      Gradient::Linear(linear) => {
88        let mut new_linear = linear.clone();
89        let needs_legacy_direction = linear.vendor_prefix == VendorPrefix::None && prefix != VendorPrefix::None;
90        if needs_legacy_direction {
91          new_linear.direction = convert_to_legacy_direction(&new_linear.direction);
92        }
93        new_linear.vendor_prefix = prefix;
94        Gradient::Linear(new_linear)
95      }
96      Gradient::RepeatingLinear(linear) => {
97        let mut new_linear = linear.clone();
98        let needs_legacy_direction = linear.vendor_prefix == VendorPrefix::None && prefix != VendorPrefix::None;
99        if needs_legacy_direction {
100          new_linear.direction = convert_to_legacy_direction(&new_linear.direction);
101        }
102        new_linear.vendor_prefix = prefix;
103        Gradient::RepeatingLinear(new_linear)
104      }
105      Gradient::Radial(radial) => Gradient::Radial(RadialGradient {
106        vendor_prefix: prefix,
107        ..radial.clone()
108      }),
109      Gradient::RepeatingRadial(radial) => Gradient::RepeatingRadial(RadialGradient {
110        vendor_prefix: prefix,
111        ..radial.clone()
112      }),
113      _ => self.clone(),
114    }
115  }
116
117  /// Attempts to convert the gradient to the legacy `-webkit-gradient()` syntax.
118  ///
119  /// Returns an error in case the conversion is not possible.
120  pub fn get_legacy_webkit(&self) -> Result<Gradient, ()> {
121    Ok(Gradient::WebKitGradient(WebKitGradient::from_standard(self)?))
122  }
123
124  /// Returns the color fallback types needed for the given browser targets.
125  pub fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
126    match self {
127      Gradient::Linear(LinearGradient { items, .. })
128      | Gradient::Radial(RadialGradient { items, .. })
129      | Gradient::RepeatingLinear(LinearGradient { items, .. })
130      | Gradient::RepeatingRadial(RadialGradient { items, .. }) => {
131        let mut fallbacks = ColorFallbackKind::empty();
132        for item in items {
133          fallbacks |= item.get_necessary_fallbacks(targets)
134        }
135        fallbacks
136      }
137      Gradient::Conic(ConicGradient { items, .. }) | Gradient::RepeatingConic(ConicGradient { items, .. }) => {
138        let mut fallbacks = ColorFallbackKind::empty();
139        for item in items {
140          fallbacks |= item.get_necessary_fallbacks(targets)
141        }
142        fallbacks
143      }
144      Gradient::WebKitGradient(..) => ColorFallbackKind::empty(),
145    }
146  }
147
148  /// Returns a fallback gradient for the given color fallback type.
149  pub fn get_fallback(&self, kind: ColorFallbackKind) -> Gradient {
150    match self {
151      Gradient::Linear(g) => Gradient::Linear(g.get_fallback(kind)),
152      Gradient::RepeatingLinear(g) => Gradient::RepeatingLinear(g.get_fallback(kind)),
153      Gradient::Radial(g) => Gradient::Radial(g.get_fallback(kind)),
154      Gradient::RepeatingRadial(g) => Gradient::RepeatingRadial(g.get_fallback(kind)),
155      Gradient::Conic(g) => Gradient::Conic(g.get_fallback(kind)),
156      Gradient::RepeatingConic(g) => Gradient::RepeatingConic(g.get_fallback(kind)),
157      Gradient::WebKitGradient(g) => Gradient::WebKitGradient(g.get_fallback(kind)),
158    }
159  }
160}
161
162impl<'i> Parse<'i> for Gradient {
163  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
164    let location = input.current_source_location();
165    let func = input.expect_function()?.clone();
166    input.parse_nested_block(|input| {
167      match_ignore_ascii_case! { &func,
168        "linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::None)?)),
169        "repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::None)?)),
170        "radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::None)?)),
171        "repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::None)?)),
172        "conic-gradient" => Ok(Gradient::Conic(ConicGradient::parse(input)?)),
173        "repeating-conic-gradient" => Ok(Gradient::RepeatingConic(ConicGradient::parse(input)?)),
174        "-webkit-linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::WebKit)?)),
175        "-webkit-repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::WebKit)?)),
176        "-webkit-radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::WebKit)?)),
177        "-webkit-repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::WebKit)?)),
178        "-moz-linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::Moz)?)),
179        "-moz-repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::Moz)?)),
180        "-moz-radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::Moz)?)),
181        "-moz-repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::Moz)?)),
182        "-o-linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::O)?)),
183        "-o-repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::O)?)),
184        "-o-radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::O)?)),
185        "-o-repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::O)?)),
186        "-webkit-gradient" => Ok(Gradient::WebKitGradient(WebKitGradient::parse(input)?)),
187        _ => Err(location.new_unexpected_token_error(cssparser::Token::Ident(func.clone())))
188      }
189    })
190  }
191}
192
193impl ToCss for Gradient {
194  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
195  where
196    W: std::fmt::Write,
197  {
198    let (f, prefix) = match self {
199      Gradient::Linear(g) => ("linear-gradient(", Some(g.vendor_prefix)),
200      Gradient::RepeatingLinear(g) => ("repeating-linear-gradient(", Some(g.vendor_prefix)),
201      Gradient::Radial(g) => ("radial-gradient(", Some(g.vendor_prefix)),
202      Gradient::RepeatingRadial(g) => ("repeating-radial-gradient(", Some(g.vendor_prefix)),
203      Gradient::Conic(_) => ("conic-gradient(", None),
204      Gradient::RepeatingConic(_) => ("repeating-conic-gradient(", None),
205      Gradient::WebKitGradient(_) => ("-webkit-gradient(", None),
206    };
207
208    if let Some(prefix) = prefix {
209      prefix.to_css(dest)?;
210    }
211
212    dest.write_str(f)?;
213
214    match self {
215      Gradient::Linear(linear) | Gradient::RepeatingLinear(linear) => {
216        linear.to_css(dest, linear.vendor_prefix != VendorPrefix::None)?
217      }
218      Gradient::Radial(radial) | Gradient::RepeatingRadial(radial) => radial.to_css(dest)?,
219      Gradient::Conic(conic) | Gradient::RepeatingConic(conic) => conic.to_css(dest)?,
220      Gradient::WebKitGradient(g) => g.to_css(dest)?,
221    }
222
223    dest.write_char(')')
224  }
225}
226
227/// A CSS [`linear-gradient()`](https://www.w3.org/TR/css-images-3/#linear-gradients) or `repeating-linear-gradient()`.
228#[derive(Debug, Clone, PartialEq)]
229#[cfg_attr(feature = "visitor", derive(Visit))]
230#[cfg_attr(
231  feature = "serde",
232  derive(serde::Serialize, serde::Deserialize),
233  serde(rename_all = "camelCase")
234)]
235#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
236#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
237pub struct LinearGradient {
238  /// The vendor prefixes for the gradient.
239  pub vendor_prefix: VendorPrefix,
240  /// The direction of the gradient.
241  pub direction: LineDirection,
242  /// The color stops and transition hints for the gradient.
243  pub items: Vec<GradientItem<LengthPercentage>>,
244}
245
246impl LinearGradient {
247  fn parse<'i, 't>(
248    input: &mut Parser<'i, 't>,
249    vendor_prefix: VendorPrefix,
250  ) -> Result<LinearGradient, ParseError<'i, ParserError<'i>>> {
251    let direction = if let Ok(direction) =
252      input.try_parse(|input| LineDirection::parse(input, vendor_prefix != VendorPrefix::None))
253    {
254      input.expect_comma()?;
255      direction
256    } else {
257      LineDirection::Vertical(VerticalPositionKeyword::Bottom)
258    };
259    let items = parse_items(input)?;
260    Ok(LinearGradient {
261      direction,
262      items,
263      vendor_prefix,
264    })
265  }
266
267  fn to_css<W>(&self, dest: &mut Printer<W>, is_prefixed: bool) -> Result<(), PrinterError>
268  where
269    W: std::fmt::Write,
270  {
271    let angle = match &self.direction {
272      LineDirection::Vertical(VerticalPositionKeyword::Bottom) => 180.0,
273      LineDirection::Vertical(VerticalPositionKeyword::Top) => 0.0,
274      LineDirection::Angle(angle) => angle.to_degrees(),
275      _ => -1.0,
276    };
277
278    // We can omit `to bottom` or `180deg` because it is the default.
279    if angle == 180.0 {
280      serialize_items(&self.items, dest)
281
282    // If we have `to top` or `0deg`, and all of the positions and hints are percentages,
283    // we can flip the gradient the other direction and omit the direction.
284    } else if angle == 0.0
285      && dest.minify
286      && self.items.iter().all(|item| {
287        matches!(
288          item,
289          GradientItem::Hint(LengthPercentage::Percentage(_))
290            | GradientItem::ColorStop(ColorStop {
291              position: None | Some(LengthPercentage::Percentage(_)),
292              ..
293            })
294        )
295      })
296    {
297      let items: Vec<GradientItem<LengthPercentage>> = self
298        .items
299        .iter()
300        .rev()
301        .map(|item| {
302          // Flip percentages.
303          match item {
304            GradientItem::Hint(LengthPercentage::Percentage(p)) => {
305              GradientItem::Hint(LengthPercentage::Percentage(Percentage(1.0 - p.0)))
306            }
307            GradientItem::ColorStop(ColorStop { color, position }) => GradientItem::ColorStop(ColorStop {
308              color: color.clone(),
309              position: position.clone().map(|p| match p {
310                LengthPercentage::Percentage(p) => LengthPercentage::Percentage(Percentage(1.0 - p.0)),
311                _ => unreachable!(),
312              }),
313            }),
314            _ => unreachable!(),
315          }
316        })
317        .collect();
318      serialize_items(&items, dest)
319    } else {
320      if self.direction != LineDirection::Vertical(VerticalPositionKeyword::Bottom)
321        && self.direction != LineDirection::Angle(Angle::Deg(180.0))
322      {
323        self.direction.to_css(dest, is_prefixed)?;
324        dest.delim(',', false)?;
325      }
326
327      serialize_items(&self.items, dest)
328    }
329  }
330
331  fn get_fallback(&self, kind: ColorFallbackKind) -> LinearGradient {
332    LinearGradient {
333      direction: self.direction.clone(),
334      items: self.items.iter().map(|item| item.get_fallback(kind)).collect(),
335      vendor_prefix: self.vendor_prefix,
336    }
337  }
338}
339
340impl IsCompatible for LinearGradient {
341  fn is_compatible(&self, browsers: Browsers) -> bool {
342    self.items.iter().all(|item| item.is_compatible(browsers))
343  }
344}
345
346/// A CSS [`radial-gradient()`](https://www.w3.org/TR/css-images-3/#radial-gradients) or `repeating-radial-gradient()`.
347#[derive(Debug, Clone, PartialEq)]
348#[cfg_attr(feature = "visitor", derive(Visit))]
349#[cfg_attr(
350  feature = "serde",
351  derive(serde::Serialize, serde::Deserialize),
352  serde(rename_all = "camelCase")
353)]
354#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
355#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
356pub struct RadialGradient {
357  /// The vendor prefixes for the gradient.
358  pub vendor_prefix: VendorPrefix,
359  /// The shape of the gradient.
360  pub shape: EndingShape,
361  /// The position of the gradient.
362  pub position: Position,
363  /// The color stops and transition hints for the gradient.
364  pub items: Vec<GradientItem<LengthPercentage>>,
365}
366
367impl<'i> RadialGradient {
368  fn parse<'t>(
369    input: &mut Parser<'i, 't>,
370    vendor_prefix: VendorPrefix,
371  ) -> Result<RadialGradient, ParseError<'i, ParserError<'i>>> {
372    let shape = input.try_parse(EndingShape::parse).ok();
373    let position = input
374      .try_parse(|input| {
375        input.expect_ident_matching("at")?;
376        Position::parse(input)
377      })
378      .ok();
379
380    if shape.is_some() || position.is_some() {
381      input.expect_comma()?;
382    }
383
384    let items = parse_items(input)?;
385    Ok(RadialGradient {
386      shape: shape.unwrap_or_default(),
387      position: position.unwrap_or(Position::center()),
388      items,
389      vendor_prefix,
390    })
391  }
392}
393
394impl ToCss for RadialGradient {
395  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
396  where
397    W: std::fmt::Write,
398  {
399    if self.shape != EndingShape::default() {
400      self.shape.to_css(dest)?;
401      if self.position.is_center() {
402        dest.delim(',', false)?;
403      } else {
404        dest.write_char(' ')?;
405      }
406    }
407
408    if !self.position.is_center() {
409      dest.write_str("at ")?;
410      self.position.to_css(dest)?;
411      dest.delim(',', false)?;
412    }
413
414    serialize_items(&self.items, dest)
415  }
416}
417
418impl RadialGradient {
419  fn get_fallback(&self, kind: ColorFallbackKind) -> RadialGradient {
420    RadialGradient {
421      shape: self.shape.clone(),
422      position: self.position.clone(),
423      items: self.items.iter().map(|item| item.get_fallback(kind)).collect(),
424      vendor_prefix: self.vendor_prefix,
425    }
426  }
427}
428
429impl IsCompatible for RadialGradient {
430  fn is_compatible(&self, browsers: Browsers) -> bool {
431    self.items.iter().all(|item| item.is_compatible(browsers))
432  }
433}
434
435/// The direction of a CSS `linear-gradient()`.
436///
437/// See [LinearGradient](LinearGradient).
438#[derive(Debug, Clone, PartialEq)]
439#[cfg_attr(feature = "visitor", derive(Visit))]
440#[cfg_attr(
441  feature = "serde",
442  derive(serde::Serialize, serde::Deserialize),
443  serde(tag = "type", rename_all = "kebab-case")
444)]
445#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
446pub enum LineDirection {
447  /// An angle.
448  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Angle>"))]
449  Angle(Angle),
450  /// A horizontal position keyword, e.g. `left` or `right.
451  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<HorizontalPositionKeyword>"))]
452  Horizontal(HorizontalPositionKeyword),
453  /// A vertical posision keyword, e.g. `top` or `bottom`.
454  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<VerticalPositionKeyword>"))]
455  Vertical(VerticalPositionKeyword),
456  /// A corner, e.g. `bottom left` or `top right`.
457  Corner {
458    /// A horizontal position keyword, e.g. `left` or `right.
459    horizontal: HorizontalPositionKeyword,
460    /// A vertical posision keyword, e.g. `top` or `bottom`.
461    vertical: VerticalPositionKeyword,
462  },
463}
464
465impl LineDirection {
466  fn parse<'i, 't>(
467    input: &mut Parser<'i, 't>,
468    is_prefixed: bool,
469  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
470    // Spec allows unitless zero angles for gradients.
471    // https://w3c.github.io/csswg-drafts/css-images-3/#linear-gradient-syntax
472    if let Ok(angle) = input.try_parse(Angle::parse_with_unitless_zero) {
473      return Ok(LineDirection::Angle(angle));
474    }
475
476    if !is_prefixed {
477      input.expect_ident_matching("to")?;
478    }
479
480    if let Ok(x) = input.try_parse(HorizontalPositionKeyword::parse) {
481      if let Ok(y) = input.try_parse(VerticalPositionKeyword::parse) {
482        return Ok(LineDirection::Corner {
483          horizontal: x,
484          vertical: y,
485        });
486      }
487      return Ok(LineDirection::Horizontal(x));
488    }
489
490    let y = VerticalPositionKeyword::parse(input)?;
491    if let Ok(x) = input.try_parse(HorizontalPositionKeyword::parse) {
492      return Ok(LineDirection::Corner {
493        horizontal: x,
494        vertical: y,
495      });
496    }
497    Ok(LineDirection::Vertical(y))
498  }
499
500  fn to_css<W>(&self, dest: &mut Printer<W>, is_prefixed: bool) -> Result<(), PrinterError>
501  where
502    W: std::fmt::Write,
503  {
504    match self {
505      LineDirection::Angle(angle) => angle.to_css(dest),
506      LineDirection::Horizontal(k) => {
507        if dest.minify {
508          match k {
509            HorizontalPositionKeyword::Left => dest.write_str("270deg"),
510            HorizontalPositionKeyword::Right => dest.write_str("90deg"),
511          }
512        } else {
513          if !is_prefixed {
514            dest.write_str("to ")?;
515          }
516          k.to_css(dest)
517        }
518      }
519      LineDirection::Vertical(k) => {
520        if dest.minify {
521          match k {
522            VerticalPositionKeyword::Top => dest.write_str("0deg"),
523            VerticalPositionKeyword::Bottom => dest.write_str("180deg"),
524          }
525        } else {
526          if !is_prefixed {
527            dest.write_str("to ")?;
528          }
529          k.to_css(dest)
530        }
531      }
532      LineDirection::Corner { horizontal, vertical } => {
533        if !is_prefixed {
534          dest.write_str("to ")?;
535        }
536        vertical.to_css(dest)?;
537        dest.write_char(' ')?;
538        horizontal.to_css(dest)
539      }
540    }
541  }
542}
543
544/// Converts a standard gradient direction to its legacy vendor-prefixed form.
545///
546/// Inverts keyword-based directions (e.g., `to bottom` → `top`) for compatibility
547/// with legacy prefixed syntaxes.
548///
549/// See: https://github.com/parcel-bundler/lightningcss/issues/918
550fn convert_to_legacy_direction(direction: &LineDirection) -> LineDirection {
551  match direction {
552    LineDirection::Horizontal(HorizontalPositionKeyword::Left) => {
553      LineDirection::Horizontal(HorizontalPositionKeyword::Right)
554    }
555    LineDirection::Horizontal(HorizontalPositionKeyword::Right) => {
556      LineDirection::Horizontal(HorizontalPositionKeyword::Left)
557    }
558    LineDirection::Vertical(VerticalPositionKeyword::Top) => {
559      LineDirection::Vertical(VerticalPositionKeyword::Bottom)
560    }
561    LineDirection::Vertical(VerticalPositionKeyword::Bottom) => {
562      LineDirection::Vertical(VerticalPositionKeyword::Top)
563    }
564    LineDirection::Corner { horizontal, vertical } => LineDirection::Corner {
565      horizontal: match horizontal {
566        HorizontalPositionKeyword::Left => HorizontalPositionKeyword::Right,
567        HorizontalPositionKeyword::Right => HorizontalPositionKeyword::Left,
568      },
569      vertical: match vertical {
570        VerticalPositionKeyword::Top => VerticalPositionKeyword::Bottom,
571        VerticalPositionKeyword::Bottom => VerticalPositionKeyword::Top,
572      },
573    },
574    LineDirection::Angle(angle) => {
575      let angle = angle.clone();
576      let deg = match angle {
577        Angle::Deg(n) => convert_to_legacy_degree(n),
578        Angle::Rad(n) => {
579          let n = n / (2.0 * PI) * 360.0;
580          convert_to_legacy_degree(n)
581        }
582        Angle::Grad(n) => {
583          let n = n / 400.0 * 360.0;
584          convert_to_legacy_degree(n)
585        }
586        Angle::Turn(n) => {
587          let n = n * 360.0;
588          convert_to_legacy_degree(n)
589        }
590      };
591      LineDirection::Angle(Angle::Deg(deg))
592    }
593  }
594}
595
596fn convert_to_legacy_degree(degree: f32) -> f32 {
597  // Add 90 degrees
598  let n = (450.0 - degree).abs() % 360.0;
599  // Round the number to 3 decimal places
600  (n * 1000.0).round() / 1000.0
601}
602
603/// A `radial-gradient()` [ending shape](https://www.w3.org/TR/css-images-3/#valdef-radial-gradient-ending-shape).
604///
605/// See [RadialGradient](RadialGradient).
606#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
607#[cfg_attr(feature = "visitor", derive(Visit))]
608#[cfg_attr(
609  feature = "serde",
610  derive(serde::Serialize, serde::Deserialize),
611  serde(tag = "type", content = "value", rename_all = "kebab-case")
612)]
613#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
614pub enum EndingShape {
615  // Note: Ellipse::parse MUST run before Circle::parse for this to be correct.
616  /// An ellipse.
617  Ellipse(Ellipse),
618  /// A circle.
619  Circle(Circle),
620}
621
622impl Default for EndingShape {
623  fn default() -> EndingShape {
624    EndingShape::Ellipse(Ellipse::Extent(ShapeExtent::FarthestCorner))
625  }
626}
627
628/// A circle ending shape for a `radial-gradient()`.
629///
630/// See [RadialGradient](RadialGradient).
631#[derive(Debug, Clone, PartialEq)]
632#[cfg_attr(feature = "visitor", derive(Visit))]
633#[cfg_attr(
634  feature = "serde",
635  derive(serde::Serialize, serde::Deserialize),
636  serde(tag = "type", content = "value", rename_all = "kebab-case")
637)]
638#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
639pub enum Circle {
640  /// A circle with a specified radius.
641  Radius(Length),
642  /// A shape extent keyword.
643  Extent(ShapeExtent),
644}
645
646impl<'i> Parse<'i> for Circle {
647  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
648    if let Ok(extent) = input.try_parse(ShapeExtent::parse) {
649      // The `circle` keyword is required. If it's not there, then it's an ellipse.
650      input.expect_ident_matching("circle")?;
651      return Ok(Circle::Extent(extent));
652    }
653
654    if let Ok(length) = input.try_parse(Length::parse) {
655      // The `circle` keyword is optional if there is only a single length.
656      // We are assuming here that Ellipse::parse ran first.
657      let _ = input.try_parse(|input| input.expect_ident_matching("circle"));
658      return Ok(Circle::Radius(length));
659    }
660
661    if input.try_parse(|input| input.expect_ident_matching("circle")).is_ok() {
662      if let Ok(extent) = input.try_parse(ShapeExtent::parse) {
663        return Ok(Circle::Extent(extent));
664      }
665
666      if let Ok(length) = input.try_parse(Length::parse) {
667        return Ok(Circle::Radius(length));
668      }
669
670      // If only the `circle` keyword was given, default to `farthest-corner`.
671      return Ok(Circle::Extent(ShapeExtent::FarthestCorner));
672    }
673
674    return Err(input.new_error_for_next_token());
675  }
676}
677
678impl ToCss for Circle {
679  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
680  where
681    W: std::fmt::Write,
682  {
683    match self {
684      Circle::Radius(r) => r.to_css(dest),
685      Circle::Extent(extent) => {
686        dest.write_str("circle")?;
687        if *extent != ShapeExtent::FarthestCorner {
688          dest.write_char(' ')?;
689          extent.to_css(dest)?;
690        }
691        Ok(())
692      }
693    }
694  }
695}
696
697/// An ellipse ending shape for a `radial-gradient()`.
698///
699/// See [RadialGradient](RadialGradient).
700#[derive(Debug, Clone, PartialEq)]
701#[cfg_attr(feature = "visitor", derive(Visit))]
702#[cfg_attr(
703  feature = "serde",
704  derive(serde::Serialize, serde::Deserialize),
705  serde(tag = "type", rename_all = "kebab-case")
706)]
707#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
708pub enum Ellipse {
709  /// An ellipse with a specified horizontal and vertical radius.
710  Size {
711    /// The x-radius of the ellipse.
712    x: LengthPercentage,
713    /// The y-radius of the ellipse.
714    y: LengthPercentage,
715  },
716  /// A shape extent keyword.
717  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<ShapeExtent>"))]
718  Extent(ShapeExtent),
719}
720
721impl<'i> Parse<'i> for Ellipse {
722  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
723    if let Ok(extent) = input.try_parse(ShapeExtent::parse) {
724      // The `ellipse` keyword is optional, but only if the `circle` keyword is not present.
725      // If it is, then we'll re-parse as a circle.
726      if input.try_parse(|input| input.expect_ident_matching("circle")).is_ok() {
727        return Err(input.new_error_for_next_token());
728      }
729      let _ = input.try_parse(|input| input.expect_ident_matching("ellipse"));
730      return Ok(Ellipse::Extent(extent));
731    }
732
733    if let Ok(x) = input.try_parse(LengthPercentage::parse) {
734      let y = LengthPercentage::parse(input)?;
735      // The `ellipse` keyword is optional if there are two lengths.
736      let _ = input.try_parse(|input| input.expect_ident_matching("ellipse"));
737      return Ok(Ellipse::Size { x, y });
738    }
739
740    if input.try_parse(|input| input.expect_ident_matching("ellipse")).is_ok() {
741      if let Ok(extent) = input.try_parse(ShapeExtent::parse) {
742        return Ok(Ellipse::Extent(extent));
743      }
744
745      if let Ok(x) = input.try_parse(LengthPercentage::parse) {
746        let y = LengthPercentage::parse(input)?;
747        return Ok(Ellipse::Size { x, y });
748      }
749
750      // Assume `farthest-corner` if only the `ellipse` keyword is present.
751      return Ok(Ellipse::Extent(ShapeExtent::FarthestCorner));
752    }
753
754    return Err(input.new_error_for_next_token());
755  }
756}
757
758impl ToCss for Ellipse {
759  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
760  where
761    W: std::fmt::Write,
762  {
763    // The `ellipse` keyword is optional, so we don't emit it.
764    match self {
765      Ellipse::Size { x, y } => {
766        x.to_css(dest)?;
767        dest.write_char(' ')?;
768        y.to_css(dest)
769      }
770      Ellipse::Extent(extent) => extent.to_css(dest),
771    }
772  }
773}
774
775enum_property! {
776  /// A shape extent for a `radial-gradient()`.
777  ///
778  /// See [RadialGradient](RadialGradient).
779  pub enum ShapeExtent {
780    /// The closest side of the box to the gradient's center.
781    ClosestSide,
782    /// The farthest side of the box from the gradient's center.
783    FarthestSide,
784    /// The closest cornder of the box to the gradient's center.
785    ClosestCorner,
786    /// The farthest corner of the box from the gradient's center.
787    FarthestCorner,
788  }
789}
790
791/// A CSS [`conic-gradient()`](https://www.w3.org/TR/css-images-4/#conic-gradients) or `repeating-conic-gradient()`.
792#[derive(Debug, Clone, PartialEq)]
793#[cfg_attr(feature = "visitor", derive(Visit))]
794#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
795#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
796#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
797pub struct ConicGradient {
798  /// The angle of the gradient.
799  pub angle: Angle,
800  /// The position of the gradient.
801  pub position: Position,
802  /// The color stops and transition hints for the gradient.
803  pub items: Vec<GradientItem<AnglePercentage>>,
804}
805
806impl ConicGradient {
807  fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
808    let angle = input.try_parse(|input| {
809      input.expect_ident_matching("from")?;
810      // Spec allows unitless zero angles for gradients.
811      // https://w3c.github.io/csswg-drafts/css-images-4/#valdef-conic-gradient-angle
812      Angle::parse_with_unitless_zero(input)
813    });
814
815    let position = input.try_parse(|input| {
816      input.expect_ident_matching("at")?;
817      Position::parse(input)
818    });
819
820    if angle.is_ok() || position.is_ok() {
821      input.expect_comma()?;
822    }
823
824    let items = parse_items(input)?;
825    Ok(ConicGradient {
826      angle: angle.unwrap_or(Angle::Deg(0.0)),
827      position: position.unwrap_or(Position::center()),
828      items,
829    })
830  }
831}
832
833impl ToCss for ConicGradient {
834  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
835  where
836    W: std::fmt::Write,
837  {
838    if !self.angle.is_zero() {
839      dest.write_str("from ")?;
840      self.angle.to_css(dest)?;
841
842      if self.position.is_center() {
843        dest.delim(',', false)?;
844      } else {
845        dest.write_char(' ')?;
846      }
847    }
848
849    if !self.position.is_center() {
850      dest.write_str("at ")?;
851      self.position.to_css(dest)?;
852      dest.delim(',', false)?;
853    }
854
855    serialize_items(&self.items, dest)
856  }
857}
858
859impl ConicGradient {
860  fn get_fallback(&self, kind: ColorFallbackKind) -> ConicGradient {
861    ConicGradient {
862      angle: self.angle.clone(),
863      position: self.position.clone(),
864      items: self.items.iter().map(|item| item.get_fallback(kind)).collect(),
865    }
866  }
867}
868
869impl IsCompatible for ConicGradient {
870  fn is_compatible(&self, browsers: Browsers) -> bool {
871    self.items.iter().all(|item| item.is_compatible(browsers))
872  }
873}
874
875/// A [`<color-stop>`](https://www.w3.org/TR/css-images-4/#color-stop-syntax) within a gradient.
876///
877/// This type is generic, and may be either a [LengthPercentage](super::length::LengthPercentage)
878/// or [Angle](super::angle::Angle) depending on what type of gradient it is within.
879#[derive(Debug, Clone, PartialEq)]
880#[cfg_attr(feature = "visitor", derive(Visit))]
881#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
882#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
883pub struct ColorStop<D> {
884  /// The color of the color stop.
885  pub color: CssColor,
886  /// The position of the color stop.
887  pub position: Option<D>,
888}
889
890impl<'i, D: Parse<'i>> Parse<'i> for ColorStop<D> {
891  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
892    let color = CssColor::parse(input)?;
893    let position = input.try_parse(D::parse).ok();
894    Ok(ColorStop { color, position })
895  }
896}
897
898impl<D: ToCss> ToCss for ColorStop<D> {
899  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
900  where
901    W: std::fmt::Write,
902  {
903    self.color.to_css(dest)?;
904    if let Some(position) = &self.position {
905      dest.write_char(' ')?;
906      position.to_css(dest)?;
907    }
908    Ok(())
909  }
910}
911
912/// Either a color stop or interpolation hint within a gradient.
913///
914/// This type is generic, and items may be either a [LengthPercentage](super::length::LengthPercentage)
915/// or [Angle](super::angle::Angle) depending on what type of gradient it is within.
916#[derive(Debug, Clone, PartialEq)]
917#[cfg_attr(feature = "visitor", derive(Visit))]
918#[cfg_attr(
919  feature = "serde",
920  derive(serde::Serialize, serde::Deserialize),
921  serde(tag = "type", rename_all = "kebab-case")
922)]
923#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
924pub enum GradientItem<D> {
925  /// A color stop.
926  ColorStop(ColorStop<D>),
927  /// A color interpolation hint.
928  #[cfg_attr(
929    feature = "serde",
930    serde(
931      bound(serialize = "D: serde::Serialize", deserialize = "D: serde::Deserialize<'de>"),
932      with = "ValueWrapper::<D>"
933    )
934  )]
935  Hint(D),
936}
937
938impl<D: ToCss> ToCss for GradientItem<D> {
939  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
940  where
941    W: std::fmt::Write,
942  {
943    match self {
944      GradientItem::ColorStop(stop) => stop.to_css(dest),
945      GradientItem::Hint(hint) => hint.to_css(dest),
946    }
947  }
948}
949
950impl<D: Clone> GradientItem<D> {
951  /// Returns the color fallback types needed for the given browser targets.
952  pub fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
953    match self {
954      GradientItem::ColorStop(stop) => stop.color.get_necessary_fallbacks(targets),
955      GradientItem::Hint(..) => ColorFallbackKind::empty(),
956    }
957  }
958
959  /// Returns a fallback gradient item for the given color fallback type.
960  pub fn get_fallback(&self, kind: ColorFallbackKind) -> GradientItem<D> {
961    match self {
962      GradientItem::ColorStop(stop) => GradientItem::ColorStop(ColorStop {
963        color: stop.color.get_fallback(kind),
964        position: stop.position.clone(),
965      }),
966      GradientItem::Hint(..) => self.clone(),
967    }
968  }
969}
970
971impl<D> IsCompatible for GradientItem<D> {
972  fn is_compatible(&self, browsers: Browsers) -> bool {
973    match self {
974      GradientItem::ColorStop(c) => c.color.is_compatible(browsers),
975      GradientItem::Hint(..) => compat::Feature::GradientInterpolationHints.is_compatible(browsers),
976    }
977  }
978}
979
980fn parse_items<'i, 't, D: Parse<'i>>(
981  input: &mut Parser<'i, 't>,
982) -> Result<Vec<GradientItem<D>>, ParseError<'i, ParserError<'i>>> {
983  let mut items = Vec::new();
984  let mut seen_stop = false;
985
986  loop {
987    input.parse_until_before(Delimiter::Comma, |input| {
988      if seen_stop {
989        if let Ok(hint) = input.try_parse(D::parse) {
990          seen_stop = false;
991          items.push(GradientItem::Hint(hint));
992          return Ok(());
993        }
994      }
995
996      let stop = ColorStop::parse(input)?;
997
998      if let Ok(position) = input.try_parse(D::parse) {
999        let color = stop.color.clone();
1000        items.push(GradientItem::ColorStop(stop));
1001
1002        items.push(GradientItem::ColorStop(ColorStop {
1003          color,
1004          position: Some(position),
1005        }))
1006      } else {
1007        items.push(GradientItem::ColorStop(stop));
1008      }
1009
1010      seen_stop = true;
1011      Ok(())
1012    })?;
1013
1014    match input.next() {
1015      Err(_) => break,
1016      Ok(Token::Comma) => continue,
1017      _ => unreachable!(),
1018    }
1019  }
1020
1021  Ok(items)
1022}
1023
1024fn serialize_items<
1025  D: ToCss + std::cmp::PartialEq<D> + std::ops::Mul<f32, Output = D> + TrySign + Clone + std::fmt::Debug,
1026  W,
1027>(
1028  items: &Vec<GradientItem<DimensionPercentage<D>>>,
1029  dest: &mut Printer<W>,
1030) -> Result<(), PrinterError>
1031where
1032  W: std::fmt::Write,
1033{
1034  let mut first = true;
1035  let mut last: Option<&GradientItem<DimensionPercentage<D>>> = None;
1036  for item in items {
1037    // Skip useless hints
1038    if *item == GradientItem::Hint(DimensionPercentage::Percentage(Percentage(0.5))) {
1039      continue;
1040    }
1041
1042    // Use double position stop if the last stop is the same color and all targets support it.
1043    if let Some(prev) = last {
1044      if !should_compile!(dest.targets.current, DoublePositionGradients) {
1045        match (prev, item) {
1046          (
1047            GradientItem::ColorStop(ColorStop {
1048              position: Some(_),
1049              color: ca,
1050            }),
1051            GradientItem::ColorStop(ColorStop {
1052              position: Some(p),
1053              color: cb,
1054            }),
1055          ) if ca == cb => {
1056            dest.write_char(' ')?;
1057            p.to_css(dest)?;
1058            last = None;
1059            continue;
1060          }
1061          _ => {}
1062        }
1063      }
1064    }
1065
1066    if first {
1067      first = false;
1068    } else {
1069      dest.delim(',', false)?;
1070    }
1071    item.to_css(dest)?;
1072    last = Some(item)
1073  }
1074  Ok(())
1075}
1076
1077/// A legacy `-webkit-gradient()`.
1078#[derive(Debug, Clone, PartialEq)]
1079#[cfg_attr(feature = "visitor", derive(Visit))]
1080#[cfg_attr(
1081  feature = "serde",
1082  derive(serde::Serialize, serde::Deserialize),
1083  serde(tag = "kind", rename_all = "kebab-case")
1084)]
1085#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1086#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1087pub enum WebKitGradient {
1088  /// A linear `-webkit-gradient()`.
1089  Linear {
1090    /// The starting point of the gradient.
1091    from: WebKitGradientPoint,
1092    /// The ending point of the gradient.
1093    to: WebKitGradientPoint,
1094    /// The color stops in the gradient.
1095    stops: Vec<WebKitColorStop>,
1096  },
1097  /// A radial `-webkit-gradient()`.
1098  Radial {
1099    /// The starting point of the gradient.
1100    from: WebKitGradientPoint,
1101    /// The starting radius of the gradient.
1102    r0: CSSNumber,
1103    /// The ending point of the gradient.
1104    to: WebKitGradientPoint,
1105    /// The ending radius of the gradient.
1106    r1: CSSNumber,
1107    /// The color stops in the gradient.
1108    stops: Vec<WebKitColorStop>,
1109  },
1110}
1111
1112impl<'i> Parse<'i> for WebKitGradient {
1113  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1114    let location = input.current_source_location();
1115    let ident = input.expect_ident_cloned()?;
1116    input.expect_comma()?;
1117
1118    match_ignore_ascii_case! { &ident,
1119      "linear" => {
1120        let from = WebKitGradientPoint::parse(input)?;
1121        input.expect_comma()?;
1122        let to = WebKitGradientPoint::parse(input)?;
1123        input.expect_comma()?;
1124        let stops = input.parse_comma_separated(WebKitColorStop::parse)?;
1125        Ok(WebKitGradient::Linear {
1126          from,
1127          to,
1128          stops
1129        })
1130      },
1131      "radial" => {
1132        let from = WebKitGradientPoint::parse(input)?;
1133        input.expect_comma()?;
1134        let r0 = CSSNumber::parse(input)?;
1135        input.expect_comma()?;
1136        let to = WebKitGradientPoint::parse(input)?;
1137        input.expect_comma()?;
1138        let r1 = CSSNumber::parse(input)?;
1139        input.expect_comma()?;
1140        let stops = input.parse_comma_separated(WebKitColorStop::parse)?;
1141        Ok(WebKitGradient::Radial {
1142          from,
1143          r0,
1144          to,
1145          r1,
1146          stops
1147        })
1148      },
1149      _ => Err(location.new_unexpected_token_error(cssparser::Token::Ident(ident.clone())))
1150    }
1151  }
1152}
1153
1154impl ToCss for WebKitGradient {
1155  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
1156  where
1157    W: std::fmt::Write,
1158  {
1159    match self {
1160      WebKitGradient::Linear { from, to, stops } => {
1161        dest.write_str("linear")?;
1162        dest.delim(',', false)?;
1163        from.to_css(dest)?;
1164        dest.delim(',', false)?;
1165        to.to_css(dest)?;
1166        for stop in stops {
1167          dest.delim(',', false)?;
1168          stop.to_css(dest)?;
1169        }
1170        Ok(())
1171      }
1172      WebKitGradient::Radial {
1173        from,
1174        r0,
1175        to,
1176        r1,
1177        stops,
1178      } => {
1179        dest.write_str("radial")?;
1180        dest.delim(',', false)?;
1181        from.to_css(dest)?;
1182        dest.delim(',', false)?;
1183        r0.to_css(dest)?;
1184        dest.delim(',', false)?;
1185        to.to_css(dest)?;
1186        dest.delim(',', false)?;
1187        r1.to_css(dest)?;
1188        for stop in stops {
1189          dest.delim(',', false)?;
1190          stop.to_css(dest)?;
1191        }
1192        Ok(())
1193      }
1194    }
1195  }
1196}
1197
1198impl WebKitGradient {
1199  fn get_fallback(&self, kind: ColorFallbackKind) -> WebKitGradient {
1200    let stops = match self {
1201      WebKitGradient::Linear { stops, .. } => stops,
1202      WebKitGradient::Radial { stops, .. } => stops,
1203    };
1204
1205    let stops = stops.iter().map(|stop| stop.get_fallback(kind)).collect();
1206
1207    match self {
1208      WebKitGradient::Linear { from, to, .. } => WebKitGradient::Linear {
1209        from: from.clone(),
1210        to: to.clone(),
1211        stops,
1212      },
1213      WebKitGradient::Radial { from, r0, to, r1, .. } => WebKitGradient::Radial {
1214        from: from.clone(),
1215        r0: *r0,
1216        to: to.clone(),
1217        r1: *r1,
1218        stops,
1219      },
1220    }
1221  }
1222}
1223
1224/// A color stop within a legacy `-webkit-gradient()`.
1225#[derive(Debug, Clone, PartialEq)]
1226#[cfg_attr(feature = "visitor", derive(Visit))]
1227#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1228#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1229pub struct WebKitColorStop {
1230  /// The color of the color stop.
1231  pub color: CssColor,
1232  /// The position of the color stop.
1233  pub position: CSSNumber,
1234}
1235
1236impl<'i> Parse<'i> for WebKitColorStop {
1237  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1238    let location = input.current_source_location();
1239    let function = input.expect_function()?.clone();
1240    input.parse_nested_block(|input| {
1241      let position = match_ignore_ascii_case! { &function,
1242        "color-stop" => {
1243          let p = NumberOrPercentage::parse(input)?;
1244          input.expect_comma()?;
1245          (&p).into()
1246        },
1247        "from" => 0.0,
1248        "to" => 1.0,
1249        _ => return Err(location.new_unexpected_token_error(cssparser::Token::Ident(function.clone())))
1250      };
1251      let color = CssColor::parse(input)?;
1252      Ok(WebKitColorStop { color, position })
1253    })
1254  }
1255}
1256
1257impl ToCss for WebKitColorStop {
1258  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
1259  where
1260    W: std::fmt::Write,
1261  {
1262    if self.position == 0.0 {
1263      dest.write_str("from(")?;
1264      self.color.to_css(dest)?;
1265    } else if self.position == 1.0 {
1266      dest.write_str("to(")?;
1267      self.color.to_css(dest)?;
1268    } else {
1269      dest.write_str("color-stop(")?;
1270      self.position.to_css(dest)?;
1271      dest.delim(',', false)?;
1272      self.color.to_css(dest)?;
1273    }
1274    dest.write_char(')')
1275  }
1276}
1277
1278impl WebKitColorStop {
1279  fn get_fallback(&self, kind: ColorFallbackKind) -> WebKitColorStop {
1280    WebKitColorStop {
1281      color: self.color.get_fallback(kind),
1282      position: self.position,
1283    }
1284  }
1285}
1286
1287/// An x/y position within a legacy `-webkit-gradient()`.
1288#[derive(Debug, Clone, PartialEq)]
1289#[cfg_attr(feature = "visitor", derive(Visit))]
1290#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1291#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1292#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1293pub struct WebKitGradientPoint {
1294  /// The x-position.
1295  pub x: WebKitGradientPointComponent<HorizontalPositionKeyword>,
1296  /// The y-position.
1297  pub y: WebKitGradientPointComponent<VerticalPositionKeyword>,
1298}
1299
1300impl<'i> Parse<'i> for WebKitGradientPoint {
1301  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1302    let x = WebKitGradientPointComponent::parse(input)?;
1303    let y = WebKitGradientPointComponent::parse(input)?;
1304    Ok(WebKitGradientPoint { x, y })
1305  }
1306}
1307
1308impl ToCss for WebKitGradientPoint {
1309  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
1310  where
1311    W: std::fmt::Write,
1312  {
1313    self.x.to_css(dest)?;
1314    dest.write_char(' ')?;
1315    self.y.to_css(dest)
1316  }
1317}
1318
1319/// A keyword or number within a [WebKitGradientPoint](WebKitGradientPoint).
1320#[derive(Debug, Clone, PartialEq)]
1321#[cfg_attr(feature = "visitor", derive(Visit))]
1322#[cfg_attr(
1323  feature = "serde",
1324  derive(serde::Serialize, serde::Deserialize),
1325  serde(tag = "type", content = "value", rename_all = "kebab-case")
1326)]
1327#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1328#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1329pub enum WebKitGradientPointComponent<S> {
1330  /// The `center` keyword.
1331  Center,
1332  /// A number or percentage.
1333  Number(NumberOrPercentage),
1334  /// A side keyword.
1335  Side(S),
1336}
1337
1338impl<'i, S: Parse<'i>> Parse<'i> for WebKitGradientPointComponent<S> {
1339  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1340    if input.try_parse(|i| i.expect_ident_matching("center")).is_ok() {
1341      return Ok(WebKitGradientPointComponent::Center);
1342    }
1343
1344    if let Ok(lp) = input.try_parse(NumberOrPercentage::parse) {
1345      return Ok(WebKitGradientPointComponent::Number(lp));
1346    }
1347
1348    let keyword = S::parse(input)?;
1349    Ok(WebKitGradientPointComponent::Side(keyword))
1350  }
1351}
1352
1353impl<S: ToCss + Clone + Into<LengthPercentage>> ToCss for WebKitGradientPointComponent<S> {
1354  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
1355  where
1356    W: std::fmt::Write,
1357  {
1358    use WebKitGradientPointComponent::*;
1359    match &self {
1360      Center => {
1361        if dest.minify {
1362          dest.write_str("50%")
1363        } else {
1364          dest.write_str("center")
1365        }
1366      }
1367      Number(lp) => {
1368        if matches!(lp, NumberOrPercentage::Percentage(Percentage(p)) if *p == 0.0) {
1369          dest.write_char('0')
1370        } else {
1371          lp.to_css(dest)
1372        }
1373      }
1374      Side(s) => {
1375        if dest.minify {
1376          let lp: LengthPercentage = s.clone().into();
1377          lp.to_css(dest)?;
1378        } else {
1379          s.to_css(dest)?;
1380        }
1381        Ok(())
1382      }
1383    }
1384  }
1385}
1386
1387impl<S: Clone> WebKitGradientPointComponent<S> {
1388  /// Attempts to convert a standard position to a webkit gradient point.
1389  fn from_position(pos: &PositionComponent<S>) -> Result<WebKitGradientPointComponent<S>, ()> {
1390    match pos {
1391      PositionComponent::Center => Ok(WebKitGradientPointComponent::Center),
1392      PositionComponent::Length(len) => {
1393        Ok(WebKitGradientPointComponent::Number(match len {
1394          LengthPercentage::Percentage(p) => NumberOrPercentage::Percentage(p.clone()),
1395          LengthPercentage::Dimension(d) => {
1396            // Webkit gradient points can only be specified in pixels.
1397            if let Some(px) = d.to_px() {
1398              NumberOrPercentage::Number(px)
1399            } else {
1400              return Err(());
1401            }
1402          }
1403          _ => return Err(()),
1404        }))
1405      }
1406      PositionComponent::Side { side, offset } => {
1407        if offset.is_some() {
1408          return Err(());
1409        }
1410        Ok(WebKitGradientPointComponent::Side(side.clone()))
1411      }
1412    }
1413  }
1414}
1415
1416impl WebKitGradient {
1417  /// Attempts to convert a standard gradient to a legacy -webkit-gradient()
1418  pub fn from_standard(gradient: &Gradient) -> Result<WebKitGradient, ()> {
1419    match gradient {
1420      Gradient::Linear(linear) => {
1421        // Convert from line direction to a from and to point, if possible.
1422        let (from, to) = match &linear.direction {
1423          LineDirection::Horizontal(horizontal) => match horizontal {
1424            HorizontalPositionKeyword::Left => ((1.0, 0.0), (0.0, 0.0)),
1425            HorizontalPositionKeyword::Right => ((0.0, 0.0), (1.0, 0.0)),
1426          },
1427          LineDirection::Vertical(vertical) => match vertical {
1428            VerticalPositionKeyword::Top => ((0.0, 1.0), (0.0, 0.0)),
1429            VerticalPositionKeyword::Bottom => ((0.0, 0.0), (0.0, 1.0)),
1430          },
1431          LineDirection::Corner { horizontal, vertical } => match (horizontal, vertical) {
1432            (HorizontalPositionKeyword::Left, VerticalPositionKeyword::Top) => ((1.0, 1.0), (0.0, 0.0)),
1433            (HorizontalPositionKeyword::Left, VerticalPositionKeyword::Bottom) => ((1.0, 0.0), (0.0, 1.0)),
1434            (HorizontalPositionKeyword::Right, VerticalPositionKeyword::Top) => ((0.0, 1.0), (1.0, 0.0)),
1435            (HorizontalPositionKeyword::Right, VerticalPositionKeyword::Bottom) => ((0.0, 0.0), (1.0, 1.0)),
1436          },
1437          LineDirection::Angle(angle) => {
1438            let degrees = angle.to_degrees();
1439            if degrees == 0.0 {
1440              ((0.0, 1.0), (0.0, 0.0))
1441            } else if degrees == 90.0 {
1442              ((0.0, 0.0), (1.0, 0.0))
1443            } else if degrees == 180.0 {
1444              ((0.0, 0.0), (0.0, 1.0))
1445            } else if degrees == 270.0 {
1446              ((1.0, 0.0), (0.0, 0.0))
1447            } else {
1448              return Err(());
1449            }
1450          }
1451        };
1452
1453        Ok(WebKitGradient::Linear {
1454          from: WebKitGradientPoint {
1455            x: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(from.0))),
1456            y: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(from.1))),
1457          },
1458          to: WebKitGradientPoint {
1459            x: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(to.0))),
1460            y: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(to.1))),
1461          },
1462          stops: convert_stops_to_webkit(&linear.items)?,
1463        })
1464      }
1465      Gradient::Radial(radial) => {
1466        // Webkit radial gradients are always circles, not ellipses, and must be specified in pixels.
1467        let radius = match &radial.shape {
1468          EndingShape::Circle(Circle::Radius(radius)) => {
1469            if let Some(r) = radius.to_px() {
1470              r
1471            } else {
1472              return Err(());
1473            }
1474          }
1475          _ => return Err(()),
1476        };
1477
1478        let x = WebKitGradientPointComponent::from_position(&radial.position.x)?;
1479        let y = WebKitGradientPointComponent::from_position(&radial.position.y)?;
1480        let point = WebKitGradientPoint { x, y };
1481        Ok(WebKitGradient::Radial {
1482          from: point.clone(),
1483          r0: 0.0,
1484          to: point,
1485          r1: radius,
1486          stops: convert_stops_to_webkit(&radial.items)?,
1487        })
1488      }
1489      _ => Err(()),
1490    }
1491  }
1492}
1493
1494fn convert_stops_to_webkit(items: &Vec<GradientItem<LengthPercentage>>) -> Result<Vec<WebKitColorStop>, ()> {
1495  let mut stops = Vec::with_capacity(items.len());
1496  for (i, item) in items.iter().enumerate() {
1497    match item {
1498      GradientItem::ColorStop(stop) => {
1499        // webkit stops must always be percentage based, not length based.
1500        let position = if let Some(pos) = &stop.position {
1501          if let LengthPercentage::Percentage(position) = pos {
1502            position.0
1503          } else {
1504            return Err(());
1505          }
1506        } else if i == 0 {
1507          0.0
1508        } else if i == items.len() - 1 {
1509          1.0
1510        } else {
1511          return Err(());
1512        };
1513
1514        stops.push(WebKitColorStop {
1515          color: stop.color.clone(),
1516          position,
1517        })
1518      }
1519      _ => return Err(()),
1520    }
1521  }
1522
1523  Ok(stops)
1524}