Skip to main content

style/values/specified/
image.rs

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