1336 lines
48 KiB
Rust
1336 lines
48 KiB
Rust
/* 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, GenericLightDark};
|
|
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::SpecifiedUrl;
|
|
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.
|
|
/// <https://drafts.csswg.org/css-images/#image-values>
|
|
pub type Image = generic::Image<Gradient, SpecifiedUrl, Color, Percentage, Resolution>;
|
|
|
|
// Images should remain small, see https://github.com/servo/servo/pull/18430
|
|
#[cfg(feature = "gecko")]
|
|
size_of_test!(Image, 16);
|
|
#[cfg(feature = "servo")]
|
|
size_of_test!(Image, 40);
|
|
|
|
/// Specified values for a CSS gradient.
|
|
/// <https://drafts.csswg.org/css-images/#gradients>
|
|
pub type Gradient = generic::Gradient<
|
|
LineDirection,
|
|
LengthPercentage,
|
|
NonNegativeLength,
|
|
NonNegativeLengthPercentage,
|
|
Position,
|
|
Angle,
|
|
AngleOrPercentage,
|
|
Color,
|
|
>;
|
|
|
|
/// Specified values for CSS cross-fade
|
|
/// cross-fade( CrossFadeElement, ...)
|
|
/// <https://drafts.csswg.org/css-images-4/#cross-fade-function>
|
|
pub type CrossFade = generic::CrossFade<Image, Color, Percentage>;
|
|
/// CrossFadeElement = percent? CrossFadeImage
|
|
pub type CrossFadeElement = generic::CrossFadeElement<Image, Color, Percentage>;
|
|
/// CrossFadeImage = image | color
|
|
pub type CrossFadeImage = generic::CrossFadeImage<Image, Color>;
|
|
|
|
/// `image-set()`
|
|
pub type ImageSet = generic::ImageSet<Image, Resolution>;
|
|
|
|
/// Each of the arguments to `image-set()`
|
|
pub type ImageSetItem = generic::ImageSetItem<Image, Resolution>;
|
|
|
|
type LengthPercentageItemList = crate::OwnedSlice<generic::GradientItem<Color, LengthPercentage>>;
|
|
|
|
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<T>(
|
|
items: &[generic::GradientItem<Color, T>],
|
|
) -> 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()
|
|
}
|
|
}
|
|
|
|
fn image_light_dark_enabled(context: &ParserContext) -> bool {
|
|
context.chrome_rules_enabled() || static_prefs::pref!("layout.css.light-dark.images.enabled")
|
|
}
|
|
|
|
#[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<Image, Color, Percentage> SpecifiedValueInfo for generic::CrossFade<Image, Color, Percentage> {
|
|
const SUPPORTED_TYPES: u8 = 0;
|
|
|
|
fn collect_completion_keywords(f: KeywordsCollectFn) {
|
|
if cross_fade_enabled() {
|
|
f(&["cross-fade"]);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<Image, Resolution> SpecifiedValueInfo for generic::ImageSet<Image, Resolution> {
|
|
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<NonNegativeLength, NonNegativeLengthPercentage>;
|
|
|
|
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, ParseError<'i>> {
|
|
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<Image, ParseError<'i>> {
|
|
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| SpecifiedUrl::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)?)),
|
|
"light-dark" if image_light_dark_enabled(context) => Self::LightDark(Box::new(GenericLightDark::parse_args_with(input, |input| {
|
|
Self::parse_with_cors_mode(context, input, cors_mode, flags)
|
|
})?)),
|
|
#[cfg(feature = "gecko")]
|
|
"-moz-element" => Self::Element(Self::parse_element(input)?),
|
|
"-moz-symbolic-icon" if context.chrome_rules_enabled() => Self::MozSymbolicIcon(input.expect_ident()?.as_ref().into()),
|
|
_ => 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(# <element-id>)`.
|
|
#[cfg(feature = "gecko")]
|
|
fn parse_element<'i>(input: &mut Parser<'i, '_>) -> Result<Atom, ParseError<'i>> {
|
|
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<Image, ParseError<'i>> {
|
|
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<Image, ParseError<'i>> {
|
|
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<Image, ParseError<'i>> {
|
|
Self::parse_with_cors_mode(
|
|
context,
|
|
input,
|
|
CorsMode::None,
|
|
ParseImageFlags::FORBID_NONE | ParseImageFlags::FORBID_NON_URL,
|
|
)
|
|
}
|
|
}
|
|
|
|
impl CrossFade {
|
|
/// cross-fade() = cross-fade( <cf-image># )
|
|
fn parse_args<'i, 't>(
|
|
context: &ParserContext,
|
|
input: &mut Parser<'i, 't>,
|
|
cors_mode: CorsMode,
|
|
flags: ParseImageFlags,
|
|
) -> Result<Self, ParseError<'i>> {
|
|
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<Percentage> {
|
|
// 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: <https://github.com/w3c/csswg-drafts/issues/5333>
|
|
input
|
|
.try_parse(|input| Percentage::parse_non_negative(context, input))
|
|
.ok()
|
|
.map(|p| p.clamp_to_hundred())
|
|
}
|
|
|
|
/// <cf-image> = <percentage>? && [ <image> | <color> ]
|
|
fn parse<'i, 't>(
|
|
context: &ParserContext,
|
|
input: &mut Parser<'i, 't>,
|
|
cors_mode: CorsMode,
|
|
flags: ParseImageFlags,
|
|
) -> Result<Self, ParseError<'i>> {
|
|
// 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<Self, ParseError<'i>> {
|
|
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<Self, ParseError<'i>> {
|
|
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<crate::OwnedStr, ParseError<'i>> {
|
|
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<Self, ParseError<'i>> {
|
|
let image = match input.try_parse(|i| i.expect_url_or_string()) {
|
|
Ok(url) => Image::Url(SpecifiedUrl::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<Self, ParseError<'i>> {
|
|
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<Self, ParseError<'i>> {
|
|
use crate::values::specified::position::{
|
|
HorizontalPositionKeyword as X, VerticalPositionKeyword as Y,
|
|
};
|
|
type Point = GenericPosition<Component<X>, Component<Y>>;
|
|
|
|
#[derive(Clone, Copy, Parse)]
|
|
enum Component<S> {
|
|
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<Self, ParseError<'i>> {
|
|
input.try_parse(|i| {
|
|
let x = Component::parse(context, i)?;
|
|
let y = Component::parse(context, i)?;
|
|
|
|
Ok(Self::new(x, y))
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<S: Side> Into<NumberOrPercentage> for Component<S> {
|
|
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<S: Side> Into<PositionComponent<S>> for Component<S> {
|
|
fn into(self) -> PositionComponent<S> {
|
|
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<S: Copy + Side> Component<S> {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
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<LengthPercentageItemList, ParseError<'i>> {
|
|
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<LengthPercentageItemList, ParseError<'i>> {
|
|
let items =
|
|
generic::GradientItem::parse_comma_separated(context, input, LengthPercentage::parse)?;
|
|
if items.is_empty() {
|
|
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<ColorInterpolationMethod> {
|
|
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<Self, ParseError<'i>> {
|
|
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<Self, ParseError<'i>> {
|
|
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<Self, ParseError<'i>> {
|
|
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.is_empty() {
|
|
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<W>(&self, dest: &mut CssWriter<W>, 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<Self, ParseError<'i>> {
|
|
// 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<Self, ParseError<'i>> {
|
|
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<Self, ParseError<'i>> {
|
|
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<T> generic::GradientItem<Color, T> {
|
|
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<T, ParseError<'i1>>
|
|
+ Copy,
|
|
) -> Result<crate::OwnedSlice<Self>, 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.is_empty() {
|
|
return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
|
|
}
|
|
Ok(items.into())
|
|
}
|
|
}
|
|
|
|
impl<T> generic::ColorStop<Color, T> {
|
|
fn parse<'i, 't>(
|
|
context: &ParserContext,
|
|
input: &mut Parser<'i, 't>,
|
|
parse_position: impl for<'i1, 't1> Fn(
|
|
&ParserContext,
|
|
&mut Parser<'i1, 't1>,
|
|
) -> Result<T, ParseError<'i1>>,
|
|
) -> Result<Self, ParseError<'i>> {
|
|
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<Self, ParseError<'i>> {
|
|
use servo_arc::Arc;
|
|
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).map(Arc::new))
|
|
})
|
|
.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,
|
|
}
|