tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

image.rs (49609B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
      4 
      5 //! CSS handling for the specified value of
      6 //! [`image`][image]s
      7 //!
      8 //! [image]: https://drafts.csswg.org/css-images/#image-values
      9 
     10 use crate::color::mix::ColorInterpolationMethod;
     11 use crate::derives::*;
     12 use crate::parser::{Parse, ParserContext};
     13 use crate::stylesheets::CorsMode;
     14 use crate::values::generics::color::{ColorMixFlags, GenericLightDark};
     15 use crate::values::generics::image::{
     16    self as generic, Circle, Ellipse, GradientCompatMode, ShapeExtent,
     17 };
     18 use crate::values::generics::image::{GradientFlags, PaintWorklet};
     19 use crate::values::generics::position::Position as GenericPosition;
     20 use crate::values::generics::NonNegative;
     21 use crate::values::specified::position::{HorizontalPositionKeyword, VerticalPositionKeyword};
     22 use crate::values::specified::position::{Position, PositionComponent, Side};
     23 use crate::values::specified::url::SpecifiedUrl;
     24 use crate::values::specified::{
     25    Angle, AngleOrPercentage, Color, Length, LengthPercentage, NonNegativeLength,
     26    NonNegativeLengthPercentage, Resolution,
     27 };
     28 use crate::values::specified::{Number, NumberOrPercentage, Percentage};
     29 use crate::Atom;
     30 use cssparser::{match_ignore_ascii_case, Delimiter, Parser, Token};
     31 use selectors::parser::SelectorParseErrorKind;
     32 use std::cmp::Ordering;
     33 use std::fmt::{self, Write};
     34 use style_traits::{CssType, CssWriter, KeywordsCollectFn, ParseError};
     35 use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss};
     36 
     37 #[inline]
     38 fn gradient_color_interpolation_method_enabled() -> bool {
     39    static_prefs::pref!("layout.css.gradient-color-interpolation-method.enabled")
     40 }
     41 
     42 /// Specified values for an image according to CSS-IMAGES.
     43 /// <https://drafts.csswg.org/css-images/#image-values>
     44 pub type Image = generic::Image<Gradient, SpecifiedUrl, Color, Percentage, Resolution>;
     45 
     46 // Images should remain small, see https://github.com/servo/servo/pull/18430
     47 size_of_test!(Image, 16);
     48 
     49 /// Specified values for a CSS gradient.
     50 /// <https://drafts.csswg.org/css-images/#gradients>
     51 pub type Gradient = generic::Gradient<
     52    LineDirection,
     53    Length,
     54    LengthPercentage,
     55    Position,
     56    Angle,
     57    AngleOrPercentage,
     58    Color,
     59 >;
     60 
     61 /// Specified values for CSS cross-fade
     62 /// cross-fade( CrossFadeElement, ...)
     63 /// <https://drafts.csswg.org/css-images-4/#cross-fade-function>
     64 pub type CrossFade = generic::CrossFade<Image, Color, Percentage>;
     65 /// CrossFadeElement = percent? CrossFadeImage
     66 pub type CrossFadeElement = generic::CrossFadeElement<Image, Color, Percentage>;
     67 /// CrossFadeImage = image | color
     68 pub type CrossFadeImage = generic::CrossFadeImage<Image, Color>;
     69 
     70 /// `image-set()`
     71 pub type ImageSet = generic::ImageSet<Image, Resolution>;
     72 
     73 /// Each of the arguments to `image-set()`
     74 pub type ImageSetItem = generic::ImageSetItem<Image, Resolution>;
     75 
     76 type LengthPercentageItemList = crate::OwnedSlice<generic::GradientItem<Color, LengthPercentage>>;
     77 
     78 impl Color {
     79    fn has_modern_syntax(&self) -> bool {
     80        match self {
     81            Self::Absolute(absolute) => !absolute.color.is_legacy_syntax(),
     82            Self::ColorMix(mix) => {
     83                if mix.flags.contains(ColorMixFlags::RESULT_IN_MODERN_SYNTAX) {
     84                    true
     85                } else {
     86                    mix.left.has_modern_syntax() || mix.right.has_modern_syntax()
     87                }
     88            },
     89            Self::LightDark(ld) => ld.light.has_modern_syntax() || ld.dark.has_modern_syntax(),
     90 
     91            // The default is that this color doesn't have any modern syntax.
     92            _ => false,
     93        }
     94    }
     95 }
     96 
     97 fn default_color_interpolation_method<T>(
     98    items: &[generic::GradientItem<Color, T>],
     99 ) -> ColorInterpolationMethod {
    100    let has_modern_syntax_item = items.iter().any(|item| match item {
    101        generic::GenericGradientItem::SimpleColorStop(color) => color.has_modern_syntax(),
    102        generic::GenericGradientItem::ComplexColorStop { color, .. } => color.has_modern_syntax(),
    103        generic::GenericGradientItem::InterpolationHint(_) => false,
    104    });
    105 
    106    if has_modern_syntax_item {
    107        ColorInterpolationMethod::default()
    108    } else {
    109        ColorInterpolationMethod::srgb()
    110    }
    111 }
    112 
    113 fn image_light_dark_enabled(context: &ParserContext) -> bool {
    114    context.chrome_rules_enabled() || static_prefs::pref!("layout.css.light-dark.images.enabled")
    115 }
    116 
    117 #[cfg(feature = "gecko")]
    118 fn cross_fade_enabled() -> bool {
    119    static_prefs::pref!("layout.css.cross-fade.enabled")
    120 }
    121 
    122 #[cfg(feature = "servo")]
    123 fn cross_fade_enabled() -> bool {
    124    false
    125 }
    126 
    127 impl SpecifiedValueInfo for Gradient {
    128    const SUPPORTED_TYPES: u8 = CssType::GRADIENT;
    129 
    130    fn collect_completion_keywords(f: KeywordsCollectFn) {
    131        // This list here should keep sync with that in Gradient::parse.
    132        f(&[
    133            "linear-gradient",
    134            "-webkit-linear-gradient",
    135            "-moz-linear-gradient",
    136            "repeating-linear-gradient",
    137            "-webkit-repeating-linear-gradient",
    138            "-moz-repeating-linear-gradient",
    139            "radial-gradient",
    140            "-webkit-radial-gradient",
    141            "-moz-radial-gradient",
    142            "repeating-radial-gradient",
    143            "-webkit-repeating-radial-gradient",
    144            "-moz-repeating-radial-gradient",
    145            "-webkit-gradient",
    146            "conic-gradient",
    147            "repeating-conic-gradient",
    148        ]);
    149    }
    150 }
    151 
    152 // Need to manually implement as whether or not cross-fade shows up in
    153 // completions & etc is dependent on it being enabled.
    154 impl<Image, Color, Percentage> SpecifiedValueInfo for generic::CrossFade<Image, Color, Percentage> {
    155    const SUPPORTED_TYPES: u8 = 0;
    156 
    157    fn collect_completion_keywords(f: KeywordsCollectFn) {
    158        if cross_fade_enabled() {
    159            f(&["cross-fade"]);
    160        }
    161    }
    162 }
    163 
    164 impl<Image, Resolution> SpecifiedValueInfo for generic::ImageSet<Image, Resolution> {
    165    const SUPPORTED_TYPES: u8 = 0;
    166 
    167    fn collect_completion_keywords(f: KeywordsCollectFn) {
    168        f(&["image-set"]);
    169    }
    170 }
    171 
    172 /// A specified gradient line direction.
    173 ///
    174 /// FIXME(emilio): This should be generic over Angle.
    175 #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
    176 pub enum LineDirection {
    177    /// An angular direction.
    178    Angle(Angle),
    179    /// A horizontal direction.
    180    Horizontal(HorizontalPositionKeyword),
    181    /// A vertical direction.
    182    Vertical(VerticalPositionKeyword),
    183    /// A direction towards a corner of a box.
    184    Corner(HorizontalPositionKeyword, VerticalPositionKeyword),
    185 }
    186 
    187 /// A specified ending shape.
    188 pub type EndingShape = generic::EndingShape<NonNegativeLength, NonNegativeLengthPercentage>;
    189 
    190 bitflags! {
    191    #[derive(Clone, Copy)]
    192    struct ParseImageFlags: u8 {
    193        const FORBID_NONE = 1 << 0;
    194        const FORBID_IMAGE_SET = 1 << 1;
    195        const FORBID_NON_URL = 1 << 2;
    196    }
    197 }
    198 
    199 impl Parse for Image {
    200    fn parse<'i, 't>(
    201        context: &ParserContext,
    202        input: &mut Parser<'i, 't>,
    203    ) -> Result<Image, ParseError<'i>> {
    204        Image::parse_with_cors_mode(context, input, CorsMode::None, ParseImageFlags::empty())
    205    }
    206 }
    207 
    208 impl Image {
    209    fn parse_with_cors_mode<'i, 't>(
    210        context: &ParserContext,
    211        input: &mut Parser<'i, 't>,
    212        cors_mode: CorsMode,
    213        flags: ParseImageFlags,
    214    ) -> Result<Image, ParseError<'i>> {
    215        if !flags.contains(ParseImageFlags::FORBID_NONE)
    216            && input.try_parse(|i| i.expect_ident_matching("none")).is_ok()
    217        {
    218            return Ok(generic::Image::None);
    219        }
    220 
    221        if let Ok(url) =
    222            input.try_parse(|input| SpecifiedUrl::parse_with_cors_mode(context, input, cors_mode))
    223        {
    224            return Ok(generic::Image::Url(url));
    225        }
    226 
    227        if !flags.contains(ParseImageFlags::FORBID_IMAGE_SET) {
    228            if let Ok(is) =
    229                input.try_parse(|input| ImageSet::parse(context, input, cors_mode, flags))
    230            {
    231                return Ok(generic::Image::ImageSet(Box::new(is)));
    232            }
    233        }
    234 
    235        if flags.contains(ParseImageFlags::FORBID_NON_URL) {
    236            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
    237        }
    238 
    239        if let Ok(gradient) = input.try_parse(|i| Gradient::parse(context, i)) {
    240            return Ok(generic::Image::Gradient(Box::new(gradient)));
    241        }
    242 
    243        let function = input.expect_function()?.clone();
    244        input.parse_nested_block(|input| Ok(match_ignore_ascii_case! { &function,
    245            #[cfg(feature = "servo")]
    246            "paint" => Self::PaintWorklet(Box::new(<PaintWorklet>::parse_args(context, input)?)),
    247            "cross-fade" if cross_fade_enabled() => Self::CrossFade(Box::new(CrossFade::parse_args(context, input, cors_mode, flags)?)),
    248            "light-dark" if image_light_dark_enabled(context) => Self::LightDark(Box::new(GenericLightDark::parse_args_with(input, |input| {
    249                Self::parse_with_cors_mode(context, input, cors_mode, flags)
    250            })?)),
    251            #[cfg(feature = "gecko")]
    252            "-moz-element" => Self::Element(Self::parse_element(input)?),
    253            #[cfg(feature = "gecko")]
    254            "-moz-symbolic-icon" if context.chrome_rules_enabled() => Self::MozSymbolicIcon(input.expect_ident()?.as_ref().into()),
    255            _ => return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedFunction(function))),
    256        }))
    257    }
    258 }
    259 
    260 impl Image {
    261    /// Creates an already specified image value from an already resolved URL
    262    /// for insertion in the cascade.
    263    #[cfg(feature = "servo")]
    264    pub fn for_cascade(url: ::servo_arc::Arc<::url::Url>) -> Self {
    265        use crate::values::CssUrl;
    266        generic::Image::Url(CssUrl::for_cascade(url))
    267    }
    268 
    269    /// Parses a `-moz-element(# <element-id>)`.
    270    #[cfg(feature = "gecko")]
    271    fn parse_element<'i>(input: &mut Parser<'i, '_>) -> Result<Atom, ParseError<'i>> {
    272        let location = input.current_source_location();
    273        Ok(match *input.next()? {
    274            Token::IDHash(ref id) => Atom::from(id.as_ref()),
    275            ref t => return Err(location.new_unexpected_token_error(t.clone())),
    276        })
    277    }
    278 
    279    /// Provides an alternate method for parsing that associates the URL with
    280    /// anonymous CORS headers.
    281    pub fn parse_with_cors_anonymous<'i, 't>(
    282        context: &ParserContext,
    283        input: &mut Parser<'i, 't>,
    284    ) -> Result<Image, ParseError<'i>> {
    285        Self::parse_with_cors_mode(
    286            context,
    287            input,
    288            CorsMode::Anonymous,
    289            ParseImageFlags::empty(),
    290        )
    291    }
    292 
    293    /// Provides an alternate method for parsing, but forbidding `none`
    294    pub fn parse_forbid_none<'i, 't>(
    295        context: &ParserContext,
    296        input: &mut Parser<'i, 't>,
    297    ) -> Result<Image, ParseError<'i>> {
    298        Self::parse_with_cors_mode(context, input, CorsMode::None, ParseImageFlags::FORBID_NONE)
    299    }
    300 
    301    /// Provides an alternate method for parsing, but only for urls.
    302    pub fn parse_only_url<'i, 't>(
    303        context: &ParserContext,
    304        input: &mut Parser<'i, 't>,
    305    ) -> Result<Image, ParseError<'i>> {
    306        Self::parse_with_cors_mode(
    307            context,
    308            input,
    309            CorsMode::None,
    310            ParseImageFlags::FORBID_NONE | ParseImageFlags::FORBID_NON_URL,
    311        )
    312    }
    313 }
    314 
    315 impl CrossFade {
    316    /// cross-fade() = cross-fade( <cf-image># )
    317    fn parse_args<'i, 't>(
    318        context: &ParserContext,
    319        input: &mut Parser<'i, 't>,
    320        cors_mode: CorsMode,
    321        flags: ParseImageFlags,
    322    ) -> Result<Self, ParseError<'i>> {
    323        let elements = crate::OwnedSlice::from(input.parse_comma_separated(|input| {
    324            CrossFadeElement::parse(context, input, cors_mode, flags)
    325        })?);
    326        Ok(Self { elements })
    327    }
    328 }
    329 
    330 impl CrossFadeElement {
    331    fn parse_percentage<'i, 't>(
    332        context: &ParserContext,
    333        input: &mut Parser<'i, 't>,
    334    ) -> Option<Percentage> {
    335        // We clamp our values here as this is the way that Safari and Chrome's
    336        // implementation handle out-of-bounds percentages but whether or not
    337        // this behavior follows the specification is still being discussed.
    338        // See: <https://github.com/w3c/csswg-drafts/issues/5333>
    339        input
    340            .try_parse(|input| Percentage::parse_non_negative(context, input))
    341            .ok()
    342            .map(|p| p.clamp_to_hundred())
    343    }
    344 
    345    /// <cf-image> = <percentage>? && [ <image> | <color> ]
    346    fn parse<'i, 't>(
    347        context: &ParserContext,
    348        input: &mut Parser<'i, 't>,
    349        cors_mode: CorsMode,
    350        flags: ParseImageFlags,
    351    ) -> Result<Self, ParseError<'i>> {
    352        // Try and parse a leading percent sign.
    353        let mut percent = Self::parse_percentage(context, input);
    354        // Parse the image
    355        let image = CrossFadeImage::parse(context, input, cors_mode, flags)?;
    356        // Try and parse a trailing percent sign.
    357        if percent.is_none() {
    358            percent = Self::parse_percentage(context, input);
    359        }
    360        Ok(Self {
    361            percent: percent.into(),
    362            image,
    363        })
    364    }
    365 }
    366 
    367 impl CrossFadeImage {
    368    fn parse<'i, 't>(
    369        context: &ParserContext,
    370        input: &mut Parser<'i, 't>,
    371        cors_mode: CorsMode,
    372        flags: ParseImageFlags,
    373    ) -> Result<Self, ParseError<'i>> {
    374        if let Ok(image) = input.try_parse(|input| {
    375            Image::parse_with_cors_mode(
    376                context,
    377                input,
    378                cors_mode,
    379                flags | ParseImageFlags::FORBID_NONE,
    380            )
    381        }) {
    382            return Ok(Self::Image(image));
    383        }
    384        Ok(Self::Color(Color::parse(context, input)?))
    385    }
    386 }
    387 
    388 impl ImageSet {
    389    fn parse<'i, 't>(
    390        context: &ParserContext,
    391        input: &mut Parser<'i, 't>,
    392        cors_mode: CorsMode,
    393        flags: ParseImageFlags,
    394    ) -> Result<Self, ParseError<'i>> {
    395        let function = input.expect_function()?;
    396        match_ignore_ascii_case! { &function,
    397            "-webkit-image-set" | "image-set" => {},
    398            _ => {
    399                let func = function.clone();
    400                return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedFunction(func)));
    401            }
    402        }
    403        let items = input.parse_nested_block(|input| {
    404            input.parse_comma_separated(|input| {
    405                ImageSetItem::parse(context, input, cors_mode, flags)
    406            })
    407        })?;
    408        Ok(Self {
    409            selected_index: std::usize::MAX,
    410            items: items.into(),
    411        })
    412    }
    413 }
    414 
    415 impl ImageSetItem {
    416    fn parse_type<'i>(p: &mut Parser<'i, '_>) -> Result<crate::OwnedStr, ParseError<'i>> {
    417        p.expect_function_matching("type")?;
    418        p.parse_nested_block(|input| Ok(input.expect_string()?.as_ref().to_owned().into()))
    419    }
    420 
    421    fn parse<'i, 't>(
    422        context: &ParserContext,
    423        input: &mut Parser<'i, 't>,
    424        cors_mode: CorsMode,
    425        flags: ParseImageFlags,
    426    ) -> Result<Self, ParseError<'i>> {
    427        let image = match input.try_parse(|i| i.expect_url_or_string()) {
    428            Ok(url) => Image::Url(SpecifiedUrl::parse_from_string(
    429                url.as_ref().into(),
    430                context,
    431                cors_mode,
    432            )),
    433            Err(..) => Image::parse_with_cors_mode(
    434                context,
    435                input,
    436                cors_mode,
    437                flags | ParseImageFlags::FORBID_NONE | ParseImageFlags::FORBID_IMAGE_SET,
    438            )?,
    439        };
    440 
    441        let mut resolution = input
    442            .try_parse(|input| Resolution::parse(context, input))
    443            .ok();
    444        let mime_type = input.try_parse(Self::parse_type).ok();
    445 
    446        // Try to parse resolution after type().
    447        if mime_type.is_some() && resolution.is_none() {
    448            resolution = input
    449                .try_parse(|input| Resolution::parse(context, input))
    450                .ok();
    451        }
    452 
    453        let resolution = resolution.unwrap_or_else(|| Resolution::from_x(1.0));
    454        let has_mime_type = mime_type.is_some();
    455        let mime_type = mime_type.unwrap_or_default();
    456 
    457        Ok(Self {
    458            image,
    459            resolution,
    460            has_mime_type,
    461            mime_type,
    462        })
    463    }
    464 }
    465 
    466 impl Parse for Gradient {
    467    fn parse<'i, 't>(
    468        context: &ParserContext,
    469        input: &mut Parser<'i, 't>,
    470    ) -> Result<Self, ParseError<'i>> {
    471        enum Shape {
    472            Linear,
    473            Radial,
    474            Conic,
    475        }
    476 
    477        let func = input.expect_function()?;
    478        let (shape, repeating, compat_mode) = match_ignore_ascii_case! { &func,
    479            "linear-gradient" => {
    480                (Shape::Linear, false, GradientCompatMode::Modern)
    481            },
    482            "-webkit-linear-gradient" => {
    483                (Shape::Linear, false, GradientCompatMode::WebKit)
    484            },
    485            #[cfg(feature = "gecko")]
    486            "-moz-linear-gradient" => {
    487                (Shape::Linear, false, GradientCompatMode::Moz)
    488            },
    489            "repeating-linear-gradient" => {
    490                (Shape::Linear, true, GradientCompatMode::Modern)
    491            },
    492            "-webkit-repeating-linear-gradient" => {
    493                (Shape::Linear, true, GradientCompatMode::WebKit)
    494            },
    495            #[cfg(feature = "gecko")]
    496            "-moz-repeating-linear-gradient" => {
    497                (Shape::Linear, true, GradientCompatMode::Moz)
    498            },
    499            "radial-gradient" => {
    500                (Shape::Radial, false, GradientCompatMode::Modern)
    501            },
    502            "-webkit-radial-gradient" => {
    503                (Shape::Radial, false, GradientCompatMode::WebKit)
    504            },
    505            #[cfg(feature = "gecko")]
    506            "-moz-radial-gradient" => {
    507                (Shape::Radial, false, GradientCompatMode::Moz)
    508            },
    509            "repeating-radial-gradient" => {
    510                (Shape::Radial, true, GradientCompatMode::Modern)
    511            },
    512            "-webkit-repeating-radial-gradient" => {
    513                (Shape::Radial, true, GradientCompatMode::WebKit)
    514            },
    515            #[cfg(feature = "gecko")]
    516            "-moz-repeating-radial-gradient" => {
    517                (Shape::Radial, true, GradientCompatMode::Moz)
    518            },
    519            "conic-gradient" => {
    520                (Shape::Conic, false, GradientCompatMode::Modern)
    521            },
    522            "repeating-conic-gradient" => {
    523                (Shape::Conic, true, GradientCompatMode::Modern)
    524            },
    525            "-webkit-gradient" => {
    526                return input.parse_nested_block(|i| {
    527                    Self::parse_webkit_gradient_argument(context, i)
    528                });
    529            },
    530            _ => {
    531                let func = func.clone();
    532                return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedFunction(func)));
    533            }
    534        };
    535 
    536        Ok(input.parse_nested_block(|i| {
    537            Ok(match shape {
    538                Shape::Linear => Self::parse_linear(context, i, repeating, compat_mode)?,
    539                Shape::Radial => Self::parse_radial(context, i, repeating, compat_mode)?,
    540                Shape::Conic => Self::parse_conic(context, i, repeating)?,
    541            })
    542        })?)
    543    }
    544 }
    545 
    546 impl Gradient {
    547    fn parse_webkit_gradient_argument<'i, 't>(
    548        context: &ParserContext,
    549        input: &mut Parser<'i, 't>,
    550    ) -> Result<Self, ParseError<'i>> {
    551        use crate::values::specified::position::{
    552            HorizontalPositionKeyword as X, VerticalPositionKeyword as Y,
    553        };
    554        type Point = GenericPosition<Component<X>, Component<Y>>;
    555 
    556        #[derive(Clone, Copy, Parse)]
    557        enum Component<S> {
    558            Center,
    559            Number(NumberOrPercentage),
    560            Side(S),
    561        }
    562 
    563        fn line_direction_from_points(first: Point, second: Point) -> LineDirection {
    564            let h_ord = first.horizontal.partial_cmp(&second.horizontal);
    565            let v_ord = first.vertical.partial_cmp(&second.vertical);
    566            let (h, v) = match (h_ord, v_ord) {
    567                (Some(h), Some(v)) => (h, v),
    568                _ => return LineDirection::Vertical(Y::Bottom),
    569            };
    570            match (h, v) {
    571                (Ordering::Less, Ordering::Less) => LineDirection::Corner(X::Right, Y::Bottom),
    572                (Ordering::Less, Ordering::Equal) => LineDirection::Horizontal(X::Right),
    573                (Ordering::Less, Ordering::Greater) => LineDirection::Corner(X::Right, Y::Top),
    574                (Ordering::Equal, Ordering::Greater) => LineDirection::Vertical(Y::Top),
    575                (Ordering::Equal, Ordering::Equal) | (Ordering::Equal, Ordering::Less) => {
    576                    LineDirection::Vertical(Y::Bottom)
    577                },
    578                (Ordering::Greater, Ordering::Less) => LineDirection::Corner(X::Left, Y::Bottom),
    579                (Ordering::Greater, Ordering::Equal) => LineDirection::Horizontal(X::Left),
    580                (Ordering::Greater, Ordering::Greater) => LineDirection::Corner(X::Left, Y::Top),
    581            }
    582        }
    583 
    584        impl Parse for Point {
    585            fn parse<'i, 't>(
    586                context: &ParserContext,
    587                input: &mut Parser<'i, 't>,
    588            ) -> Result<Self, ParseError<'i>> {
    589                input.try_parse(|i| {
    590                    let x = Component::parse(context, i)?;
    591                    let y = Component::parse(context, i)?;
    592 
    593                    Ok(Self::new(x, y))
    594                })
    595            }
    596        }
    597 
    598        impl<S: Side> Into<NumberOrPercentage> for Component<S> {
    599            fn into(self) -> NumberOrPercentage {
    600                match self {
    601                    Component::Center => NumberOrPercentage::Percentage(Percentage::new(0.5)),
    602                    Component::Number(number) => number,
    603                    Component::Side(side) => {
    604                        let p = if side.is_start() {
    605                            Percentage::zero()
    606                        } else {
    607                            Percentage::hundred()
    608                        };
    609                        NumberOrPercentage::Percentage(p)
    610                    },
    611                }
    612            }
    613        }
    614 
    615        impl<S: Side> Into<PositionComponent<S>> for Component<S> {
    616            fn into(self) -> PositionComponent<S> {
    617                match self {
    618                    Component::Center => PositionComponent::Center,
    619                    Component::Number(NumberOrPercentage::Number(number)) => {
    620                        PositionComponent::Length(Length::from_px(number.value).into())
    621                    },
    622                    Component::Number(NumberOrPercentage::Percentage(p)) => {
    623                        PositionComponent::Length(p.into())
    624                    },
    625                    Component::Side(side) => PositionComponent::Side(side, None),
    626                }
    627            }
    628        }
    629 
    630        impl<S: Copy + Side> Component<S> {
    631            fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
    632                match ((*self).into(), (*other).into()) {
    633                    (NumberOrPercentage::Percentage(a), NumberOrPercentage::Percentage(b)) => {
    634                        a.get().partial_cmp(&b.get())
    635                    },
    636                    (NumberOrPercentage::Number(a), NumberOrPercentage::Number(b)) => {
    637                        a.value.partial_cmp(&b.value)
    638                    },
    639                    (_, _) => None,
    640                }
    641            }
    642        }
    643 
    644        let ident = input.expect_ident_cloned()?;
    645        input.expect_comma()?;
    646 
    647        Ok(match_ignore_ascii_case! { &ident,
    648            "linear" => {
    649                let first = Point::parse(context, input)?;
    650                input.expect_comma()?;
    651                let second = Point::parse(context, input)?;
    652 
    653                let direction = line_direction_from_points(first, second);
    654                let items = Gradient::parse_webkit_gradient_stops(context, input, false)?;
    655 
    656                generic::Gradient::Linear {
    657                    direction,
    658                    color_interpolation_method: ColorInterpolationMethod::srgb(),
    659                    items,
    660                    // Legacy gradients always use srgb as a default.
    661                    flags: generic::GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
    662                    compat_mode: GradientCompatMode::Modern,
    663                }
    664            },
    665            "radial" => {
    666                let first_point = Point::parse(context, input)?;
    667                input.expect_comma()?;
    668                let first_radius = Number::parse_non_negative(context, input)?;
    669                input.expect_comma()?;
    670                let second_point = Point::parse(context, input)?;
    671                input.expect_comma()?;
    672                let second_radius = Number::parse_non_negative(context, input)?;
    673 
    674                let (reverse_stops, point, radius) = if second_radius.value >= first_radius.value {
    675                    (false, second_point, second_radius)
    676                } else {
    677                    (true, first_point, first_radius)
    678                };
    679 
    680                let rad = Circle::Radius(NonNegative(Length::from_px(radius.value)));
    681                let shape = generic::EndingShape::Circle(rad);
    682                let position = Position::new(point.horizontal.into(), point.vertical.into());
    683                let items = Gradient::parse_webkit_gradient_stops(context, input, reverse_stops)?;
    684 
    685                generic::Gradient::Radial {
    686                    shape,
    687                    position,
    688                    color_interpolation_method: ColorInterpolationMethod::srgb(),
    689                    items,
    690                    // Legacy gradients always use srgb as a default.
    691                    flags: generic::GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
    692                    compat_mode: GradientCompatMode::Modern,
    693                }
    694            },
    695            _ => {
    696                let e = SelectorParseErrorKind::UnexpectedIdent(ident.clone());
    697                return Err(input.new_custom_error(e));
    698            },
    699        })
    700    }
    701 
    702    fn parse_webkit_gradient_stops<'i, 't>(
    703        context: &ParserContext,
    704        input: &mut Parser<'i, 't>,
    705        reverse_stops: bool,
    706    ) -> Result<LengthPercentageItemList, ParseError<'i>> {
    707        let mut items = input
    708            .try_parse(|i| {
    709                i.expect_comma()?;
    710                i.parse_comma_separated(|i| {
    711                    let function = i.expect_function()?.clone();
    712                    let (color, mut p) = i.parse_nested_block(|i| {
    713                        let p = match_ignore_ascii_case! { &function,
    714                            "color-stop" => {
    715                                let p = NumberOrPercentage::parse(context, i)?.to_percentage();
    716                                i.expect_comma()?;
    717                                p
    718                            },
    719                            "from" => Percentage::zero(),
    720                            "to" => Percentage::hundred(),
    721                            _ => {
    722                                return Err(i.new_custom_error(
    723                                    StyleParseErrorKind::UnexpectedFunction(function.clone())
    724                                ))
    725                            },
    726                        };
    727                        let color = Color::parse(context, i)?;
    728                        if color == Color::CurrentColor {
    729                            return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError));
    730                        }
    731                        Ok((color.into(), p))
    732                    })?;
    733                    if reverse_stops {
    734                        p.reverse();
    735                    }
    736                    Ok(generic::GradientItem::ComplexColorStop {
    737                        color,
    738                        position: p.into(),
    739                    })
    740                })
    741            })
    742            .unwrap_or(vec![]);
    743 
    744        if items.is_empty() {
    745            items = vec![
    746                generic::GradientItem::ComplexColorStop {
    747                    color: Color::transparent(),
    748                    position: LengthPercentage::zero_percent(),
    749                },
    750                generic::GradientItem::ComplexColorStop {
    751                    color: Color::transparent(),
    752                    position: LengthPercentage::hundred_percent(),
    753                },
    754            ];
    755        } else if items.len() == 1 {
    756            let first = items[0].clone();
    757            items.push(first);
    758        } else {
    759            items.sort_by(|a, b| {
    760                match (a, b) {
    761                    (
    762                        &generic::GradientItem::ComplexColorStop {
    763                            position: ref a_position,
    764                            ..
    765                        },
    766                        &generic::GradientItem::ComplexColorStop {
    767                            position: ref b_position,
    768                            ..
    769                        },
    770                    ) => match (a_position, b_position) {
    771                        (&LengthPercentage::Percentage(a), &LengthPercentage::Percentage(b)) => {
    772                            return a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal);
    773                        },
    774                        _ => {},
    775                    },
    776                    _ => {},
    777                }
    778                if reverse_stops {
    779                    Ordering::Greater
    780                } else {
    781                    Ordering::Less
    782                }
    783            })
    784        }
    785        Ok(items.into())
    786    }
    787 
    788    /// Not used for -webkit-gradient syntax and conic-gradient
    789    fn parse_stops<'i, 't>(
    790        context: &ParserContext,
    791        input: &mut Parser<'i, 't>,
    792    ) -> Result<LengthPercentageItemList, ParseError<'i>> {
    793        let items =
    794            generic::GradientItem::parse_comma_separated(context, input, LengthPercentage::parse)?;
    795        if items.is_empty() {
    796            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
    797        }
    798        Ok(items)
    799    }
    800 
    801    /// Try to parse a color interpolation method.
    802    fn try_parse_color_interpolation_method<'i, 't>(
    803        context: &ParserContext,
    804        input: &mut Parser<'i, 't>,
    805    ) -> Option<ColorInterpolationMethod> {
    806        if gradient_color_interpolation_method_enabled() {
    807            input
    808                .try_parse(|i| ColorInterpolationMethod::parse(context, i))
    809                .ok()
    810        } else {
    811            None
    812        }
    813    }
    814 
    815    /// Parses a linear gradient.
    816    /// GradientCompatMode can change during `-moz-` prefixed gradient parsing if it come across a `to` keyword.
    817    fn parse_linear<'i, 't>(
    818        context: &ParserContext,
    819        input: &mut Parser<'i, 't>,
    820        repeating: bool,
    821        mut compat_mode: GradientCompatMode,
    822    ) -> Result<Self, ParseError<'i>> {
    823        let mut flags = GradientFlags::empty();
    824        flags.set(GradientFlags::REPEATING, repeating);
    825 
    826        let mut color_interpolation_method =
    827            Self::try_parse_color_interpolation_method(context, input);
    828 
    829        let direction = input
    830            .try_parse(|p| LineDirection::parse(context, p, &mut compat_mode))
    831            .ok();
    832 
    833        if direction.is_some() && color_interpolation_method.is_none() {
    834            color_interpolation_method = Self::try_parse_color_interpolation_method(context, input);
    835        }
    836 
    837        // If either of the 2 options were specified, we require a comma.
    838        if color_interpolation_method.is_some() || direction.is_some() {
    839            input.expect_comma()?;
    840        }
    841 
    842        let items = Gradient::parse_stops(context, input)?;
    843 
    844        let default = default_color_interpolation_method(&items);
    845        let color_interpolation_method = color_interpolation_method.unwrap_or(default);
    846        flags.set(
    847            GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
    848            default == color_interpolation_method,
    849        );
    850 
    851        let direction = direction.unwrap_or(match compat_mode {
    852            GradientCompatMode::Modern => LineDirection::Vertical(VerticalPositionKeyword::Bottom),
    853            _ => LineDirection::Vertical(VerticalPositionKeyword::Top),
    854        });
    855 
    856        Ok(Gradient::Linear {
    857            direction,
    858            color_interpolation_method,
    859            items,
    860            flags,
    861            compat_mode,
    862        })
    863    }
    864 
    865    /// Parses a radial gradient.
    866    fn parse_radial<'i, 't>(
    867        context: &ParserContext,
    868        input: &mut Parser<'i, 't>,
    869        repeating: bool,
    870        compat_mode: GradientCompatMode,
    871    ) -> Result<Self, ParseError<'i>> {
    872        let mut flags = GradientFlags::empty();
    873        flags.set(GradientFlags::REPEATING, repeating);
    874 
    875        let mut color_interpolation_method =
    876            Self::try_parse_color_interpolation_method(context, input);
    877 
    878        let (shape, position) = match compat_mode {
    879            GradientCompatMode::Modern => {
    880                let shape = input.try_parse(|i| EndingShape::parse(context, i, compat_mode));
    881                let position = input.try_parse(|i| {
    882                    i.expect_ident_matching("at")?;
    883                    Position::parse(context, i)
    884                });
    885                (shape, position.ok())
    886            },
    887            _ => {
    888                let position = input.try_parse(|i| Position::parse(context, i));
    889                let shape = input.try_parse(|i| {
    890                    if position.is_ok() {
    891                        i.expect_comma()?;
    892                    }
    893                    EndingShape::parse(context, i, compat_mode)
    894                });
    895                (shape, position.ok())
    896            },
    897        };
    898 
    899        let has_shape_or_position = shape.is_ok() || position.is_some();
    900        if has_shape_or_position && color_interpolation_method.is_none() {
    901            color_interpolation_method = Self::try_parse_color_interpolation_method(context, input);
    902        }
    903 
    904        if has_shape_or_position || color_interpolation_method.is_some() {
    905            input.expect_comma()?;
    906        }
    907 
    908        let shape = shape.unwrap_or({
    909            generic::EndingShape::Ellipse(Ellipse::Extent(ShapeExtent::FarthestCorner))
    910        });
    911 
    912        let position = position.unwrap_or(Position::center());
    913 
    914        let items = Gradient::parse_stops(context, input)?;
    915 
    916        let default = default_color_interpolation_method(&items);
    917        let color_interpolation_method = color_interpolation_method.unwrap_or(default);
    918        flags.set(
    919            GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
    920            default == color_interpolation_method,
    921        );
    922 
    923        Ok(Gradient::Radial {
    924            shape,
    925            position,
    926            color_interpolation_method,
    927            items,
    928            flags,
    929            compat_mode,
    930        })
    931    }
    932 
    933    /// Parse a conic gradient.
    934    fn parse_conic<'i, 't>(
    935        context: &ParserContext,
    936        input: &mut Parser<'i, 't>,
    937        repeating: bool,
    938    ) -> Result<Self, ParseError<'i>> {
    939        let mut flags = GradientFlags::empty();
    940        flags.set(GradientFlags::REPEATING, repeating);
    941 
    942        let mut color_interpolation_method =
    943            Self::try_parse_color_interpolation_method(context, input);
    944 
    945        let angle = input.try_parse(|i| {
    946            i.expect_ident_matching("from")?;
    947            // Spec allows unitless zero start angles
    948            // https://drafts.csswg.org/css-images-4/#valdef-conic-gradient-angle
    949            Angle::parse_with_unitless(context, i)
    950        });
    951        let position = input.try_parse(|i| {
    952            i.expect_ident_matching("at")?;
    953            Position::parse(context, i)
    954        });
    955 
    956        let has_angle_or_position = angle.is_ok() || position.is_ok();
    957        if has_angle_or_position && color_interpolation_method.is_none() {
    958            color_interpolation_method = Self::try_parse_color_interpolation_method(context, input);
    959        }
    960 
    961        if has_angle_or_position || color_interpolation_method.is_some() {
    962            input.expect_comma()?;
    963        }
    964 
    965        let angle = angle.unwrap_or(Angle::zero());
    966 
    967        let position = position.unwrap_or(Position::center());
    968 
    969        let items = generic::GradientItem::parse_comma_separated(
    970            context,
    971            input,
    972            AngleOrPercentage::parse_with_unitless,
    973        )?;
    974 
    975        if items.is_empty() {
    976            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
    977        }
    978 
    979        let default = default_color_interpolation_method(&items);
    980        let color_interpolation_method = color_interpolation_method.unwrap_or(default);
    981        flags.set(
    982            GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
    983            default == color_interpolation_method,
    984        );
    985 
    986        Ok(Gradient::Conic {
    987            angle,
    988            position,
    989            color_interpolation_method,
    990            items,
    991            flags,
    992        })
    993    }
    994 }
    995 
    996 impl generic::LineDirection for LineDirection {
    997    fn points_downwards(&self, compat_mode: GradientCompatMode) -> bool {
    998        match *self {
    999            LineDirection::Angle(ref angle) => angle.degrees() == 180.0,
   1000            LineDirection::Vertical(VerticalPositionKeyword::Bottom) => {
   1001                compat_mode == GradientCompatMode::Modern
   1002            },
   1003            LineDirection::Vertical(VerticalPositionKeyword::Top) => {
   1004                compat_mode != GradientCompatMode::Modern
   1005            },
   1006            _ => false,
   1007        }
   1008    }
   1009 
   1010    fn to_css<W>(&self, dest: &mut CssWriter<W>, compat_mode: GradientCompatMode) -> fmt::Result
   1011    where
   1012        W: Write,
   1013    {
   1014        match *self {
   1015            LineDirection::Angle(angle) => angle.to_css(dest),
   1016            LineDirection::Horizontal(x) => {
   1017                if compat_mode == GradientCompatMode::Modern {
   1018                    dest.write_str("to ")?;
   1019                }
   1020                x.to_css(dest)
   1021            },
   1022            LineDirection::Vertical(y) => {
   1023                if compat_mode == GradientCompatMode::Modern {
   1024                    dest.write_str("to ")?;
   1025                }
   1026                y.to_css(dest)
   1027            },
   1028            LineDirection::Corner(x, y) => {
   1029                if compat_mode == GradientCompatMode::Modern {
   1030                    dest.write_str("to ")?;
   1031                }
   1032                x.to_css(dest)?;
   1033                dest.write_char(' ')?;
   1034                y.to_css(dest)
   1035            },
   1036        }
   1037    }
   1038 }
   1039 
   1040 impl LineDirection {
   1041    fn parse<'i, 't>(
   1042        context: &ParserContext,
   1043        input: &mut Parser<'i, 't>,
   1044        compat_mode: &mut GradientCompatMode,
   1045    ) -> Result<Self, ParseError<'i>> {
   1046        // Gradients allow unitless zero angles as an exception, see:
   1047        // https://github.com/w3c/csswg-drafts/issues/1162
   1048        if let Ok(angle) = input.try_parse(|i| Angle::parse_with_unitless(context, i)) {
   1049            return Ok(LineDirection::Angle(angle));
   1050        }
   1051 
   1052        input.try_parse(|i| {
   1053            let to_ident = i.try_parse(|i| i.expect_ident_matching("to"));
   1054            match *compat_mode {
   1055                // `to` keyword is mandatory in modern syntax.
   1056                GradientCompatMode::Modern => to_ident?,
   1057                // Fall back to Modern compatibility mode in case there is a `to` keyword.
   1058                // According to Gecko, `-moz-linear-gradient(to ...)` should serialize like
   1059                // `linear-gradient(to ...)`.
   1060                GradientCompatMode::Moz if to_ident.is_ok() => {
   1061                    *compat_mode = GradientCompatMode::Modern
   1062                },
   1063                // There is no `to` keyword in webkit prefixed syntax. If it's consumed,
   1064                // parsing should throw an error.
   1065                GradientCompatMode::WebKit if to_ident.is_ok() => {
   1066                    return Err(
   1067                        i.new_custom_error(SelectorParseErrorKind::UnexpectedIdent("to".into()))
   1068                    );
   1069                },
   1070                _ => {},
   1071            }
   1072 
   1073            if let Ok(x) = i.try_parse(HorizontalPositionKeyword::parse) {
   1074                if let Ok(y) = i.try_parse(VerticalPositionKeyword::parse) {
   1075                    return Ok(LineDirection::Corner(x, y));
   1076                }
   1077                return Ok(LineDirection::Horizontal(x));
   1078            }
   1079            let y = VerticalPositionKeyword::parse(i)?;
   1080            if let Ok(x) = i.try_parse(HorizontalPositionKeyword::parse) {
   1081                return Ok(LineDirection::Corner(x, y));
   1082            }
   1083            Ok(LineDirection::Vertical(y))
   1084        })
   1085    }
   1086 }
   1087 
   1088 impl EndingShape {
   1089    fn parse<'i, 't>(
   1090        context: &ParserContext,
   1091        input: &mut Parser<'i, 't>,
   1092        compat_mode: GradientCompatMode,
   1093    ) -> Result<Self, ParseError<'i>> {
   1094        if let Ok(extent) = input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode))
   1095        {
   1096            if input
   1097                .try_parse(|i| i.expect_ident_matching("circle"))
   1098                .is_ok()
   1099            {
   1100                return Ok(generic::EndingShape::Circle(Circle::Extent(extent)));
   1101            }
   1102            let _ = input.try_parse(|i| i.expect_ident_matching("ellipse"));
   1103            return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(extent)));
   1104        }
   1105        if input
   1106            .try_parse(|i| i.expect_ident_matching("circle"))
   1107            .is_ok()
   1108        {
   1109            if let Ok(extent) =
   1110                input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode))
   1111            {
   1112                return Ok(generic::EndingShape::Circle(Circle::Extent(extent)));
   1113            }
   1114            if compat_mode == GradientCompatMode::Modern {
   1115                if let Ok(length) = input.try_parse(|i| NonNegativeLength::parse(context, i)) {
   1116                    return Ok(generic::EndingShape::Circle(Circle::Radius(length)));
   1117                }
   1118            }
   1119            return Ok(generic::EndingShape::Circle(Circle::Extent(
   1120                ShapeExtent::FarthestCorner,
   1121            )));
   1122        }
   1123        if input
   1124            .try_parse(|i| i.expect_ident_matching("ellipse"))
   1125            .is_ok()
   1126        {
   1127            if let Ok(extent) =
   1128                input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode))
   1129            {
   1130                return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(extent)));
   1131            }
   1132            if compat_mode == GradientCompatMode::Modern {
   1133                let pair: Result<_, ParseError> = input.try_parse(|i| {
   1134                    let x = NonNegativeLengthPercentage::parse(context, i)?;
   1135                    let y = NonNegativeLengthPercentage::parse(context, i)?;
   1136                    Ok((x, y))
   1137                });
   1138                if let Ok((x, y)) = pair {
   1139                    return Ok(generic::EndingShape::Ellipse(Ellipse::Radii(x, y)));
   1140                }
   1141            }
   1142            return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(
   1143                ShapeExtent::FarthestCorner,
   1144            )));
   1145        }
   1146        if let Ok(length) = input.try_parse(|i| NonNegativeLength::parse(context, i)) {
   1147            if let Ok(y) = input.try_parse(|i| NonNegativeLengthPercentage::parse(context, i)) {
   1148                if compat_mode == GradientCompatMode::Modern {
   1149                    let _ = input.try_parse(|i| i.expect_ident_matching("ellipse"));
   1150                }
   1151                return Ok(generic::EndingShape::Ellipse(Ellipse::Radii(
   1152                    NonNegative(LengthPercentage::from(length.0)),
   1153                    y,
   1154                )));
   1155            }
   1156            if compat_mode == GradientCompatMode::Modern {
   1157                let y = input.try_parse(|i| {
   1158                    i.expect_ident_matching("ellipse")?;
   1159                    NonNegativeLengthPercentage::parse(context, i)
   1160                });
   1161                if let Ok(y) = y {
   1162                    return Ok(generic::EndingShape::Ellipse(Ellipse::Radii(
   1163                        NonNegative(LengthPercentage::from(length.0)),
   1164                        y,
   1165                    )));
   1166                }
   1167                let _ = input.try_parse(|i| i.expect_ident_matching("circle"));
   1168            }
   1169 
   1170            return Ok(generic::EndingShape::Circle(Circle::Radius(length)));
   1171        }
   1172        input.try_parse(|i| {
   1173            let x = Percentage::parse_non_negative(context, i)?;
   1174            let y = if let Ok(y) = i.try_parse(|i| NonNegativeLengthPercentage::parse(context, i)) {
   1175                if compat_mode == GradientCompatMode::Modern {
   1176                    let _ = i.try_parse(|i| i.expect_ident_matching("ellipse"));
   1177                }
   1178                y
   1179            } else {
   1180                if compat_mode == GradientCompatMode::Modern {
   1181                    i.expect_ident_matching("ellipse")?;
   1182                }
   1183                NonNegativeLengthPercentage::parse(context, i)?
   1184            };
   1185            Ok(generic::EndingShape::Ellipse(Ellipse::Radii(
   1186                NonNegative(LengthPercentage::from(x)),
   1187                y,
   1188            )))
   1189        })
   1190    }
   1191 }
   1192 
   1193 impl ShapeExtent {
   1194    fn parse_with_compat_mode<'i, 't>(
   1195        input: &mut Parser<'i, 't>,
   1196        compat_mode: GradientCompatMode,
   1197    ) -> Result<Self, ParseError<'i>> {
   1198        match Self::parse(input)? {
   1199            ShapeExtent::Contain | ShapeExtent::Cover
   1200                if compat_mode == GradientCompatMode::Modern =>
   1201            {
   1202                Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
   1203            },
   1204            ShapeExtent::Contain => Ok(ShapeExtent::ClosestSide),
   1205            ShapeExtent::Cover => Ok(ShapeExtent::FarthestCorner),
   1206            keyword => Ok(keyword),
   1207        }
   1208    }
   1209 }
   1210 
   1211 impl<T> generic::GradientItem<Color, T> {
   1212    fn parse_comma_separated<'i, 't>(
   1213        context: &ParserContext,
   1214        input: &mut Parser<'i, 't>,
   1215        parse_position: impl for<'i1, 't1> Fn(&ParserContext, &mut Parser<'i1, 't1>) -> Result<T, ParseError<'i1>>
   1216            + Copy,
   1217    ) -> Result<crate::OwnedSlice<Self>, ParseError<'i>> {
   1218        let mut items = Vec::new();
   1219        let mut seen_stop = false;
   1220 
   1221        loop {
   1222            input.parse_until_before(Delimiter::Comma, |input| {
   1223                if seen_stop {
   1224                    if let Ok(hint) = input.try_parse(|i| parse_position(context, i)) {
   1225                        seen_stop = false;
   1226                        items.push(generic::GradientItem::InterpolationHint(hint));
   1227                        return Ok(());
   1228                    }
   1229                }
   1230 
   1231                let stop = generic::ColorStop::parse(context, input, parse_position)?;
   1232 
   1233                if let Ok(multi_position) = input.try_parse(|i| parse_position(context, i)) {
   1234                    let stop_color = stop.color.clone();
   1235                    items.push(stop.into_item());
   1236                    items.push(
   1237                        generic::ColorStop {
   1238                            color: stop_color,
   1239                            position: Some(multi_position),
   1240                        }
   1241                        .into_item(),
   1242                    );
   1243                } else {
   1244                    items.push(stop.into_item());
   1245                }
   1246 
   1247                seen_stop = true;
   1248                Ok(())
   1249            })?;
   1250 
   1251            match input.next() {
   1252                Err(_) => break,
   1253                Ok(&Token::Comma) => continue,
   1254                Ok(_) => unreachable!(),
   1255            }
   1256        }
   1257 
   1258        if !seen_stop || items.is_empty() {
   1259            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
   1260        }
   1261        Ok(items.into())
   1262    }
   1263 }
   1264 
   1265 impl<T> generic::ColorStop<Color, T> {
   1266    fn parse<'i, 't>(
   1267        context: &ParserContext,
   1268        input: &mut Parser<'i, 't>,
   1269        parse_position: impl for<'i1, 't1> Fn(
   1270            &ParserContext,
   1271            &mut Parser<'i1, 't1>,
   1272        ) -> Result<T, ParseError<'i1>>,
   1273    ) -> Result<Self, ParseError<'i>> {
   1274        Ok(generic::ColorStop {
   1275            color: Color::parse(context, input)?,
   1276            position: input.try_parse(|i| parse_position(context, i)).ok(),
   1277        })
   1278    }
   1279 }
   1280 
   1281 impl PaintWorklet {
   1282    #[cfg(feature = "servo")]
   1283    fn parse_args<'i>(
   1284        context: &ParserContext,
   1285        input: &mut Parser<'i, '_>,
   1286    ) -> Result<Self, ParseError<'i>> {
   1287        use crate::custom_properties::SpecifiedValue;
   1288        use servo_arc::Arc;
   1289        let name = Atom::from(&**input.expect_ident()?);
   1290        let arguments = input
   1291            .try_parse(|input| {
   1292                input.expect_comma()?;
   1293                input.parse_comma_separated(|input| {
   1294                    SpecifiedValue::parse(input, &context.url_data).map(Arc::new)
   1295                })
   1296            })
   1297            .unwrap_or_default();
   1298        Ok(Self { name, arguments })
   1299    }
   1300 }
   1301 
   1302 /// https://drafts.csswg.org/css-images/#propdef-image-rendering
   1303 #[allow(missing_docs)]
   1304 #[derive(
   1305    Clone,
   1306    Copy,
   1307    Debug,
   1308    Eq,
   1309    Hash,
   1310    MallocSizeOf,
   1311    Parse,
   1312    PartialEq,
   1313    SpecifiedValueInfo,
   1314    ToCss,
   1315    ToComputedValue,
   1316    ToResolvedValue,
   1317    ToShmem,
   1318    ToTyped,
   1319 )]
   1320 #[repr(u8)]
   1321 pub enum ImageRendering {
   1322    Auto,
   1323    #[cfg(feature = "gecko")]
   1324    Smooth,
   1325    #[parse(aliases = "-moz-crisp-edges")]
   1326    CrispEdges,
   1327    Pixelated,
   1328    // From the spec:
   1329    //
   1330    //     This property previously accepted the values optimizeSpeed and
   1331    //     optimizeQuality. These are now deprecated; a user agent must accept
   1332    //     them as valid values but must treat them as having the same behavior
   1333    //     as crisp-edges and smooth respectively, and authors must not use
   1334    //     them.
   1335    //
   1336    #[cfg(feature = "gecko")]
   1337    Optimizespeed,
   1338    #[cfg(feature = "gecko")]
   1339    Optimizequality,
   1340 }