/* 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 http://mozilla.org/MPL/2.0/. */ #![deny(missing_docs)] //! Fairly complete css-color implementation. //! Relative colors, color-mix, system colors, and other such things require better calc() support //! and integration. use super::{ color_function::ColorFunction, component::{ColorComponent, ColorComponentType}, AbsoluteColor, }; use crate::{ parser::ParserContext, values::{ generics::calc::CalcUnits, specified::{ angle::Angle as SpecifiedAngle, calc::Leaf as SpecifiedLeaf, color::Color as SpecifiedColor, }, }, }; use cssparser::{ color::{clamp_floor_256_f32, clamp_unit_f32, parse_hash_color, PredefinedColorSpace, OPAQUE}, match_ignore_ascii_case, CowRcStr, Parser, Token, }; use std::str::FromStr; use style_traits::{ParseError, StyleParseErrorKind}; /// Returns true if the relative color syntax pref is enabled. #[inline] pub fn rcs_enabled() -> bool { static_prefs::pref!("layout.css.relative-color-syntax.enabled") } impl From for ColorComponent { #[inline] fn from(value: u8) -> Self { ColorComponent::Value(value) } } /// Return the named color with the given name. /// /// Matching is case-insensitive in the ASCII range. /// CSS escaping (if relevant) should be resolved before calling this function. /// (For example, the value of an `Ident` token is fine.) #[inline] pub fn parse_color_keyword(ident: &str) -> Result { Ok(match_ignore_ascii_case! { ident, "transparent" => { SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(0u8, 0u8, 0u8, 0.0)) }, "currentcolor" => SpecifiedColor::CurrentColor, _ => { let (r, g, b) = cssparser::color::parse_named_color(ident)?; SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(r, g, b, OPAQUE)) }, }) } /// Parse a CSS color using the specified [`ColorParser`] and return a new color /// value on success. pub fn parse_color_with<'i, 't>( color_parser: &ColorParser<'_, '_>, input: &mut Parser<'i, 't>, ) -> Result> { let location = input.current_source_location(); let token = input.next()?; match *token { Token::Hash(ref value) | Token::IDHash(ref value) => parse_hash_color(value.as_bytes()) .map(|(r, g, b, a)| { SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(r, g, b, a)) }), Token::Ident(ref value) => parse_color_keyword(value), Token::Function(ref name) => { let name = name.clone(); return input.parse_nested_block(|arguments| { Ok(SpecifiedColor::from_absolute_color( parse_color_function(color_parser, name, arguments)?.resolve_to_absolute(), )) }); }, _ => Err(()), } .map_err(|()| location.new_unexpected_token_error(token.clone())) } /// Parse one of the color functions: rgba(), lab(), color(), etc. #[inline] fn parse_color_function<'i, 't>( color_parser: &ColorParser<'_, '_>, name: CowRcStr<'i>, arguments: &mut Parser<'i, 't>, ) -> Result> { let color = match_ignore_ascii_case! { &name, "rgb" | "rgba" => parse_rgb(color_parser, arguments), "hsl" | "hsla" => parse_hsl(color_parser, arguments), "hwb" => parse_hwb(color_parser, arguments), "lab" => parse_lab_like(color_parser, arguments, ColorFunction::Lab), "lch" => parse_lch_like(color_parser, arguments, ColorFunction::Lch), "oklab" => parse_lab_like(color_parser, arguments, ColorFunction::Oklab), "oklch" => parse_lch_like(color_parser, arguments, ColorFunction::Oklch), "color" => parse_color_with_color_space(color_parser, arguments), _ => return Err(arguments.new_unexpected_token_error(Token::Ident(name))), }?; arguments.expect_exhausted()?; Ok(color) } fn parse_legacy_alpha<'i, 't>( color_parser: &ColorParser<'_, '_>, arguments: &mut Parser<'i, 't>, ) -> Result, ParseError<'i>> { if !arguments.is_exhausted() { arguments.expect_comma()?; color_parser.parse_number_or_percentage(arguments, false) } else { Ok(ColorComponent::Value(NumberOrPercentage::Number { value: OPAQUE, })) } } fn parse_modern_alpha<'i, 't>( color_parser: &ColorParser<'_, '_>, arguments: &mut Parser<'i, 't>, ) -> Result, ParseError<'i>> { if !arguments.is_exhausted() { arguments.expect_delim('/')?; color_parser.parse_number_or_percentage(arguments, true) } else { Ok(ColorComponent::Value(NumberOrPercentage::Number { value: OPAQUE, })) } } impl ColorComponent { /// Return true if the component contains a percentage. pub fn is_percentage(&self) -> Result { Ok(match self { Self::Value(NumberOrPercentage::Percentage { .. }) => true, _ => false, }) } } /// Parse the relative color syntax "from" syntax `from `. fn parse_origin_color<'i, 't>( color_parser: &ColorParser<'_, '_>, arguments: &mut Parser<'i, 't>, ) -> Result, ParseError<'i>> { if !rcs_enabled() { return Ok(None); } // Not finding the from keyword is not an error, it just means we don't // have an origin color. if arguments .try_parse(|p| p.expect_ident_matching("from")) .is_err() { return Ok(None); } // We still fail if we can't parse the origin color. parse_color_with(color_parser, arguments).map(|color| Some(color)) } #[inline] fn parse_rgb<'i, 't>( color_parser: &ColorParser<'_, '_>, arguments: &mut Parser<'i, 't>, ) -> Result> { let origin_color = parse_origin_color(color_parser, arguments)?; let location = arguments.current_source_location(); let maybe_red = color_parser.parse_number_or_percentage(arguments, true)?; // If the first component is not "none" and is followed by a comma, then we // are parsing the legacy syntax. Legacy syntax also doesn't support an // origin color. let is_legacy_syntax = origin_color.is_none() && !maybe_red.is_none() && arguments.try_parse(|p| p.expect_comma()).is_ok(); let (red, green, blue, alpha) = if is_legacy_syntax { let Ok(is_percentage) = maybe_red.is_percentage() else { return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); }; let (red, green, blue) = if is_percentage { let red = maybe_red.map_value(|v| clamp_unit_f32(v.to_number(1.0))); let green = color_parser .parse_percentage(arguments, false)? .map_value(clamp_unit_f32); arguments.expect_comma()?; let blue = color_parser .parse_percentage(arguments, false)? .map_value(clamp_unit_f32); (red, green, blue) } else { let red = maybe_red.map_value(|v| clamp_floor_256_f32(v.to_number(255.0))); let green = color_parser .parse_number(arguments, false)? .map_value(clamp_floor_256_f32); arguments.expect_comma()?; let blue = color_parser .parse_number(arguments, false)? .map_value(clamp_floor_256_f32); (red, green, blue) }; let alpha = parse_legacy_alpha(color_parser, arguments)?; (red, green, blue, alpha) } else { let red = maybe_red.map_value(|v| clamp_floor_256_f32(v.to_number(255.0))); let green = color_parser .parse_number_or_percentage(arguments, true)? .map_value(|v| clamp_floor_256_f32(v.to_number(255.0))); let blue = color_parser .parse_number_or_percentage(arguments, true)? .map_value(|v| clamp_floor_256_f32(v.to_number(255.0))); let alpha = parse_modern_alpha(color_parser, arguments)?; (red, green, blue, alpha) }; Ok(ColorFunction::Rgb(red, green, blue, alpha)) } /// Parses hsl syntax. /// /// #[inline] fn parse_hsl<'i, 't>( color_parser: &ColorParser<'_, '_>, arguments: &mut Parser<'i, 't>, ) -> Result> { let origin_color = parse_origin_color(color_parser, arguments)?; let hue = color_parser.parse_number_or_angle(arguments, true)?; // If the hue is not "none" and is followed by a comma, then we are parsing // the legacy syntax. Legacy syntax also doesn't support an origin color. let is_legacy_syntax = origin_color.is_none() && !hue.is_none() && arguments.try_parse(|p| p.expect_comma()).is_ok(); let (saturation, lightness, alpha) = if is_legacy_syntax { let saturation = color_parser .parse_percentage(arguments, false)? .map_value(|unit_value| NumberOrPercentage::Percentage { unit_value }); arguments.expect_comma()?; let lightness = color_parser .parse_percentage(arguments, false)? .map_value(|unit_value| NumberOrPercentage::Percentage { unit_value }); ( saturation, lightness, parse_legacy_alpha(color_parser, arguments)?, ) } else { let saturation = color_parser.parse_number_or_percentage(arguments, true)?; let lightness = color_parser.parse_number_or_percentage(arguments, true)?; ( saturation, lightness, parse_modern_alpha(color_parser, arguments)?, ) }; Ok(ColorFunction::Hsl(hue, saturation, lightness, alpha)) } /// Parses hwb syntax. /// /// #[inline] fn parse_hwb<'i, 't>( color_parser: &ColorParser<'_, '_>, arguments: &mut Parser<'i, 't>, ) -> Result> { let _origin_color = parse_origin_color(color_parser, arguments)?; let (hue, whiteness, blackness, alpha) = parse_components( color_parser, arguments, ColorParser::parse_number_or_angle, ColorParser::parse_number_or_percentage, ColorParser::parse_number_or_percentage, )?; Ok(ColorFunction::Hwb(hue, whiteness, blackness, alpha)) } type IntoLabFn = fn( l: ColorComponent, a: ColorComponent, b: ColorComponent, alpha: ColorComponent, ) -> Output; #[inline] fn parse_lab_like<'i, 't>( color_parser: &ColorParser<'_, '_>, arguments: &mut Parser<'i, 't>, into_color: IntoLabFn, ) -> Result> { let _origin_color = parse_origin_color(color_parser, arguments)?; let (lightness, a, b, alpha) = parse_components( color_parser, arguments, ColorParser::parse_number_or_percentage, ColorParser::parse_number_or_percentage, ColorParser::parse_number_or_percentage, )?; Ok(into_color(lightness, a, b, alpha)) } type IntoLchFn = fn( l: ColorComponent, a: ColorComponent, b: ColorComponent, alpha: ColorComponent, ) -> Output; #[inline] fn parse_lch_like<'i, 't>( color_parser: &ColorParser<'_, '_>, arguments: &mut Parser<'i, 't>, into_color: IntoLchFn, ) -> Result> { let _origin_color = parse_origin_color(color_parser, arguments)?; let (lightness, chroma, hue, alpha) = parse_components( color_parser, arguments, ColorParser::parse_number_or_percentage, ColorParser::parse_number_or_percentage, ColorParser::parse_number_or_angle, )?; Ok(into_color(lightness, chroma, hue, alpha)) } /// Parse the color() function. #[inline] fn parse_color_with_color_space<'i, 't>( color_parser: &ColorParser<'_, '_>, arguments: &mut Parser<'i, 't>, ) -> Result> { let _origin_color = parse_origin_color(color_parser, arguments)?; let color_space = { let location = arguments.current_source_location(); let ident = arguments.expect_ident()?; PredefinedColorSpace::from_str(ident) .map_err(|_| location.new_unexpected_token_error(Token::Ident(ident.clone())))? }; let (c1, c2, c3, alpha) = parse_components( color_parser, arguments, ColorParser::parse_number_or_percentage, ColorParser::parse_number_or_percentage, ColorParser::parse_number_or_percentage, )?; Ok(ColorFunction::Color(color_space, c1, c2, c3, alpha)) } type ComponentParseResult<'i, R1, R2, R3> = Result< ( ColorComponent, ColorComponent, ColorComponent, ColorComponent, ), ParseError<'i>, >; /// Parse the color components and alpha with the modern [color-4] syntax. pub fn parse_components<'a, 'b: 'a, 'i, 't, F1, F2, F3, R1, R2, R3>( color_parser: &ColorParser<'a, 'b>, input: &mut Parser<'i, 't>, f1: F1, f2: F2, f3: F3, ) -> ComponentParseResult<'i, R1, R2, R3> where F1: FnOnce( &ColorParser<'a, 'b>, &mut Parser<'i, 't>, bool, ) -> Result, ParseError<'i>>, F2: FnOnce( &ColorParser<'a, 'b>, &mut Parser<'i, 't>, bool, ) -> Result, ParseError<'i>>, F3: FnOnce( &ColorParser<'a, 'b>, &mut Parser<'i, 't>, bool, ) -> Result, ParseError<'i>>, { let r1 = f1(color_parser, input, true)?; let r2 = f2(color_parser, input, true)?; let r3 = f3(color_parser, input, true)?; let alpha = parse_modern_alpha(color_parser, input)?; Ok((r1, r2, r3, alpha)) } /// Either a number or a percentage. #[derive(Clone, Copy, Debug)] pub enum NumberOrPercentage { /// ``. Number { /// The numeric value parsed, as a float. value: f32, }, /// `` Percentage { /// The value as a float, divided by 100 so that the nominal range is /// 0.0 to 1.0. unit_value: f32, }, } impl NumberOrPercentage { /// Return the value as a number. Percentages will be adjusted to the range /// [0..percent_basis]. pub fn to_number(&self, percentage_basis: f32) -> f32 { match *self { Self::Number { value } => value, Self::Percentage { unit_value } => unit_value * percentage_basis, } } } impl ColorComponentType for NumberOrPercentage { fn units() -> CalcUnits { CalcUnits::PERCENTAGE } fn try_from_token(token: &Token) -> Result { Ok(match *token { Token::Number { value, .. } => Self::Number { value }, Token::Percentage { unit_value, .. } => Self::Percentage { unit_value }, _ => { return Err(()); }, }) } fn try_from_leaf(leaf: &SpecifiedLeaf) -> Result { Ok(match *leaf { SpecifiedLeaf::Percentage(unit_value) => Self::Percentage { unit_value }, SpecifiedLeaf::Number(value) => Self::Number { value }, _ => return Err(()), }) } } /// Either an angle or a number. #[derive(Clone, Copy, Debug)] pub enum NumberOrAngle { /// ``. Number { /// The numeric value parsed, as a float. value: f32, }, /// `` Angle { /// The value as a number of degrees. degrees: f32, }, } impl NumberOrAngle { /// Return the angle in degrees. `NumberOrAngle::Number` is returned as /// degrees, because it is the canonical unit. pub fn degrees(&self) -> f32 { match *self { Self::Number { value } => value, Self::Angle { degrees } => degrees, } } } impl ColorComponentType for NumberOrAngle { fn units() -> CalcUnits { CalcUnits::ANGLE } fn try_from_token(token: &Token) -> Result { Ok(match *token { Token::Number { value, .. } => Self::Number { value }, Token::Dimension { value, ref unit, .. } => { let degrees = SpecifiedAngle::parse_dimension(value, unit, /* from_calc = */ false) .map(|angle| angle.degrees())?; NumberOrAngle::Angle { degrees } }, _ => { return Err(()); }, }) } fn try_from_leaf(leaf: &SpecifiedLeaf) -> Result { Ok(match *leaf { SpecifiedLeaf::Angle(angle) => Self::Angle { degrees: angle.degrees(), }, SpecifiedLeaf::Number(value) => Self::Number { value }, _ => return Err(()), }) } } /// The raw f32 here is for . impl ColorComponentType for f32 { fn units() -> CalcUnits { CalcUnits::empty() } fn try_from_token(token: &Token) -> Result { if let Token::Number { value, .. } = *token { Ok(value) } else { Err(()) } } fn try_from_leaf(leaf: &SpecifiedLeaf) -> Result { if let SpecifiedLeaf::Number(value) = *leaf { Ok(value) } else { Err(()) } } } /// Used to parse the components of a color. pub struct ColorParser<'a, 'b: 'a> { /// Parser context used for parsing the colors. pub context: &'a ParserContext<'b>, } impl<'a, 'b: 'a> ColorParser<'a, 'b> { /// Parse an `` or `` value. fn parse_number_or_angle<'i, 't>( &self, input: &mut Parser<'i, 't>, allow_none: bool, ) -> Result, ParseError<'i>> { ColorComponent::parse(self.context, input, allow_none) } /// Parse a `` value. fn parse_percentage<'i, 't>( &self, input: &mut Parser<'i, 't>, allow_none: bool, ) -> Result, ParseError<'i>> { let location = input.current_source_location(); // We can use the [NumberOrPercentage] type here, because parsing it // doesn't have any more overhead than just parsing a percentage on its // own. Ok( match ColorComponent::::parse(self.context, input, allow_none)? { ColorComponent::None => ColorComponent::None, ColorComponent::Value(NumberOrPercentage::Percentage { unit_value }) => { ColorComponent::Value(unit_value) }, _ => return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)), }, ) } /// Parse a `` value. fn parse_number<'i, 't>( &self, input: &mut Parser<'i, 't>, allow_none: bool, ) -> Result, ParseError<'i>> { ColorComponent::parse(self.context, input, allow_none) } /// Parse a `` or `` value. fn parse_number_or_percentage<'i, 't>( &self, input: &mut Parser<'i, 't>, allow_none: bool, ) -> Result, ParseError<'i>> { ColorComponent::parse(self.context, input, allow_none) } } /// This trait is used by the [`ColorParser`] to construct colors of any type. pub trait FromParsedColor { /// Construct a new color from the CSS `currentcolor` keyword. fn from_current_color() -> Self; /// Construct a new color from red, green, blue and alpha components. fn from_rgba( red: ColorComponent, green: ColorComponent, blue: ColorComponent, alpha: ColorComponent, ) -> Self; /// Construct a new color from hue, saturation, lightness and alpha components. fn from_hsl( hue: ColorComponent, saturation: ColorComponent, lightness: ColorComponent, alpha: ColorComponent, ) -> Self; /// Construct a new color from hue, blackness, whiteness and alpha components. fn from_hwb( hue: ColorComponent, whiteness: ColorComponent, blackness: ColorComponent, alpha: ColorComponent, ) -> Self; /// Construct a new color from the `lab` notation. fn from_lab( lightness: ColorComponent, a: ColorComponent, b: ColorComponent, alpha: ColorComponent, ) -> Self; /// Construct a new color from the `lch` notation. fn from_lch( lightness: ColorComponent, chroma: ColorComponent, hue: ColorComponent, alpha: ColorComponent, ) -> Self; /// Construct a new color from the `oklab` notation. fn from_oklab( lightness: ColorComponent, a: ColorComponent, b: ColorComponent, alpha: ColorComponent, ) -> Self; /// Construct a new color from the `oklch` notation. fn from_oklch( lightness: ColorComponent, chroma: ColorComponent, hue: ColorComponent, alpha: ColorComponent, ) -> Self; /// Construct a new color with a predefined color space. fn from_color_function( color_space: PredefinedColorSpace, c1: ColorComponent, c2: ColorComponent, c3: ColorComponent, alpha: ColorComponent, ) -> Self; }