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