/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! CSS handling for the specified value of //! [`image`][image]s //! //! [image]: https://drafts.csswg.org/css-images/#image-values use crate::color::mix::ColorInterpolationMethod; use crate::parser::{Parse, ParserContext}; use crate::stylesheets::CorsMode; use crate::values::generics::color::ColorMixFlags; use crate::values::generics::image::{ self as generic, Circle, Ellipse, GradientCompatMode, ShapeExtent, }; use crate::values::generics::image::{GradientFlags, PaintWorklet}; use crate::values::generics::position::Position as GenericPosition; use crate::values::generics::NonNegative; use crate::values::specified::position::{HorizontalPositionKeyword, VerticalPositionKeyword}; use crate::values::specified::position::{Position, PositionComponent, Side}; use crate::values::specified::url::SpecifiedImageUrl; use crate::values::specified::{ Angle, AngleOrPercentage, Color, Length, LengthPercentage, NonNegativeLength, NonNegativeLengthPercentage, Resolution, }; use crate::values::specified::{Number, NumberOrPercentage, Percentage}; use crate::Atom; use cssparser::{Delimiter, Parser, Token}; use selectors::parser::SelectorParseErrorKind; use std::cmp::Ordering; use std::fmt::{self, Write}; use style_traits::{CssType, CssWriter, KeywordsCollectFn, ParseError}; use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss}; #[inline] fn gradient_color_interpolation_method_enabled() -> bool { static_prefs::pref!("layout.css.gradient-color-interpolation-method.enabled") } /// Specified values for an image according to CSS-IMAGES. /// pub type Image = generic::Image; // Images should remain small, see https://github.com/servo/servo/pull/18430 size_of_test!(Image, 40); /// Specified values for a CSS gradient. /// pub type Gradient = generic::Gradient< LineDirection, LengthPercentage, NonNegativeLength, NonNegativeLengthPercentage, Position, Angle, AngleOrPercentage, Color, >; /// Specified values for CSS cross-fade /// cross-fade( CrossFadeElement, ...) /// pub type CrossFade = generic::CrossFade; /// CrossFadeElement = percent? CrossFadeImage pub type CrossFadeElement = generic::CrossFadeElement; /// CrossFadeImage = image | color pub type CrossFadeImage = generic::CrossFadeImage; /// `image-set()` pub type ImageSet = generic::ImageSet; /// Each of the arguments to `image-set()` pub type ImageSetItem = generic::ImageSetItem; type LengthPercentageItemList = crate::OwnedSlice>; impl Color { fn has_modern_syntax(&self) -> bool { match self { Self::Absolute(absolute) => !absolute.color.is_legacy_syntax(), Self::ColorMix(mix) => { if mix.flags.contains(ColorMixFlags::RESULT_IN_MODERN_SYNTAX) { true } else { mix.left.has_modern_syntax() || mix.right.has_modern_syntax() } }, Self::LightDark(ld) => ld.light.has_modern_syntax() || ld.dark.has_modern_syntax(), // The default is that this color doesn't have any modern syntax. _ => false, } } } fn default_color_interpolation_method( items: &[generic::GradientItem], ) -> ColorInterpolationMethod { let has_modern_syntax_item = items.iter().any(|item| match item { generic::GenericGradientItem::SimpleColorStop(color) => color.has_modern_syntax(), generic::GenericGradientItem::ComplexColorStop { color, .. } => color.has_modern_syntax(), generic::GenericGradientItem::InterpolationHint(_) => false, }); if has_modern_syntax_item { ColorInterpolationMethod::oklab() } else { ColorInterpolationMethod::srgb() } } #[cfg(feature = "gecko")] fn cross_fade_enabled() -> bool { static_prefs::pref!("layout.css.cross-fade.enabled") } #[cfg(feature = "servo")] fn cross_fade_enabled() -> bool { false } impl SpecifiedValueInfo for Gradient { const SUPPORTED_TYPES: u8 = CssType::GRADIENT; fn collect_completion_keywords(f: KeywordsCollectFn) { // This list here should keep sync with that in Gradient::parse. f(&[ "linear-gradient", "-webkit-linear-gradient", "-moz-linear-gradient", "repeating-linear-gradient", "-webkit-repeating-linear-gradient", "-moz-repeating-linear-gradient", "radial-gradient", "-webkit-radial-gradient", "-moz-radial-gradient", "repeating-radial-gradient", "-webkit-repeating-radial-gradient", "-moz-repeating-radial-gradient", "-webkit-gradient", "conic-gradient", "repeating-conic-gradient", ]); } } // Need to manually implement as whether or not cross-fade shows up in // completions & etc is dependent on it being enabled. impl SpecifiedValueInfo for generic::CrossFade { const SUPPORTED_TYPES: u8 = 0; fn collect_completion_keywords(f: KeywordsCollectFn) { if cross_fade_enabled() { f(&["cross-fade"]); } } } impl SpecifiedValueInfo for generic::ImageSet { const SUPPORTED_TYPES: u8 = 0; fn collect_completion_keywords(f: KeywordsCollectFn) { f(&["image-set"]); } } /// A specified gradient line direction. /// /// FIXME(emilio): This should be generic over Angle. #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] pub enum LineDirection { /// An angular direction. Angle(Angle), /// A horizontal direction. Horizontal(HorizontalPositionKeyword), /// A vertical direction. Vertical(VerticalPositionKeyword), /// A direction towards a corner of a box. Corner(HorizontalPositionKeyword, VerticalPositionKeyword), } /// A specified ending shape. pub type EndingShape = generic::EndingShape; bitflags! { #[derive(Clone, Copy)] struct ParseImageFlags: u8 { const FORBID_NONE = 1 << 0; const FORBID_IMAGE_SET = 1 << 1; const FORBID_NON_URL = 1 << 2; } } impl Parse for Image { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { Image::parse_with_cors_mode(context, input, CorsMode::None, ParseImageFlags::empty()) } } impl Image { fn parse_with_cors_mode<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, cors_mode: CorsMode, flags: ParseImageFlags, ) -> Result> { if !flags.contains(ParseImageFlags::FORBID_NONE) && input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { return Ok(generic::Image::None); } if let Ok(url) = input .try_parse(|input| SpecifiedImageUrl::parse_with_cors_mode(context, input, cors_mode)) { return Ok(generic::Image::Url(url)); } if !flags.contains(ParseImageFlags::FORBID_IMAGE_SET) { if let Ok(is) = input.try_parse(|input| ImageSet::parse(context, input, cors_mode, flags)) { return Ok(generic::Image::ImageSet(Box::new(is))); } } if flags.contains(ParseImageFlags::FORBID_NON_URL) { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } if let Ok(gradient) = input.try_parse(|i| Gradient::parse(context, i)) { return Ok(generic::Image::Gradient(Box::new(gradient))); } let function = input.expect_function()?.clone(); input.parse_nested_block(|input| { Ok(match_ignore_ascii_case! { &function, #[cfg(feature = "servo")] "paint" => Self::PaintWorklet(PaintWorklet::parse_args(context, input)?), "cross-fade" if cross_fade_enabled() => Self::CrossFade(Box::new(CrossFade::parse_args(context, input, cors_mode, flags)?)), #[cfg(feature = "gecko")] "-moz-element" => Self::Element(Self::parse_element(input)?), _ => return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedFunction(function))), }) }) } } impl Image { /// Creates an already specified image value from an already resolved URL /// for insertion in the cascade. #[cfg(feature = "servo")] pub fn for_cascade(url: ::servo_arc::Arc<::url::Url>) -> Self { use crate::values::CssUrl; generic::Image::Url(CssUrl::for_cascade(url)) } /// Parses a `-moz-element(# )`. #[cfg(feature = "gecko")] fn parse_element<'i>(input: &mut Parser<'i, '_>) -> Result> { let location = input.current_source_location(); Ok(match *input.next()? { Token::IDHash(ref id) => Atom::from(id.as_ref()), ref t => return Err(location.new_unexpected_token_error(t.clone())), }) } /// Provides an alternate method for parsing that associates the URL with /// anonymous CORS headers. pub fn parse_with_cors_anonymous<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { Self::parse_with_cors_mode( context, input, CorsMode::Anonymous, ParseImageFlags::empty(), ) } /// Provides an alternate method for parsing, but forbidding `none` pub fn parse_forbid_none<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { Self::parse_with_cors_mode(context, input, CorsMode::None, ParseImageFlags::FORBID_NONE) } /// Provides an alternate method for parsing, but only for urls. pub fn parse_only_url<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { Self::parse_with_cors_mode( context, input, CorsMode::None, ParseImageFlags::FORBID_NONE | ParseImageFlags::FORBID_NON_URL, ) } } impl CrossFade { /// cross-fade() = cross-fade( # ) fn parse_args<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, cors_mode: CorsMode, flags: ParseImageFlags, ) -> Result> { let elements = crate::OwnedSlice::from(input.parse_comma_separated(|input| { CrossFadeElement::parse(context, input, cors_mode, flags) })?); Ok(Self { elements }) } } impl CrossFadeElement { fn parse_percentage<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Option { // We clamp our values here as this is the way that Safari and Chrome's // implementation handle out-of-bounds percentages but whether or not // this behavior follows the specification is still being discussed. // See: input .try_parse(|input| Percentage::parse_non_negative(context, input)) .ok() .map(|p| p.clamp_to_hundred()) } /// = ? && [ | ] fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, cors_mode: CorsMode, flags: ParseImageFlags, ) -> Result> { // Try and parse a leading percent sign. let mut percent = Self::parse_percentage(context, input); // Parse the image let image = CrossFadeImage::parse(context, input, cors_mode, flags)?; // Try and parse a trailing percent sign. if percent.is_none() { percent = Self::parse_percentage(context, input); } Ok(Self { percent: percent.into(), image, }) } } impl CrossFadeImage { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, cors_mode: CorsMode, flags: ParseImageFlags, ) -> Result> { if let Ok(image) = input.try_parse(|input| { Image::parse_with_cors_mode( context, input, cors_mode, flags | ParseImageFlags::FORBID_NONE, ) }) { return Ok(Self::Image(image)); } Ok(Self::Color(Color::parse(context, input)?)) } } impl ImageSet { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, cors_mode: CorsMode, flags: ParseImageFlags, ) -> Result> { let function = input.expect_function()?; match_ignore_ascii_case! { &function, "-webkit-image-set" | "image-set" => {}, _ => { let func = function.clone(); return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedFunction(func))); } } let items = input.parse_nested_block(|input| { input.parse_comma_separated(|input| { ImageSetItem::parse(context, input, cors_mode, flags) }) })?; Ok(Self { selected_index: std::usize::MAX, items: items.into(), }) } } impl ImageSetItem { fn parse_type<'i>(p: &mut Parser<'i, '_>) -> Result> { p.expect_function_matching("type")?; p.parse_nested_block(|input| Ok(input.expect_string()?.as_ref().to_owned().into())) } fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, cors_mode: CorsMode, flags: ParseImageFlags, ) -> Result> { let image = match input.try_parse(|i| i.expect_url_or_string()) { Ok(url) => Image::Url(SpecifiedImageUrl::parse_from_string( url.as_ref().into(), context, cors_mode, )), Err(..) => Image::parse_with_cors_mode( context, input, cors_mode, flags | ParseImageFlags::FORBID_NONE | ParseImageFlags::FORBID_IMAGE_SET, )?, }; let mut resolution = input .try_parse(|input| Resolution::parse(context, input)) .ok(); let mime_type = input.try_parse(Self::parse_type).ok(); // Try to parse resolution after type(). if mime_type.is_some() && resolution.is_none() { resolution = input .try_parse(|input| Resolution::parse(context, input)) .ok(); } let resolution = resolution.unwrap_or_else(|| Resolution::from_x(1.0)); let has_mime_type = mime_type.is_some(); let mime_type = mime_type.unwrap_or_default(); Ok(Self { image, resolution, has_mime_type, mime_type, }) } } impl Parse for Gradient { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { enum Shape { Linear, Radial, Conic, } let func = input.expect_function()?; let (shape, repeating, compat_mode) = match_ignore_ascii_case! { &func, "linear-gradient" => { (Shape::Linear, false, GradientCompatMode::Modern) }, "-webkit-linear-gradient" => { (Shape::Linear, false, GradientCompatMode::WebKit) }, #[cfg(feature = "gecko")] "-moz-linear-gradient" => { (Shape::Linear, false, GradientCompatMode::Moz) }, "repeating-linear-gradient" => { (Shape::Linear, true, GradientCompatMode::Modern) }, "-webkit-repeating-linear-gradient" => { (Shape::Linear, true, GradientCompatMode::WebKit) }, #[cfg(feature = "gecko")] "-moz-repeating-linear-gradient" => { (Shape::Linear, true, GradientCompatMode::Moz) }, "radial-gradient" => { (Shape::Radial, false, GradientCompatMode::Modern) }, "-webkit-radial-gradient" => { (Shape::Radial, false, GradientCompatMode::WebKit) }, #[cfg(feature = "gecko")] "-moz-radial-gradient" => { (Shape::Radial, false, GradientCompatMode::Moz) }, "repeating-radial-gradient" => { (Shape::Radial, true, GradientCompatMode::Modern) }, "-webkit-repeating-radial-gradient" => { (Shape::Radial, true, GradientCompatMode::WebKit) }, #[cfg(feature = "gecko")] "-moz-repeating-radial-gradient" => { (Shape::Radial, true, GradientCompatMode::Moz) }, "conic-gradient" => { (Shape::Conic, false, GradientCompatMode::Modern) }, "repeating-conic-gradient" => { (Shape::Conic, true, GradientCompatMode::Modern) }, "-webkit-gradient" => { return input.parse_nested_block(|i| { Self::parse_webkit_gradient_argument(context, i) }); }, _ => { let func = func.clone(); return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedFunction(func))); } }; Ok(input.parse_nested_block(|i| { Ok(match shape { Shape::Linear => Self::parse_linear(context, i, repeating, compat_mode)?, Shape::Radial => Self::parse_radial(context, i, repeating, compat_mode)?, Shape::Conic => Self::parse_conic(context, i, repeating)?, }) })?) } } impl Gradient { fn parse_webkit_gradient_argument<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { use crate::values::specified::position::{ HorizontalPositionKeyword as X, VerticalPositionKeyword as Y, }; type Point = GenericPosition, Component>; #[derive(Clone, Copy, Parse)] enum Component { Center, Number(NumberOrPercentage), Side(S), } fn line_direction_from_points(first: Point, second: Point) -> LineDirection { let h_ord = first.horizontal.partial_cmp(&second.horizontal); let v_ord = first.vertical.partial_cmp(&second.vertical); let (h, v) = match (h_ord, v_ord) { (Some(h), Some(v)) => (h, v), _ => return LineDirection::Vertical(Y::Bottom), }; match (h, v) { (Ordering::Less, Ordering::Less) => LineDirection::Corner(X::Right, Y::Bottom), (Ordering::Less, Ordering::Equal) => LineDirection::Horizontal(X::Right), (Ordering::Less, Ordering::Greater) => LineDirection::Corner(X::Right, Y::Top), (Ordering::Equal, Ordering::Greater) => LineDirection::Vertical(Y::Top), (Ordering::Equal, Ordering::Equal) | (Ordering::Equal, Ordering::Less) => { LineDirection::Vertical(Y::Bottom) }, (Ordering::Greater, Ordering::Less) => LineDirection::Corner(X::Left, Y::Bottom), (Ordering::Greater, Ordering::Equal) => LineDirection::Horizontal(X::Left), (Ordering::Greater, Ordering::Greater) => LineDirection::Corner(X::Left, Y::Top), } } impl Parse for Point { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { input.try_parse(|i| { let x = Component::parse(context, i)?; let y = Component::parse(context, i)?; Ok(Self::new(x, y)) }) } } impl Into for Component { fn into(self) -> NumberOrPercentage { match self { Component::Center => NumberOrPercentage::Percentage(Percentage::new(0.5)), Component::Number(number) => number, Component::Side(side) => { let p = if side.is_start() { Percentage::zero() } else { Percentage::hundred() }; NumberOrPercentage::Percentage(p) }, } } } impl Into> for Component { fn into(self) -> PositionComponent { match self { Component::Center => PositionComponent::Center, Component::Number(NumberOrPercentage::Number(number)) => { PositionComponent::Length(Length::from_px(number.value).into()) }, Component::Number(NumberOrPercentage::Percentage(p)) => { PositionComponent::Length(p.into()) }, Component::Side(side) => PositionComponent::Side(side, None), } } } impl Component { fn partial_cmp(&self, other: &Self) -> Option { match ((*self).into(), (*other).into()) { (NumberOrPercentage::Percentage(a), NumberOrPercentage::Percentage(b)) => { a.get().partial_cmp(&b.get()) }, (NumberOrPercentage::Number(a), NumberOrPercentage::Number(b)) => { a.value.partial_cmp(&b.value) }, (_, _) => None, } } } let ident = input.expect_ident_cloned()?; input.expect_comma()?; Ok(match_ignore_ascii_case! { &ident, "linear" => { let first = Point::parse(context, input)?; input.expect_comma()?; let second = Point::parse(context, input)?; let direction = line_direction_from_points(first, second); let items = Gradient::parse_webkit_gradient_stops(context, input, false)?; generic::Gradient::Linear { direction, color_interpolation_method: ColorInterpolationMethod::srgb(), items, // Legacy gradients always use srgb as a default. flags: generic::GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD, compat_mode: GradientCompatMode::Modern, } }, "radial" => { let first_point = Point::parse(context, input)?; input.expect_comma()?; let first_radius = Number::parse_non_negative(context, input)?; input.expect_comma()?; let second_point = Point::parse(context, input)?; input.expect_comma()?; let second_radius = Number::parse_non_negative(context, input)?; let (reverse_stops, point, radius) = if second_radius.value >= first_radius.value { (false, second_point, second_radius) } else { (true, first_point, first_radius) }; let rad = Circle::Radius(NonNegative(Length::from_px(radius.value))); let shape = generic::EndingShape::Circle(rad); let position = Position::new(point.horizontal.into(), point.vertical.into()); let items = Gradient::parse_webkit_gradient_stops(context, input, reverse_stops)?; generic::Gradient::Radial { shape, position, color_interpolation_method: ColorInterpolationMethod::srgb(), items, // Legacy gradients always use srgb as a default. flags: generic::GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD, compat_mode: GradientCompatMode::Modern, } }, _ => { let e = SelectorParseErrorKind::UnexpectedIdent(ident.clone()); return Err(input.new_custom_error(e)); }, }) } fn parse_webkit_gradient_stops<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, reverse_stops: bool, ) -> Result> { let mut items = input .try_parse(|i| { i.expect_comma()?; i.parse_comma_separated(|i| { let function = i.expect_function()?.clone(); let (color, mut p) = i.parse_nested_block(|i| { let p = match_ignore_ascii_case! { &function, "color-stop" => { let p = NumberOrPercentage::parse(context, i)?.to_percentage(); i.expect_comma()?; p }, "from" => Percentage::zero(), "to" => Percentage::hundred(), _ => { return Err(i.new_custom_error( StyleParseErrorKind::UnexpectedFunction(function.clone()) )) }, }; let color = Color::parse(context, i)?; if color == Color::CurrentColor { return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } Ok((color.into(), p)) })?; if reverse_stops { p.reverse(); } Ok(generic::GradientItem::ComplexColorStop { color, position: p.into(), }) }) }) .unwrap_or(vec![]); if items.is_empty() { items = vec![ generic::GradientItem::ComplexColorStop { color: Color::transparent(), position: LengthPercentage::zero_percent(), }, generic::GradientItem::ComplexColorStop { color: Color::transparent(), position: LengthPercentage::hundred_percent(), }, ]; } else if items.len() == 1 { let first = items[0].clone(); items.push(first); } else { items.sort_by(|a, b| { match (a, b) { ( &generic::GradientItem::ComplexColorStop { position: ref a_position, .. }, &generic::GradientItem::ComplexColorStop { position: ref b_position, .. }, ) => match (a_position, b_position) { (&LengthPercentage::Percentage(a), &LengthPercentage::Percentage(b)) => { return a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal); }, _ => {}, }, _ => {}, } if reverse_stops { Ordering::Greater } else { Ordering::Less } }) } Ok(items.into()) } /// Not used for -webkit-gradient syntax and conic-gradient fn parse_stops<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let items = generic::GradientItem::parse_comma_separated(context, input, LengthPercentage::parse)?; if items.len() < 2 { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } Ok(items) } /// Try to parse a color interpolation method. fn try_parse_color_interpolation_method<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Option { if gradient_color_interpolation_method_enabled() { input .try_parse(|i| ColorInterpolationMethod::parse(context, i)) .ok() } else { None } } /// Parses a linear gradient. /// GradientCompatMode can change during `-moz-` prefixed gradient parsing if it come across a `to` keyword. fn parse_linear<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, repeating: bool, mut compat_mode: GradientCompatMode, ) -> Result> { let mut flags = GradientFlags::empty(); flags.set(GradientFlags::REPEATING, repeating); let mut color_interpolation_method = Self::try_parse_color_interpolation_method(context, input); let direction = input .try_parse(|p| LineDirection::parse(context, p, &mut compat_mode)) .ok(); if direction.is_some() && color_interpolation_method.is_none() { color_interpolation_method = Self::try_parse_color_interpolation_method(context, input); } // If either of the 2 options were specified, we require a comma. if color_interpolation_method.is_some() || direction.is_some() { input.expect_comma()?; } let items = Gradient::parse_stops(context, input)?; let default = default_color_interpolation_method(&items); let color_interpolation_method = color_interpolation_method.unwrap_or(default); flags.set( GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD, default == color_interpolation_method, ); let direction = direction.unwrap_or(match compat_mode { GradientCompatMode::Modern => LineDirection::Vertical(VerticalPositionKeyword::Bottom), _ => LineDirection::Vertical(VerticalPositionKeyword::Top), }); Ok(Gradient::Linear { direction, color_interpolation_method, items, flags, compat_mode, }) } /// Parses a radial gradient. fn parse_radial<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, repeating: bool, compat_mode: GradientCompatMode, ) -> Result> { let mut flags = GradientFlags::empty(); flags.set(GradientFlags::REPEATING, repeating); let mut color_interpolation_method = Self::try_parse_color_interpolation_method(context, input); let (shape, position) = match compat_mode { GradientCompatMode::Modern => { let shape = input.try_parse(|i| EndingShape::parse(context, i, compat_mode)); let position = input.try_parse(|i| { i.expect_ident_matching("at")?; Position::parse(context, i) }); (shape, position.ok()) }, _ => { let position = input.try_parse(|i| Position::parse(context, i)); let shape = input.try_parse(|i| { if position.is_ok() { i.expect_comma()?; } EndingShape::parse(context, i, compat_mode) }); (shape, position.ok()) }, }; let has_shape_or_position = shape.is_ok() || position.is_some(); if has_shape_or_position && color_interpolation_method.is_none() { color_interpolation_method = Self::try_parse_color_interpolation_method(context, input); } if has_shape_or_position || color_interpolation_method.is_some() { input.expect_comma()?; } let shape = shape.unwrap_or({ generic::EndingShape::Ellipse(Ellipse::Extent(ShapeExtent::FarthestCorner)) }); let position = position.unwrap_or(Position::center()); let items = Gradient::parse_stops(context, input)?; let default = default_color_interpolation_method(&items); let color_interpolation_method = color_interpolation_method.unwrap_or(default); flags.set( GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD, default == color_interpolation_method, ); Ok(Gradient::Radial { shape, position, color_interpolation_method, items, flags, compat_mode, }) } /// Parse a conic gradient. fn parse_conic<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, repeating: bool, ) -> Result> { let mut flags = GradientFlags::empty(); flags.set(GradientFlags::REPEATING, repeating); let mut color_interpolation_method = Self::try_parse_color_interpolation_method(context, input); let angle = input.try_parse(|i| { i.expect_ident_matching("from")?; // Spec allows unitless zero start angles // https://drafts.csswg.org/css-images-4/#valdef-conic-gradient-angle Angle::parse_with_unitless(context, i) }); let position = input.try_parse(|i| { i.expect_ident_matching("at")?; Position::parse(context, i) }); let has_angle_or_position = angle.is_ok() || position.is_ok(); if has_angle_or_position && color_interpolation_method.is_none() { color_interpolation_method = Self::try_parse_color_interpolation_method(context, input); } if has_angle_or_position || color_interpolation_method.is_some() { input.expect_comma()?; } let angle = angle.unwrap_or(Angle::zero()); let position = position.unwrap_or(Position::center()); let items = generic::GradientItem::parse_comma_separated( context, input, AngleOrPercentage::parse_with_unitless, )?; if items.len() < 2 { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } let default = default_color_interpolation_method(&items); let color_interpolation_method = color_interpolation_method.unwrap_or(default); flags.set( GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD, default == color_interpolation_method, ); Ok(Gradient::Conic { angle, position, color_interpolation_method, items, flags, }) } } impl generic::LineDirection for LineDirection { fn points_downwards(&self, compat_mode: GradientCompatMode) -> bool { match *self { LineDirection::Angle(ref angle) => angle.degrees() == 180.0, LineDirection::Vertical(VerticalPositionKeyword::Bottom) => { compat_mode == GradientCompatMode::Modern }, LineDirection::Vertical(VerticalPositionKeyword::Top) => { compat_mode != GradientCompatMode::Modern }, _ => false, } } fn to_css(&self, dest: &mut CssWriter, compat_mode: GradientCompatMode) -> fmt::Result where W: Write, { match *self { LineDirection::Angle(angle) => angle.to_css(dest), LineDirection::Horizontal(x) => { if compat_mode == GradientCompatMode::Modern { dest.write_str("to ")?; } x.to_css(dest) }, LineDirection::Vertical(y) => { if compat_mode == GradientCompatMode::Modern { dest.write_str("to ")?; } y.to_css(dest) }, LineDirection::Corner(x, y) => { if compat_mode == GradientCompatMode::Modern { dest.write_str("to ")?; } x.to_css(dest)?; dest.write_char(' ')?; y.to_css(dest) }, } } } impl LineDirection { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, compat_mode: &mut GradientCompatMode, ) -> Result> { // Gradients allow unitless zero angles as an exception, see: // https://github.com/w3c/csswg-drafts/issues/1162 if let Ok(angle) = input.try_parse(|i| Angle::parse_with_unitless(context, i)) { return Ok(LineDirection::Angle(angle)); } input.try_parse(|i| { let to_ident = i.try_parse(|i| i.expect_ident_matching("to")); match *compat_mode { // `to` keyword is mandatory in modern syntax. GradientCompatMode::Modern => to_ident?, // Fall back to Modern compatibility mode in case there is a `to` keyword. // According to Gecko, `-moz-linear-gradient(to ...)` should serialize like // `linear-gradient(to ...)`. GradientCompatMode::Moz if to_ident.is_ok() => { *compat_mode = GradientCompatMode::Modern }, // There is no `to` keyword in webkit prefixed syntax. If it's consumed, // parsing should throw an error. GradientCompatMode::WebKit if to_ident.is_ok() => { return Err( i.new_custom_error(SelectorParseErrorKind::UnexpectedIdent("to".into())) ); }, _ => {}, } if let Ok(x) = i.try_parse(HorizontalPositionKeyword::parse) { if let Ok(y) = i.try_parse(VerticalPositionKeyword::parse) { return Ok(LineDirection::Corner(x, y)); } return Ok(LineDirection::Horizontal(x)); } let y = VerticalPositionKeyword::parse(i)?; if let Ok(x) = i.try_parse(HorizontalPositionKeyword::parse) { return Ok(LineDirection::Corner(x, y)); } Ok(LineDirection::Vertical(y)) }) } } impl EndingShape { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, compat_mode: GradientCompatMode, ) -> Result> { if let Ok(extent) = input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode)) { if input .try_parse(|i| i.expect_ident_matching("circle")) .is_ok() { return Ok(generic::EndingShape::Circle(Circle::Extent(extent))); } let _ = input.try_parse(|i| i.expect_ident_matching("ellipse")); return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(extent))); } if input .try_parse(|i| i.expect_ident_matching("circle")) .is_ok() { if let Ok(extent) = input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode)) { return Ok(generic::EndingShape::Circle(Circle::Extent(extent))); } if compat_mode == GradientCompatMode::Modern { if let Ok(length) = input.try_parse(|i| NonNegativeLength::parse(context, i)) { return Ok(generic::EndingShape::Circle(Circle::Radius(length))); } } return Ok(generic::EndingShape::Circle(Circle::Extent( ShapeExtent::FarthestCorner, ))); } if input .try_parse(|i| i.expect_ident_matching("ellipse")) .is_ok() { if let Ok(extent) = input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode)) { return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(extent))); } if compat_mode == GradientCompatMode::Modern { let pair: Result<_, ParseError> = input.try_parse(|i| { let x = NonNegativeLengthPercentage::parse(context, i)?; let y = NonNegativeLengthPercentage::parse(context, i)?; Ok((x, y)) }); if let Ok((x, y)) = pair { return Ok(generic::EndingShape::Ellipse(Ellipse::Radii(x, y))); } } return Ok(generic::EndingShape::Ellipse(Ellipse::Extent( ShapeExtent::FarthestCorner, ))); } if let Ok(length) = input.try_parse(|i| NonNegativeLength::parse(context, i)) { if let Ok(y) = input.try_parse(|i| NonNegativeLengthPercentage::parse(context, i)) { if compat_mode == GradientCompatMode::Modern { let _ = input.try_parse(|i| i.expect_ident_matching("ellipse")); } return Ok(generic::EndingShape::Ellipse(Ellipse::Radii( NonNegative(LengthPercentage::from(length.0)), y, ))); } if compat_mode == GradientCompatMode::Modern { let y = input.try_parse(|i| { i.expect_ident_matching("ellipse")?; NonNegativeLengthPercentage::parse(context, i) }); if let Ok(y) = y { return Ok(generic::EndingShape::Ellipse(Ellipse::Radii( NonNegative(LengthPercentage::from(length.0)), y, ))); } let _ = input.try_parse(|i| i.expect_ident_matching("circle")); } return Ok(generic::EndingShape::Circle(Circle::Radius(length))); } input.try_parse(|i| { let x = Percentage::parse_non_negative(context, i)?; let y = if let Ok(y) = i.try_parse(|i| NonNegativeLengthPercentage::parse(context, i)) { if compat_mode == GradientCompatMode::Modern { let _ = i.try_parse(|i| i.expect_ident_matching("ellipse")); } y } else { if compat_mode == GradientCompatMode::Modern { i.expect_ident_matching("ellipse")?; } NonNegativeLengthPercentage::parse(context, i)? }; Ok(generic::EndingShape::Ellipse(Ellipse::Radii( NonNegative(LengthPercentage::from(x)), y, ))) }) } } impl ShapeExtent { fn parse_with_compat_mode<'i, 't>( input: &mut Parser<'i, 't>, compat_mode: GradientCompatMode, ) -> Result> { match Self::parse(input)? { ShapeExtent::Contain | ShapeExtent::Cover if compat_mode == GradientCompatMode::Modern => { Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) }, ShapeExtent::Contain => Ok(ShapeExtent::ClosestSide), ShapeExtent::Cover => Ok(ShapeExtent::FarthestCorner), keyword => Ok(keyword), } } } impl generic::GradientItem { fn parse_comma_separated<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, parse_position: impl for<'i1, 't1> Fn(&ParserContext, &mut Parser<'i1, 't1>) -> Result> + Copy, ) -> Result, ParseError<'i>> { let mut items = Vec::new(); let mut seen_stop = false; loop { input.parse_until_before(Delimiter::Comma, |input| { if seen_stop { if let Ok(hint) = input.try_parse(|i| parse_position(context, i)) { seen_stop = false; items.push(generic::GradientItem::InterpolationHint(hint)); return Ok(()); } } let stop = generic::ColorStop::parse(context, input, parse_position)?; if let Ok(multi_position) = input.try_parse(|i| parse_position(context, i)) { let stop_color = stop.color.clone(); items.push(stop.into_item()); items.push( generic::ColorStop { color: stop_color, position: Some(multi_position), } .into_item(), ); } else { items.push(stop.into_item()); } seen_stop = true; Ok(()) })?; match input.next() { Err(_) => break, Ok(&Token::Comma) => continue, Ok(_) => unreachable!(), } } if !seen_stop || items.len() < 2 { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } Ok(items.into()) } } impl generic::ColorStop { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, parse_position: impl for<'i1, 't1> Fn( &ParserContext, &mut Parser<'i1, 't1>, ) -> Result>, ) -> Result> { Ok(generic::ColorStop { color: Color::parse(context, input)?, position: input.try_parse(|i| parse_position(context, i)).ok(), }) } } impl PaintWorklet { #[cfg(feature = "servo")] fn parse_args<'i>(context: &ParserContext, input: &mut Parser<'i, '_>) -> Result> { use crate::custom_properties::SpecifiedValue; let name = Atom::from(&**input.expect_ident()?); let arguments = input .try_parse(|input| { input.expect_comma()?; input.parse_comma_separated(|input| SpecifiedValue::parse(input, &context.url_data)) }) .unwrap_or_default(); Ok(Self { name, arguments }) } } /// https://drafts.csswg.org/css-images/#propdef-image-rendering #[allow(missing_docs)] #[derive( Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToComputedValue, ToResolvedValue, ToShmem, )] #[repr(u8)] pub enum ImageRendering { Auto, #[cfg(feature = "gecko")] Smooth, #[parse(aliases = "-moz-crisp-edges")] CrispEdges, Pixelated, // From the spec: // // This property previously accepted the values optimizeSpeed and // optimizeQuality. These are now deprecated; a user agent must accept // them as valid values but must treat them as having the same behavior // as crisp-edges and smooth respectively, and authors must not use // them. // #[cfg(feature = "gecko")] Optimizespeed, #[cfg(feature = "gecko")] Optimizequality, }