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