diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 49e91ad3..0d3c0229 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -51,6 +51,14 @@ jobs: run: cargo build working-directory: macros + - name: Color build + run: cargo build + working-directory: color + + - name: Color test + run: cargo test + working-directory: color + - name: Cargo miri test if: "matrix.toolchain == 'nightly'" run: cargo miri test --features skip_long_tests ${{ matrix.features }} diff --git a/Cargo.toml b/Cargo.toml index 4dc608d7..aae87dcf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,4 +34,4 @@ dummy_match_byte = [] skip_long_tests = [] [workspace] -members = [".", "./macros"] +members = [".", "./macros", "./color"] diff --git a/color/Cargo.toml b/color/Cargo.toml new file mode 100644 index 00000000..d3201a12 --- /dev/null +++ b/color/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "cssparser-color" +version = "0.1.0" +authors = ["Emilio Cobos Álvarez "] +description = "Color implementation based on cssparser" +documentation = "https://docs.rs/cssparser-color/" +repository = "https://github.com/servo/rust-cssparser" +license = "MPL-2.0" +edition = "2021" + +[lib] +path = "lib.rs" + +[dependencies] +cssparser = { path = ".." } + +[dev-dependencies] +serde_json = "1.0" +difference = "2.0" +encoding_rs = "0.8" diff --git a/color/lib.rs b/color/lib.rs new file mode 100644 index 00000000..1c52e38d --- /dev/null +++ b/color/lib.rs @@ -0,0 +1,1304 @@ +/* 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. + +#[cfg(test)] +mod tests; + +use cssparser::color::{ + clamp_floor_256_f32, clamp_unit_f32, parse_hash_color, serialize_color_alpha, + PredefinedColorSpace, OPAQUE, +}; +use cssparser::{match_ignore_ascii_case, CowRcStr, ParseError, Parser, ToCss, Token}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::f32::consts::PI; +use std::fmt; +use std::str::FromStr; + +/// 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 +where + Output: FromParsedColor, +{ + Ok(match_ignore_ascii_case! { ident , + "transparent" => Output::from_rgba(0, 0, 0, 0.0), + "currentcolor" => Output::from_current_color(), + _ => { + let (r, g, b) = cssparser::color::parse_named_color(ident)?; + Output::from_rgba(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, P>( + color_parser: &P, + input: &mut Parser<'i, 't>, +) -> Result> +where + P: ColorParser<'i>, +{ + 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)| { + P::Output::from_rgba(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| { + parse_color_function(color_parser, name, arguments) + }); + } + _ => 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, P>( + color_parser: &P, + name: CowRcStr<'i>, + arguments: &mut Parser<'i, 't>, +) -> Result> +where + P: ColorParser<'i>, +{ + 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), + + // for L: 0% = 0.0, 100% = 100.0 + // for a and b: -100% = -125, 100% = 125 + "lab" => parse_lab_like(color_parser, arguments, 100.0, 125.0, P::Output::from_lab), + + // for L: 0% = 0.0, 100% = 100.0 + // for C: 0% = 0, 100% = 150 + "lch" => parse_lch_like(color_parser, arguments, 100.0, 150.0, P::Output::from_lch), + + // for L: 0% = 0.0, 100% = 1.0 + // for a and b: -100% = -0.4, 100% = 0.4 + "oklab" => parse_lab_like(color_parser, arguments, 1.0, 0.4, P::Output::from_oklab), + + // for L: 0% = 0.0, 100% = 1.0 + // for C: 0% = 0.0 100% = 0.4 + "oklch" => parse_lch_like(color_parser, arguments, 1.0, 0.4, P::Output::from_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) +} + +/// Parse the alpha component by itself from either number or percentage, +/// clipping the result to [0.0..1.0]. +#[inline] +fn parse_alpha_component<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result> +where + P: ColorParser<'i>, +{ + Ok(color_parser + .parse_number_or_percentage(arguments)? + .unit_value() + .clamp(0.0, OPAQUE)) +} + +fn parse_legacy_alpha<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result> +where + P: ColorParser<'i>, +{ + Ok(if !arguments.is_exhausted() { + arguments.expect_comma()?; + parse_alpha_component(color_parser, arguments)? + } else { + OPAQUE + }) +} + +fn parse_modern_alpha<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + if !arguments.is_exhausted() { + arguments.expect_delim('/')?; + parse_none_or(arguments, |p| parse_alpha_component(color_parser, p)) + } else { + Ok(Some(OPAQUE)) + } +} + +#[inline] +fn parse_rgb<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result> +where + P: ColorParser<'i>, +{ + let maybe_red = parse_none_or(arguments, |p| color_parser.parse_number_or_percentage(p))?; + + // If the first component is not "none" and is followed by a comma, then we + // are parsing the legacy syntax. + let is_legacy_syntax = maybe_red.is_some() && arguments.try_parse(|p| p.expect_comma()).is_ok(); + + let (red, green, blue, alpha) = if is_legacy_syntax { + let (red, green, blue) = match maybe_red.unwrap() { + NumberOrPercentage::Number { value } => { + let red = clamp_floor_256_f32(value); + let green = clamp_floor_256_f32(color_parser.parse_number(arguments)?); + arguments.expect_comma()?; + let blue = clamp_floor_256_f32(color_parser.parse_number(arguments)?); + (red, green, blue) + } + NumberOrPercentage::Percentage { unit_value } => { + let red = clamp_unit_f32(unit_value); + let green = clamp_unit_f32(color_parser.parse_percentage(arguments)?); + arguments.expect_comma()?; + let blue = clamp_unit_f32(color_parser.parse_percentage(arguments)?); + (red, green, blue) + } + }; + + let alpha = parse_legacy_alpha(color_parser, arguments)?; + + (red, green, blue, alpha) + } else { + #[inline] + fn get_component_value(c: Option) -> u8 { + c.map(|c| match c { + NumberOrPercentage::Number { value } => clamp_floor_256_f32(value), + NumberOrPercentage::Percentage { unit_value } => clamp_unit_f32(unit_value), + }) + .unwrap_or(0) + } + + let red = get_component_value(maybe_red); + + let green = get_component_value(parse_none_or(arguments, |p| { + color_parser.parse_number_or_percentage(p) + })?); + + let blue = get_component_value(parse_none_or(arguments, |p| { + color_parser.parse_number_or_percentage(p) + })?); + + let alpha = parse_modern_alpha(color_parser, arguments)?.unwrap_or(0.0); + + (red, green, blue, alpha) + }; + + Ok(P::Output::from_rgba(red, green, blue, alpha)) +} + +/// Parses hsl syntax. +/// +/// +#[inline] +fn parse_hsl<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result> +where + P: ColorParser<'i>, +{ + let maybe_hue = parse_none_or(arguments, |p| color_parser.parse_angle_or_number(p))?; + + // If the hue is not "none" and is followed by a comma, then we are parsing + // the legacy syntax. + let is_legacy_syntax = maybe_hue.is_some() && arguments.try_parse(|p| p.expect_comma()).is_ok(); + + let saturation: Option; + let lightness: Option; + + let alpha = if is_legacy_syntax { + saturation = Some(color_parser.parse_percentage(arguments)?); + arguments.expect_comma()?; + lightness = Some(color_parser.parse_percentage(arguments)?); + Some(parse_legacy_alpha(color_parser, arguments)?) + } else { + saturation = parse_none_or(arguments, |p| color_parser.parse_percentage(p))?; + lightness = parse_none_or(arguments, |p| color_parser.parse_percentage(p))?; + + parse_modern_alpha(color_parser, arguments)? + }; + + let hue = maybe_hue.map(|h| normalize_hue(h.degrees())); + let saturation = saturation.map(|s| s.clamp(0.0, 1.0)); + let lightness = lightness.map(|l| l.clamp(0.0, 1.0)); + + Ok(P::Output::from_hsl(hue, saturation, lightness, alpha)) +} + +/// Parses hwb syntax. +/// +/// +#[inline] +fn parse_hwb<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result> +where + P: ColorParser<'i>, +{ + let (hue, whiteness, blackness, alpha) = parse_components( + color_parser, + arguments, + P::parse_angle_or_number, + P::parse_percentage, + P::parse_percentage, + )?; + + let hue = hue.map(|h| normalize_hue(h.degrees())); + let whiteness = whiteness.map(|w| w.clamp(0.0, 1.0)); + let blackness = blackness.map(|b| b.clamp(0.0, 1.0)); + + Ok(P::Output::from_hwb(hue, whiteness, blackness, alpha)) +} + +/// +#[inline] +pub fn hwb_to_rgb(h: f32, w: f32, b: f32) -> (f32, f32, f32) { + if w + b >= 1.0 { + let gray = w / (w + b); + return (gray, gray, gray); + } + + // hue is expected in the range [0..1]. + let (mut red, mut green, mut blue) = hsl_to_rgb(h, 1.0, 0.5); + let x = 1.0 - w - b; + red = red * x + w; + green = green * x + w; + blue = blue * x + w; + (red, green, blue) +} + +/// +/// except with h pre-multiplied by 3, to avoid some rounding errors. +#[inline] +pub fn hsl_to_rgb(hue: f32, saturation: f32, lightness: f32) -> (f32, f32, f32) { + debug_assert!((0.0..=1.0).contains(&hue)); + + fn hue_to_rgb(m1: f32, m2: f32, mut h3: f32) -> f32 { + if h3 < 0. { + h3 += 3. + } + if h3 > 3. { + h3 -= 3. + } + if h3 * 2. < 1. { + m1 + (m2 - m1) * h3 * 2. + } else if h3 * 2. < 3. { + m2 + } else if h3 < 2. { + m1 + (m2 - m1) * (2. - h3) * 2. + } else { + m1 + } + } + let m2 = if lightness <= 0.5 { + lightness * (saturation + 1.) + } else { + lightness + saturation - lightness * saturation + }; + let m1 = lightness * 2. - m2; + let hue_times_3 = hue * 3.; + let red = hue_to_rgb(m1, m2, hue_times_3 + 1.); + let green = hue_to_rgb(m1, m2, hue_times_3); + let blue = hue_to_rgb(m1, m2, hue_times_3 - 1.); + (red, green, blue) +} + +type IntoColorFn = + fn(l: Option, a: Option, b: Option, alpha: Option) -> Output; + +#[inline] +fn parse_lab_like<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, + lightness_range: f32, + a_b_range: f32, + into_color: IntoColorFn, +) -> Result> +where + P: ColorParser<'i>, +{ + let (lightness, a, b, alpha) = parse_components( + color_parser, + arguments, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + )?; + + let lightness = lightness.map(|l| l.value(lightness_range)); + let a = a.map(|a| a.value(a_b_range)); + let b = b.map(|b| b.value(a_b_range)); + + Ok(into_color(lightness, a, b, alpha)) +} + +#[inline] +fn parse_lch_like<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, + lightness_range: f32, + chroma_range: f32, + into_color: IntoColorFn, +) -> Result> +where + P: ColorParser<'i>, +{ + let (lightness, chroma, hue, alpha) = parse_components( + color_parser, + arguments, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + P::parse_angle_or_number, + )?; + + let lightness = lightness.map(|l| l.value(lightness_range)); + let chroma = chroma.map(|c| c.value(chroma_range)); + let hue = hue.map(|h| normalize_hue(h.degrees())); + + Ok(into_color(lightness, chroma, hue, alpha)) +} + +/// Parse the color() function. +#[inline] +fn parse_color_with_color_space<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result> +where + P: ColorParser<'i>, +{ + 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, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + )?; + + let c1 = c1.map(|c| c.unit_value()); + let c2 = c2.map(|c| c.unit_value()); + let c3 = c3.map(|c| c.unit_value()); + + Ok(P::Output::from_color_function( + color_space, + c1, + c2, + c3, + alpha, + )) +} + +type ComponentParseResult<'i, R1, R2, R3, Error> = + Result<(Option, Option, Option, Option), ParseError<'i, Error>>; + +/// Parse the color components and alpha with the modern [color-4] syntax. +pub fn parse_components<'i, 't, P, F1, F2, F3, R1, R2, R3>( + color_parser: &P, + input: &mut Parser<'i, 't>, + f1: F1, + f2: F2, + f3: F3, +) -> ComponentParseResult<'i, R1, R2, R3, P::Error> +where + P: ColorParser<'i>, + F1: FnOnce(&P, &mut Parser<'i, 't>) -> Result>, + F2: FnOnce(&P, &mut Parser<'i, 't>) -> Result>, + F3: FnOnce(&P, &mut Parser<'i, 't>) -> Result>, +{ + let r1 = parse_none_or(input, |p| f1(color_parser, p))?; + let r2 = parse_none_or(input, |p| f2(color_parser, p))?; + let r3 = parse_none_or(input, |p| f3(color_parser, p))?; + + let alpha = parse_modern_alpha(color_parser, input)?; + + Ok((r1, r2, r3, alpha)) +} + +fn parse_none_or<'i, 't, F, T, E>(input: &mut Parser<'i, 't>, thing: F) -> Result, E> +where + F: FnOnce(&mut Parser<'i, 't>) -> Result, +{ + match input.try_parse(|p| p.expect_ident_matching("none")) { + Ok(_) => Ok(None), + Err(_) => Ok(Some(thing(input)?)), + } +} + +/// A [`ModernComponent`] can serialize to `none`, `nan`, `infinity` and +/// floating point values. +struct ModernComponent<'a>(&'a Option); + +impl<'a> ToCss for ModernComponent<'a> { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + if let Some(value) = self.0 { + if value.is_finite() { + value.to_css(dest) + } else if value.is_nan() { + dest.write_str("calc(NaN)") + } else { + debug_assert!(value.is_infinite()); + if value.is_sign_negative() { + dest.write_str("calc(-infinity)") + } else { + dest.write_str("calc(infinity)") + } + } + } else { + dest.write_str("none") + } + } +} + +// Guaratees hue in [0..360) +fn normalize_hue(hue: f32) -> f32 { + // + // Subtract an integer before rounding, to avoid some rounding errors: + hue - 360.0 * (hue / 360.0).floor() +} + +/// A color with red, green, blue, and alpha components, in a byte each. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct RgbaLegacy { + /// The red component. + pub red: u8, + /// The green component. + pub green: u8, + /// The blue component. + pub blue: u8, + /// The alpha component. + pub alpha: f32, +} + +impl RgbaLegacy { + /// Constructs a new RGBA value from float components. It expects the red, + /// green, blue and alpha channels in that order, and all values will be + /// clamped to the 0.0 ... 1.0 range. + #[inline] + pub fn from_floats(red: f32, green: f32, blue: f32, alpha: f32) -> Self { + Self::new( + clamp_unit_f32(red), + clamp_unit_f32(green), + clamp_unit_f32(blue), + alpha.clamp(0.0, OPAQUE), + ) + } + + /// Same thing, but with `u8` values instead of floats in the 0 to 1 range. + #[inline] + pub const fn new(red: u8, green: u8, blue: u8, alpha: f32) -> Self { + Self { + red, + green, + blue, + alpha, + } + } +} + +#[cfg(feature = "serde")] +impl Serialize for RgbaLegacy { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + (self.red, self.green, self.blue, self.alpha).serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for RgbaLegacy { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let (r, g, b, a) = Deserialize::deserialize(deserializer)?; + Ok(RgbaLegacy::new(r, g, b, a)) + } +} + +impl ToCss for RgbaLegacy { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + let has_alpha = self.alpha != OPAQUE; + + dest.write_str(if has_alpha { "rgba(" } else { "rgb(" })?; + self.red.to_css(dest)?; + dest.write_str(", ")?; + self.green.to_css(dest)?; + dest.write_str(", ")?; + self.blue.to_css(dest)?; + + // Legacy syntax does not allow none components. + serialize_color_alpha(dest, Some(self.alpha), true)?; + + dest.write_char(')') + } +} + +/// Color specified by hue, saturation and lightness components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Hsl { + /// The hue component. + pub hue: Option, + /// The saturation component. + pub saturation: Option, + /// The lightness component. + pub lightness: Option, + /// The alpha component. + pub alpha: Option, +} + +impl Hsl { + /// Construct a new HSL color from it's components. + pub fn new( + hue: Option, + saturation: Option, + lightness: Option, + alpha: Option, + ) -> Self { + Self { + hue, + saturation, + lightness, + alpha, + } + } +} + +impl ToCss for Hsl { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + // HSL serializes to RGB, so we have to convert it. + let (red, green, blue) = hsl_to_rgb( + self.hue.unwrap_or(0.0) / 360.0, + self.saturation.unwrap_or(0.0), + self.lightness.unwrap_or(0.0), + ); + + RgbaLegacy::from_floats(red, green, blue, self.alpha.unwrap_or(OPAQUE)).to_css(dest) + } +} + +#[cfg(feature = "serde")] +impl Serialize for Hsl { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + (self.hue, self.saturation, self.lightness, self.alpha).serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Hsl { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let (lightness, a, b, alpha) = Deserialize::deserialize(deserializer)?; + Ok(Self::new(lightness, a, b, alpha)) + } +} + +/// Color specified by hue, whiteness and blackness components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Hwb { + /// The hue component. + pub hue: Option, + /// The whiteness component. + pub whiteness: Option, + /// The blackness component. + pub blackness: Option, + /// The alpha component. + pub alpha: Option, +} + +impl Hwb { + /// Construct a new HWB color from it's components. + pub fn new( + hue: Option, + whiteness: Option, + blackness: Option, + alpha: Option, + ) -> Self { + Self { + hue, + whiteness, + blackness, + alpha, + } + } +} + +impl ToCss for Hwb { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + // HWB serializes to RGB, so we have to convert it. + let (red, green, blue) = hwb_to_rgb( + self.hue.unwrap_or(0.0) / 360.0, + self.whiteness.unwrap_or(0.0), + self.blackness.unwrap_or(0.0), + ); + + RgbaLegacy::from_floats(red, green, blue, self.alpha.unwrap_or(OPAQUE)).to_css(dest) + } +} + +#[cfg(feature = "serde")] +impl Serialize for Hwb { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + (self.hue, self.whiteness, self.blackness, self.alpha).serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Hwb { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let (lightness, whiteness, blackness, alpha) = Deserialize::deserialize(deserializer)?; + Ok(Self::new(lightness, whiteness, blackness, alpha)) + } +} + +// NOTE: LAB and OKLAB is not declared inside the [impl_lab_like] macro, +// because it causes cbindgen to ignore them. + +/// Color specified by lightness, a- and b-axis components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Lab { + /// The lightness component. + pub lightness: Option, + /// The a-axis component. + pub a: Option, + /// The b-axis component. + pub b: Option, + /// The alpha component. + pub alpha: Option, +} + +/// Color specified by lightness, a- and b-axis components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Oklab { + /// The lightness component. + pub lightness: Option, + /// The a-axis component. + pub a: Option, + /// The b-axis component. + pub b: Option, + /// The alpha component. + pub alpha: Option, +} + +macro_rules! impl_lab_like { + ($cls:ident, $fname:literal) => { + impl $cls { + /// Construct a new Lab color format with lightness, a, b and alpha components. + pub fn new( + lightness: Option, + a: Option, + b: Option, + alpha: Option, + ) -> Self { + Self { + lightness, + a, + b, + alpha, + } + } + } + + #[cfg(feature = "serde")] + impl Serialize for $cls { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + (self.lightness, self.a, self.b, self.alpha).serialize(serializer) + } + } + + #[cfg(feature = "serde")] + impl<'de> Deserialize<'de> for $cls { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let (lightness, a, b, alpha) = Deserialize::deserialize(deserializer)?; + Ok(Self::new(lightness, a, b, alpha)) + } + } + + impl ToCss for $cls { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + dest.write_str($fname)?; + dest.write_str("(")?; + ModernComponent(&self.lightness).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.a).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.b).to_css(dest)?; + serialize_color_alpha(dest, self.alpha, false)?; + dest.write_char(')') + } + } + }; +} + +impl_lab_like!(Lab, "lab"); +impl_lab_like!(Oklab, "oklab"); + +// NOTE: LCH and OKLCH is not declared inside the [impl_lch_like] macro, +// because it causes cbindgen to ignore them. + +/// Color specified by lightness, chroma and hue components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Lch { + /// The lightness component. + pub lightness: Option, + /// The chroma component. + pub chroma: Option, + /// The hue component. + pub hue: Option, + /// The alpha component. + pub alpha: Option, +} + +/// Color specified by lightness, chroma and hue components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Oklch { + /// The lightness component. + pub lightness: Option, + /// The chroma component. + pub chroma: Option, + /// The hue component. + pub hue: Option, + /// The alpha component. + pub alpha: Option, +} + +macro_rules! impl_lch_like { + ($cls:ident, $fname:literal) => { + impl $cls { + /// Construct a new color with lightness, chroma and hue components. + pub fn new( + lightness: Option, + chroma: Option, + hue: Option, + alpha: Option, + ) -> Self { + Self { + lightness, + chroma, + hue, + alpha, + } + } + } + + #[cfg(feature = "serde")] + impl Serialize for $cls { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + (self.lightness, self.chroma, self.hue, self.alpha).serialize(serializer) + } + } + + #[cfg(feature = "serde")] + impl<'de> Deserialize<'de> for $cls { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let (lightness, chroma, hue, alpha) = Deserialize::deserialize(deserializer)?; + Ok(Self::new(lightness, chroma, hue, alpha)) + } + } + + impl ToCss for $cls { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + dest.write_str($fname)?; + dest.write_str("(")?; + ModernComponent(&self.lightness).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.chroma).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.hue).to_css(dest)?; + serialize_color_alpha(dest, self.alpha, false)?; + dest.write_char(')') + } + } + }; +} + +impl_lch_like!(Lch, "lch"); +impl_lch_like!(Oklch, "oklch"); + +/// A color specified by the color() function. +/// +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct ColorFunction { + /// The color space for this color. + pub color_space: PredefinedColorSpace, + /// The first component of the color. Either red or x. + pub c1: Option, + /// The second component of the color. Either green or y. + pub c2: Option, + /// The third component of the color. Either blue or z. + pub c3: Option, + /// The alpha component of the color. + pub alpha: Option, +} + +impl ColorFunction { + /// Construct a new color function definition with the given color space and + /// color components. + pub fn new( + color_space: PredefinedColorSpace, + c1: Option, + c2: Option, + c3: Option, + alpha: Option, + ) -> Self { + Self { + color_space, + c1, + c2, + c3, + alpha, + } + } +} + +impl ToCss for ColorFunction { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + dest.write_str("color(")?; + self.color_space.to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.c1).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.c2).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.c3).to_css(dest)?; + + serialize_color_alpha(dest, self.alpha, false)?; + + dest.write_char(')') + } +} + +/// Describes one of the value values according to the CSS +/// specification. +/// +/// Most components are `Option<_>`, so when the value is `None`, that component +/// serializes to the "none" keyword. +/// +/// +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum Color { + /// The 'currentcolor' keyword. + CurrentColor, + /// Specify sRGB colors directly by their red/green/blue/alpha chanels. + Rgba(RgbaLegacy), + /// Specifies a color in sRGB using hue, saturation and lightness components. + Hsl(Hsl), + /// Specifies a color in sRGB using hue, whiteness and blackness components. + Hwb(Hwb), + /// Specifies a CIELAB color by CIE Lightness and its a- and b-axis hue + /// coordinates (red/green-ness, and yellow/blue-ness) using the CIE LAB + /// rectangular coordinate model. + Lab(Lab), + /// Specifies a CIELAB color by CIE Lightness, Chroma, and hue using the + /// CIE LCH cylindrical coordinate model. + Lch(Lch), + /// Specifies an Oklab color by Oklab Lightness and its a- and b-axis hue + /// coordinates (red/green-ness, and yellow/blue-ness) using the Oklab + /// rectangular coordinate model. + Oklab(Oklab), + /// Specifies an Oklab color by Oklab Lightness, Chroma, and hue using + /// the OKLCH cylindrical coordinate model. + Oklch(Oklch), + /// Specifies a color in a predefined color space. + ColorFunction(ColorFunction), +} + +impl ToCss for Color { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + match *self { + Color::CurrentColor => dest.write_str("currentcolor"), + Color::Rgba(rgba) => rgba.to_css(dest), + Color::Hsl(hsl) => hsl.to_css(dest), + Color::Hwb(hwb) => hwb.to_css(dest), + Color::Lab(lab) => lab.to_css(dest), + Color::Lch(lch) => lch.to_css(dest), + Color::Oklab(lab) => lab.to_css(dest), + Color::Oklch(lch) => lch.to_css(dest), + Color::ColorFunction(color_function) => color_function.to_css(dest), + } + } +} + +/// Either a number or a percentage. +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 percentage. + pub fn unit_value(&self) -> f32 { + match *self { + NumberOrPercentage::Number { value } => value, + NumberOrPercentage::Percentage { unit_value } => unit_value, + } + } + + /// Return the value as a number with a percentage adjusted to the + /// `percentage_basis`. + pub fn value(&self, percentage_basis: f32) -> f32 { + match *self { + Self::Number { value } => value, + Self::Percentage { unit_value } => unit_value * percentage_basis, + } + } +} + +/// Either an angle or a number. +pub enum AngleOrNumber { + /// ``. + Number { + /// The numeric value parsed, as a float. + value: f32, + }, + /// `` + Angle { + /// The value as a number of degrees. + degrees: f32, + }, +} + +impl AngleOrNumber { + /// Return the angle in degrees. `AngleOrNumber::Number` is returned as + /// degrees, because it is the canonical unit. + pub fn degrees(&self) -> f32 { + match *self { + AngleOrNumber::Number { value } => value, + AngleOrNumber::Angle { degrees } => degrees, + } + } +} + +/// A trait that can be used to hook into how `cssparser` parses color +/// components, with the intention of implementing more complicated behavior. +/// +/// For example, this is used by Servo to support calc() in color. +pub trait ColorParser<'i> { + /// The type that the parser will construct on a successful parse. + type Output: FromParsedColor; + + /// A custom error type that can be returned from the parsing functions. + type Error: 'i; + + /// Parse an `` or ``. + /// + /// Returns the result in degrees. + fn parse_angle_or_number<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result> { + let location = input.current_source_location(); + Ok(match *input.next()? { + Token::Number { value, .. } => AngleOrNumber::Number { value }, + Token::Dimension { + value: v, ref unit, .. + } => { + let degrees = match_ignore_ascii_case! { unit, + "deg" => v, + "grad" => v * 360. / 400., + "rad" => v * 360. / (2. * PI), + "turn" => v * 360., + _ => { + return Err(location.new_unexpected_token_error(Token::Ident(unit.clone()))) + } + }; + + AngleOrNumber::Angle { degrees } + } + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }) + } + + /// Parse a `` value. + /// + /// Returns the result in a number from 0.0 to 1.0. + fn parse_percentage<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result> { + input.expect_percentage().map_err(From::from) + } + + /// Parse a `` value. + fn parse_number<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result> { + input.expect_number().map_err(From::from) + } + + /// Parse a `` value or a `` value. + fn parse_number_or_percentage<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result> { + let location = input.current_source_location(); + Ok(match *input.next()? { + Token::Number { value, .. } => NumberOrPercentage::Number { value }, + Token::Percentage { unit_value, .. } => NumberOrPercentage::Percentage { unit_value }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }) + } +} + +/// Default implementation of a [`ColorParser`] +pub struct DefaultColorParser; + +impl<'i> ColorParser<'i> for DefaultColorParser { + type Output = Color; + type Error = (); +} + +impl Color { + /// Parse a value, per CSS Color Module Level 3. + /// + /// FIXME(#2) Deprecated CSS2 System Colors are not supported yet. + pub fn parse<'i>(input: &mut Parser<'i, '_>) -> Result> { + parse_color_with(&DefaultColorParser, input) + } +} + +/// 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: u8, green: u8, blue: u8, alpha: f32) -> Self; + + /// Construct a new color from hue, saturation, lightness and alpha components. + fn from_hsl( + hue: Option, + saturation: Option, + lightness: Option, + alpha: Option, + ) -> Self; + + /// Construct a new color from hue, blackness, whiteness and alpha components. + fn from_hwb( + hue: Option, + whiteness: Option, + blackness: Option, + alpha: Option, + ) -> Self; + + /// Construct a new color from the `lab` notation. + fn from_lab(lightness: Option, a: Option, b: Option, alpha: Option) + -> Self; + + /// Construct a new color from the `lch` notation. + fn from_lch( + lightness: Option, + chroma: Option, + hue: Option, + alpha: Option, + ) -> Self; + + /// Construct a new color from the `oklab` notation. + fn from_oklab( + lightness: Option, + a: Option, + b: Option, + alpha: Option, + ) -> Self; + + /// Construct a new color from the `oklch` notation. + fn from_oklch( + lightness: Option, + chroma: Option, + hue: Option, + alpha: Option, + ) -> Self; + + /// Construct a new color with a predefined color space. + fn from_color_function( + color_space: PredefinedColorSpace, + c1: Option, + c2: Option, + c3: Option, + alpha: Option, + ) -> Self; +} + +impl FromParsedColor for Color { + #[inline] + fn from_current_color() -> Self { + Color::CurrentColor + } + + #[inline] + fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self { + Color::Rgba(RgbaLegacy::new(red, green, blue, alpha)) + } + + fn from_hsl( + hue: Option, + saturation: Option, + lightness: Option, + alpha: Option, + ) -> Self { + Color::Hsl(Hsl::new(hue, saturation, lightness, alpha)) + } + + fn from_hwb( + hue: Option, + blackness: Option, + whiteness: Option, + alpha: Option, + ) -> Self { + Color::Hwb(Hwb::new(hue, blackness, whiteness, alpha)) + } + + #[inline] + fn from_lab( + lightness: Option, + a: Option, + b: Option, + alpha: Option, + ) -> Self { + Color::Lab(Lab::new(lightness, a, b, alpha)) + } + + #[inline] + fn from_lch( + lightness: Option, + chroma: Option, + hue: Option, + alpha: Option, + ) -> Self { + Color::Lch(Lch::new(lightness, chroma, hue, alpha)) + } + + #[inline] + fn from_oklab( + lightness: Option, + a: Option, + b: Option, + alpha: Option, + ) -> Self { + Color::Oklab(Oklab::new(lightness, a, b, alpha)) + } + + #[inline] + fn from_oklch( + lightness: Option, + chroma: Option, + hue: Option, + alpha: Option, + ) -> Self { + Color::Oklch(Oklch::new(lightness, chroma, hue, alpha)) + } + + #[inline] + fn from_color_function( + color_space: PredefinedColorSpace, + c1: Option, + c2: Option, + c3: Option, + alpha: Option, + ) -> Self { + Color::ColorFunction(ColorFunction::new(color_space, c1, c2, c3, alpha)) + } +} diff --git a/color/tests.rs b/color/tests.rs new file mode 100644 index 00000000..babb076b --- /dev/null +++ b/color/tests.rs @@ -0,0 +1,401 @@ +/* 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/. */ + +use super::*; +use crate::{ColorParser, PredefinedColorSpace, Color, RgbaLegacy}; +use cssparser::{Parser, ParserInput}; +use serde_json::{self, json, Value}; + +fn almost_equals(a: &Value, b: &Value) -> bool { + match (a, b) { + (&Value::Number(ref a), &Value::Number(ref b)) => { + let a = a.as_f64().unwrap(); + let b = b.as_f64().unwrap(); + (a - b).abs() <= a.abs() * 1e-6 + } + + (&Value::Bool(a), &Value::Bool(b)) => a == b, + (&Value::String(ref a), &Value::String(ref b)) => a == b, + (&Value::Array(ref a), &Value::Array(ref b)) => { + a.len() == b.len() + && a.iter() + .zip(b.iter()) + .all(|(ref a, ref b)| almost_equals(*a, *b)) + } + (&Value::Object(_), &Value::Object(_)) => panic!("Not implemented"), + (&Value::Null, &Value::Null) => true, + _ => false, + } +} + +fn assert_json_eq(results: Value, expected: Value, message: &str) { + if !almost_equals(&results, &expected) { + println!( + "{}", + ::difference::Changeset::new( + &serde_json::to_string_pretty(&results).unwrap(), + &serde_json::to_string_pretty(&expected).unwrap(), + "\n", + ) + ); + panic!("{}", message) + } +} + + +fn run_raw_json_tests ()>(json_data: &str, run: F) { + let items = match serde_json::from_str(json_data) { + Ok(Value::Array(items)) => items, + other => panic!("Invalid JSON: {:?}", other), + }; + assert!(items.len() % 2 == 0); + let mut input = None; + for item in items.into_iter() { + match (&input, item) { + (&None, json_obj) => input = Some(json_obj), + (&Some(_), expected) => { + let input = input.take().unwrap(); + run(input, expected) + } + }; + } +} + +fn run_json_tests Value>(json_data: &str, parse: F) { + run_raw_json_tests(json_data, |input, expected| match input { + Value::String(input) => { + let mut parse_input = ParserInput::new(&input); + let result = parse(&mut Parser::new(&mut parse_input)); + assert_json_eq(result, expected, &input); + } + _ => panic!("Unexpected JSON"), + }); +} +fn run_color_tests) -> Value>(json_data: &str, to_json: F) { + run_json_tests(json_data, |input| { + let result: Result<_, ParseError<()>> = + input.parse_entirely(|i| Color::parse(i).map_err(Into::into)); + to_json(result.map_err(|_| ())) + }); +} + +#[test] +fn color3() { + run_color_tests(include_str!("../src/css-parsing-tests/color3.json"), |c| { + c.ok() + .map(|v| v.to_css_string().to_json()) + .unwrap_or(Value::Null) + }) +} + +#[cfg_attr(all(miri, feature = "skip_long_tests"), ignore)] +#[test] +fn color3_hsl() { + run_color_tests(include_str!("../src/css-parsing-tests/color3_hsl.json"), |c| { + c.ok() + .map(|v| v.to_css_string().to_json()) + .unwrap_or(Value::Null) + }) +} + +/// color3_keywords.json is different: R, G and B are in 0..255 rather than 0..1 +#[test] +fn color3_keywords() { + run_color_tests( + include_str!("../src/css-parsing-tests/color3_keywords.json"), + |c| { + c.ok() + .map(|v| v.to_css_string().to_json()) + .unwrap_or(Value::Null) + }, + ) +} + +#[cfg_attr(all(miri, feature = "skip_long_tests"), ignore)] +#[test] +fn color4_hwb() { + run_color_tests(include_str!("../src/css-parsing-tests/color4_hwb.json"), |c| { + c.ok() + .map(|v| v.to_css_string().to_json()) + .unwrap_or(Value::Null) + }) +} + +#[cfg_attr(all(miri, feature = "skip_long_tests"), ignore)] +#[test] +fn color4_lab_lch_oklab_oklch() { + run_color_tests( + include_str!("../src/css-parsing-tests/color4_lab_lch_oklab_oklch.json"), + |c| { + c.ok() + .map(|v| v.to_css_string().to_json()) + .unwrap_or(Value::Null) + }, + ) +} + +#[test] +fn color4_color_function() { + run_color_tests( + include_str!("../src/css-parsing-tests/color4_color_function.json"), + |c| { + c.ok() + .map(|v| v.to_css_string().to_json()) + .unwrap_or(Value::Null) + }, + ) +} + +macro_rules! parse_single_color { + ($i:expr) => {{ + let input = $i; + let mut input = ParserInput::new(input); + let mut input = Parser::new(&mut input); + Color::parse(&mut input).map_err(Into::>::into) + }}; +} + +#[test] +fn color4_invalid_color_space() { + let result = parse_single_color!("color(invalid 1 1 1)"); + assert!(result.is_err()); +} + +#[test] +fn serialize_current_color() { + let c = Color::CurrentColor; + assert!(c.to_css_string() == "currentcolor"); +} + +#[test] +fn serialize_rgb_full_alpha() { + let c = Color::Rgba(RgbaLegacy::new(255, 230, 204, 1.0)); + assert_eq!(c.to_css_string(), "rgb(255, 230, 204)"); +} + +#[test] +fn serialize_rgba() { + let c = Color::Rgba(RgbaLegacy::new(26, 51, 77, 0.125)); + assert_eq!(c.to_css_string(), "rgba(26, 51, 77, 0.125)"); +} + +#[test] +fn serialize_rgba_two_digit_float_if_roundtrips() { + let c = Color::Rgba(RgbaLegacy::from_floats(0., 0., 0., 0.5)); + assert_eq!(c.to_css_string(), "rgba(0, 0, 0, 0.5)"); +} + +trait ToJson { + fn to_json(&self) -> Value; +} + +impl ToJson for T +where + T: Clone, + Value: From, +{ + fn to_json(&self) -> Value { + Value::from(self.clone()) + } +} + +impl ToJson for Color { + fn to_json(&self) -> Value { + match *self { + Color::CurrentColor => "currentcolor".to_json(), + Color::Rgba(ref rgba) => { + json!([rgba.red, rgba.green, rgba.blue, rgba.alpha]) + } + Color::Hsl(ref c) => json!([c.hue, c.saturation, c.lightness, c.alpha]), + Color::Hwb(ref c) => json!([c.hue, c.whiteness, c.blackness, c.alpha]), + Color::Lab(ref c) => json!([c.lightness, c.a, c.b, c.alpha]), + Color::Lch(ref c) => json!([c.lightness, c.chroma, c.hue, c.alpha]), + Color::Oklab(ref c) => json!([c.lightness, c.a, c.b, c.alpha]), + Color::Oklch(ref c) => json!([c.lightness, c.chroma, c.hue, c.alpha]), + Color::ColorFunction(ref c) => { + json!([c.color_space.as_str(), c.c1, c.c2, c.c3, c.alpha]) + } + } + } +} + +#[test] +fn generic_parser() { + #[derive(Debug, PartialEq)] + enum OutputType { + CurrentColor, + Rgba(u8, u8, u8, f32), + Hsl(Option, Option, Option, Option), + Hwb(Option, Option, Option, Option), + Lab(Option, Option, Option, Option), + Lch(Option, Option, Option, Option), + Oklab(Option, Option, Option, Option), + Oklch(Option, Option, Option, Option), + ColorFunction( + PredefinedColorSpace, + Option, + Option, + Option, + Option, + ), + } + + impl FromParsedColor for OutputType { + fn from_current_color() -> Self { + OutputType::CurrentColor + } + + fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self { + OutputType::Rgba(red, green, blue, alpha) + } + + fn from_hsl( + hue: Option, + saturation: Option, + lightness: Option, + alpha: Option, + ) -> Self { + OutputType::Hsl(hue, saturation, lightness, alpha) + } + + fn from_hwb( + hue: Option, + blackness: Option, + whiteness: Option, + alpha: Option, + ) -> Self { + OutputType::Hwb(hue, blackness, whiteness, alpha) + } + + fn from_lab( + lightness: Option, + a: Option, + b: Option, + alpha: Option, + ) -> Self { + OutputType::Lab(lightness, a, b, alpha) + } + + fn from_lch( + lightness: Option, + chroma: Option, + hue: Option, + alpha: Option, + ) -> Self { + OutputType::Lch(lightness, chroma, hue, alpha) + } + + fn from_oklab( + lightness: Option, + a: Option, + b: Option, + alpha: Option, + ) -> Self { + OutputType::Oklab(lightness, a, b, alpha) + } + + fn from_oklch( + lightness: Option, + chroma: Option, + hue: Option, + alpha: Option, + ) -> Self { + OutputType::Oklch(lightness, chroma, hue, alpha) + } + + fn from_color_function( + color_space: PredefinedColorSpace, + c1: Option, + c2: Option, + c3: Option, + alpha: Option, + ) -> Self { + OutputType::ColorFunction(color_space, c1, c2, c3, alpha) + } + } + + struct TestColorParser; + impl<'i> ColorParser<'i> for TestColorParser { + type Output = OutputType; + type Error = (); + } + + #[rustfmt::skip] + const TESTS: &[(&str, OutputType)] = &[ + ("currentColor", OutputType::CurrentColor), + ("rgb(1, 2, 3)", OutputType::Rgba(1, 2, 3, 1.0)), + ("rgba(1, 2, 3, 0.4)", OutputType::Rgba(1, 2, 3, 0.4)), + ("rgb(none none none / none)", OutputType::Rgba(0, 0, 0, 0.0)), + ("rgb(1 none 3 / none)", OutputType::Rgba(1, 0, 3, 0.0)), + + ("hsla(45deg, 20%, 30%, 0.4)", OutputType::Hsl(Some(45.0), Some(0.2), Some(0.3), Some(0.4))), + ("hsl(45deg none none)", OutputType::Hsl(Some(45.0), None, None, Some(1.0))), + ("hsl(none 10% none / none)", OutputType::Hsl(None, Some(0.1), None, None)), + ("hsl(120 100.0% 50.0%)", OutputType::Hsl(Some(120.0), Some(1.0), Some(0.5), Some(1.0))), + + ("hwb(45deg 20% 30% / 0.4)", OutputType::Hwb(Some(45.0), Some(0.2), Some(0.3), Some(0.4))), + + ("lab(100 20 30 / 0.4)", OutputType::Lab(Some(100.0), Some(20.0), Some(30.0), Some(0.4))), + ("lch(100 20 30 / 0.4)", OutputType::Lch(Some(100.0), Some(20.0), Some(30.0), Some(0.4))), + + ("oklab(100 20 30 / 0.4)", OutputType::Oklab(Some(100.0), Some(20.0), Some(30.0), Some(0.4))), + ("oklch(100 20 30 / 0.4)", OutputType::Oklch(Some(100.0), Some(20.0), Some(30.0), Some(0.4))), + + ("color(srgb 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::Srgb, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), + ("color(srgb none none none)", OutputType::ColorFunction(PredefinedColorSpace::Srgb, None, None, None, Some(1.0))), + ("color(srgb none none none / none)", OutputType::ColorFunction(PredefinedColorSpace::Srgb, None, None, None, None)), + ("color(srgb-linear 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::SrgbLinear, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), + ("color(display-p3 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::DisplayP3, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), + ("color(a98-rgb 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::A98Rgb, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), + ("color(prophoto-rgb 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::ProphotoRgb, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), + ("color(rec2020 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::Rec2020, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), + ("color(xyz-d50 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::XyzD50, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), + ("color(xyz-d65 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::XyzD65, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), + ]; + + for (input, expected) in TESTS { + let mut input = ParserInput::new(*input); + let mut input = Parser::new(&mut input); + + let actual: OutputType = parse_color_with(&TestColorParser, &mut input).unwrap(); + assert_eq!(actual, *expected); + } +} + +#[test] +fn serialize_modern_components() { + // None. + assert_eq!(ModernComponent(&None).to_css_string(), "none".to_string()); + + // Finite values. + assert_eq!( + ModernComponent(&Some(10.0)).to_css_string(), + "10".to_string() + ); + assert_eq!( + ModernComponent(&Some(-10.0)).to_css_string(), + "-10".to_string() + ); + assert_eq!(ModernComponent(&Some(0.0)).to_css_string(), "0".to_string()); + assert_eq!( + ModernComponent(&Some(-0.0)).to_css_string(), + "0".to_string() + ); + + // Infinite values. + assert_eq!( + ModernComponent(&Some(f32::INFINITY)).to_css_string(), + "calc(infinity)".to_string() + ); + assert_eq!( + ModernComponent(&Some(f32::NEG_INFINITY)).to_css_string(), + "calc(-infinity)".to_string() + ); + + // NaN. + assert_eq!( + ModernComponent(&Some(f32::NAN)).to_css_string(), + "calc(NaN)".to_string() + ); +} diff --git a/src/color.rs b/src/color.rs index 477c40eb..d5f9a5c0 100644 --- a/src/color.rs +++ b/src/color.rs @@ -2,19 +2,48 @@ * 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/. */ +//! General color-parsing utilities, independent on the specific color storage and parsing +//! implementation. +//! +//! For a more complete css-color implementation take a look at cssparser-color crate, or at +//! Gecko's color module. + // Allow text like in docs. #![allow(rustdoc::invalid_html_tags)] -use std::f32::consts::PI; +/// The opaque alpha value of 1.0. +pub const OPAQUE: f32 = 1.0; + +use crate::ToCss; use std::fmt; use std::str::FromStr; -use super::{CowRcStr, ParseError, Parser, ToCss, Token}; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +/// Clamp a 0..1 number to a 0..255 range to u8. +/// +/// Whilst scaling by 256 and flooring would provide +/// an equal distribution of integers to percentage inputs, +/// this is not what Gecko does so we instead multiply by 255 +/// and round (adding 0.5 and flooring is equivalent to rounding) +/// +/// Chrome does something similar for the alpha value, but not +/// the rgb values. +/// +/// See +/// +/// Clamping to 256 and rounding after would let 1.0 map to 256, and +/// `256.0_f32 as u8` is undefined behavior: +/// +/// +#[inline] +pub fn clamp_unit_f32(val: f32) -> u8 { + clamp_floor_256_f32(val * 255.) +} -const OPAQUE: f32 = 1.0; +/// Round and clamp a single number to a u8. +#[inline] +pub fn clamp_floor_256_f32(val: f32) -> u8 { + val.round().clamp(0., 255.) as u8 +} /// Serialize the alpha copmonent of a color according to the specification. /// @@ -45,438 +74,6 @@ pub fn serialize_color_alpha( rounded_alpha.to_css(dest) } -/// A [`ModernComponent`] can serialize to `none`, `nan`, `infinity` and -/// floating point values. -struct ModernComponent<'a>(&'a Option); - -impl<'a> ToCss for ModernComponent<'a> { - fn to_css(&self, dest: &mut W) -> fmt::Result - where - W: fmt::Write, - { - if let Some(value) = self.0 { - if value.is_finite() { - value.to_css(dest) - } else if value.is_nan() { - dest.write_str("calc(NaN)") - } else { - debug_assert!(value.is_infinite()); - if value.is_sign_negative() { - dest.write_str("calc(-infinity)") - } else { - dest.write_str("calc(infinity)") - } - } - } else { - dest.write_str("none") - } - } -} - -// Guaratees hue in [0..360) -fn normalize_hue(hue: f32) -> f32 { - // - // Subtract an integer before rounding, to avoid some rounding errors: - hue - 360.0 * (hue / 360.0).floor() -} - -/// A color with red, green, blue, and alpha components, in a byte each. -#[derive(Clone, Copy, PartialEq, Debug)] -pub struct RgbaLegacy { - /// The red component. - pub red: u8, - /// The green component. - pub green: u8, - /// The blue component. - pub blue: u8, - /// The alpha component. - pub alpha: f32, -} - -impl RgbaLegacy { - /// Constructs a new RGBA value from float components. It expects the red, - /// green, blue and alpha channels in that order, and all values will be - /// clamped to the 0.0 ... 1.0 range. - #[inline] - pub fn from_floats(red: f32, green: f32, blue: f32, alpha: f32) -> Self { - Self::new( - clamp_unit_f32(red), - clamp_unit_f32(green), - clamp_unit_f32(blue), - alpha.clamp(0.0, OPAQUE), - ) - } - - /// Same thing, but with `u8` values instead of floats in the 0 to 1 range. - #[inline] - pub const fn new(red: u8, green: u8, blue: u8, alpha: f32) -> Self { - Self { - red, - green, - blue, - alpha, - } - } -} - -#[cfg(feature = "serde")] -impl Serialize for RgbaLegacy { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - (self.red, self.green, self.blue, self.alpha).serialize(serializer) - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for RgbaLegacy { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let (r, g, b, a) = Deserialize::deserialize(deserializer)?; - Ok(RgbaLegacy::new(r, g, b, a)) - } -} - -impl ToCss for RgbaLegacy { - fn to_css(&self, dest: &mut W) -> fmt::Result - where - W: fmt::Write, - { - let has_alpha = self.alpha != OPAQUE; - - dest.write_str(if has_alpha { "rgba(" } else { "rgb(" })?; - self.red.to_css(dest)?; - dest.write_str(", ")?; - self.green.to_css(dest)?; - dest.write_str(", ")?; - self.blue.to_css(dest)?; - - // Legacy syntax does not allow none components. - serialize_color_alpha(dest, Some(self.alpha), true)?; - - dest.write_char(')') - } -} - -/// Color specified by hue, saturation and lightness components. -#[derive(Clone, Copy, PartialEq, Debug)] -pub struct Hsl { - /// The hue component. - pub hue: Option, - /// The saturation component. - pub saturation: Option, - /// The lightness component. - pub lightness: Option, - /// The alpha component. - pub alpha: Option, -} - -impl Hsl { - /// Construct a new HSL color from it's components. - pub fn new( - hue: Option, - saturation: Option, - lightness: Option, - alpha: Option, - ) -> Self { - Self { - hue, - saturation, - lightness, - alpha, - } - } -} - -impl ToCss for Hsl { - fn to_css(&self, dest: &mut W) -> fmt::Result - where - W: fmt::Write, - { - // HSL serializes to RGB, so we have to convert it. - let (red, green, blue) = hsl_to_rgb( - self.hue.unwrap_or(0.0) / 360.0, - self.saturation.unwrap_or(0.0), - self.lightness.unwrap_or(0.0), - ); - - RgbaLegacy::from_floats(red, green, blue, self.alpha.unwrap_or(OPAQUE)).to_css(dest) - } -} - -#[cfg(feature = "serde")] -impl Serialize for Hsl { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - (self.hue, self.saturation, self.lightness, self.alpha).serialize(serializer) - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for Hsl { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let (lightness, a, b, alpha) = Deserialize::deserialize(deserializer)?; - Ok(Self::new(lightness, a, b, alpha)) - } -} - -/// Color specified by hue, whiteness and blackness components. -#[derive(Clone, Copy, PartialEq, Debug)] -pub struct Hwb { - /// The hue component. - pub hue: Option, - /// The whiteness component. - pub whiteness: Option, - /// The blackness component. - pub blackness: Option, - /// The alpha component. - pub alpha: Option, -} - -impl Hwb { - /// Construct a new HWB color from it's components. - pub fn new( - hue: Option, - whiteness: Option, - blackness: Option, - alpha: Option, - ) -> Self { - Self { - hue, - whiteness, - blackness, - alpha, - } - } -} - -impl ToCss for Hwb { - fn to_css(&self, dest: &mut W) -> fmt::Result - where - W: fmt::Write, - { - // HWB serializes to RGB, so we have to convert it. - let (red, green, blue) = hwb_to_rgb( - self.hue.unwrap_or(0.0) / 360.0, - self.whiteness.unwrap_or(0.0), - self.blackness.unwrap_or(0.0), - ); - - RgbaLegacy::from_floats(red, green, blue, self.alpha.unwrap_or(OPAQUE)).to_css(dest) - } -} - -#[cfg(feature = "serde")] -impl Serialize for Hwb { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - (self.hue, self.whiteness, self.blackness, self.alpha).serialize(serializer) - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for Hwb { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let (lightness, whiteness, blackness, alpha) = Deserialize::deserialize(deserializer)?; - Ok(Self::new(lightness, whiteness, blackness, alpha)) - } -} - -// NOTE: LAB and OKLAB is not declared inside the [impl_lab_like] macro, -// because it causes cbindgen to ignore them. - -/// Color specified by lightness, a- and b-axis components. -#[derive(Clone, Copy, PartialEq, Debug)] -pub struct Lab { - /// The lightness component. - pub lightness: Option, - /// The a-axis component. - pub a: Option, - /// The b-axis component. - pub b: Option, - /// The alpha component. - pub alpha: Option, -} - -/// Color specified by lightness, a- and b-axis components. -#[derive(Clone, Copy, PartialEq, Debug)] -pub struct Oklab { - /// The lightness component. - pub lightness: Option, - /// The a-axis component. - pub a: Option, - /// The b-axis component. - pub b: Option, - /// The alpha component. - pub alpha: Option, -} - -macro_rules! impl_lab_like { - ($cls:ident, $fname:literal) => { - impl $cls { - /// Construct a new Lab color format with lightness, a, b and alpha components. - pub fn new( - lightness: Option, - a: Option, - b: Option, - alpha: Option, - ) -> Self { - Self { - lightness, - a, - b, - alpha, - } - } - } - - #[cfg(feature = "serde")] - impl Serialize for $cls { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - (self.lightness, self.a, self.b, self.alpha).serialize(serializer) - } - } - - #[cfg(feature = "serde")] - impl<'de> Deserialize<'de> for $cls { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let (lightness, a, b, alpha) = Deserialize::deserialize(deserializer)?; - Ok(Self::new(lightness, a, b, alpha)) - } - } - - impl ToCss for $cls { - fn to_css(&self, dest: &mut W) -> fmt::Result - where - W: fmt::Write, - { - dest.write_str($fname)?; - dest.write_str("(")?; - ModernComponent(&self.lightness).to_css(dest)?; - dest.write_char(' ')?; - ModernComponent(&self.a).to_css(dest)?; - dest.write_char(' ')?; - ModernComponent(&self.b).to_css(dest)?; - serialize_color_alpha(dest, self.alpha, false)?; - dest.write_char(')') - } - } - }; -} - -impl_lab_like!(Lab, "lab"); -impl_lab_like!(Oklab, "oklab"); - -// NOTE: LCH and OKLCH is not declared inside the [impl_lch_like] macro, -// because it causes cbindgen to ignore them. - -/// Color specified by lightness, chroma and hue components. -#[derive(Clone, Copy, PartialEq, Debug)] -pub struct Lch { - /// The lightness component. - pub lightness: Option, - /// The chroma component. - pub chroma: Option, - /// The hue component. - pub hue: Option, - /// The alpha component. - pub alpha: Option, -} - -/// Color specified by lightness, chroma and hue components. -#[derive(Clone, Copy, PartialEq, Debug)] -pub struct Oklch { - /// The lightness component. - pub lightness: Option, - /// The chroma component. - pub chroma: Option, - /// The hue component. - pub hue: Option, - /// The alpha component. - pub alpha: Option, -} - -macro_rules! impl_lch_like { - ($cls:ident, $fname:literal) => { - impl $cls { - /// Construct a new color with lightness, chroma and hue components. - pub fn new( - lightness: Option, - chroma: Option, - hue: Option, - alpha: Option, - ) -> Self { - Self { - lightness, - chroma, - hue, - alpha, - } - } - } - - #[cfg(feature = "serde")] - impl Serialize for $cls { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - (self.lightness, self.chroma, self.hue, self.alpha).serialize(serializer) - } - } - - #[cfg(feature = "serde")] - impl<'de> Deserialize<'de> for $cls { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let (lightness, chroma, hue, alpha) = Deserialize::deserialize(deserializer)?; - Ok(Self::new(lightness, chroma, hue, alpha)) - } - } - - impl ToCss for $cls { - fn to_css(&self, dest: &mut W) -> fmt::Result - where - W: fmt::Write, - { - dest.write_str($fname)?; - dest.write_str("(")?; - ModernComponent(&self.lightness).to_css(dest)?; - dest.write_char(' ')?; - ModernComponent(&self.chroma).to_css(dest)?; - dest.write_char(' ')?; - ModernComponent(&self.hue).to_css(dest)?; - serialize_color_alpha(dest, self.alpha, false)?; - dest.write_char(')') - } - } - }; -} - -impl_lch_like!(Lch, "lch"); -impl_lch_like!(Oklch, "oklch"); - /// A Predefined color space specified in: /// #[derive(Clone, Copy, PartialEq, Debug)] @@ -543,352 +140,29 @@ impl ToCss for PredefinedColorSpace { } } -/// A color specified by the color() function. -/// -#[derive(Clone, Copy, PartialEq, Debug)] -pub struct ColorFunction { - /// The color space for this color. - pub color_space: PredefinedColorSpace, - /// The first component of the color. Either red or x. - pub c1: Option, - /// The second component of the color. Either green or y. - pub c2: Option, - /// The third component of the color. Either blue or z. - pub c3: Option, - /// The alpha component of the color. - pub alpha: Option, -} - -impl ColorFunction { - /// Construct a new color function definition with the given color space and - /// color components. - pub fn new( - color_space: PredefinedColorSpace, - c1: Option, - c2: Option, - c3: Option, - alpha: Option, - ) -> Self { - Self { - color_space, - c1, - c2, - c3, - alpha, - } - } -} - -impl ToCss for ColorFunction { - fn to_css(&self, dest: &mut W) -> fmt::Result - where - W: fmt::Write, - { - dest.write_str("color(")?; - self.color_space.to_css(dest)?; - dest.write_char(' ')?; - ModernComponent(&self.c1).to_css(dest)?; - dest.write_char(' ')?; - ModernComponent(&self.c2).to_css(dest)?; - dest.write_char(' ')?; - ModernComponent(&self.c3).to_css(dest)?; - - serialize_color_alpha(dest, self.alpha, false)?; - - dest.write_char(')') - } -} - -/// Describes one of the value values according to the CSS -/// specification. -/// -/// Most components are `Option<_>`, so when the value is `None`, that component -/// serializes to the "none" keyword. -/// -/// -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum Color { - /// The 'currentcolor' keyword. - CurrentColor, - /// Specify sRGB colors directly by their red/green/blue/alpha chanels. - Rgba(RgbaLegacy), - /// Specifies a color in sRGB using hue, saturation and lightness components. - Hsl(Hsl), - /// Specifies a color in sRGB using hue, whiteness and blackness components. - Hwb(Hwb), - /// Specifies a CIELAB color by CIE Lightness and its a- and b-axis hue - /// coordinates (red/green-ness, and yellow/blue-ness) using the CIE LAB - /// rectangular coordinate model. - Lab(Lab), - /// Specifies a CIELAB color by CIE Lightness, Chroma, and hue using the - /// CIE LCH cylindrical coordinate model. - Lch(Lch), - /// Specifies an Oklab color by Oklab Lightness and its a- and b-axis hue - /// coordinates (red/green-ness, and yellow/blue-ness) using the Oklab - /// rectangular coordinate model. - Oklab(Oklab), - /// Specifies an Oklab color by Oklab Lightness, Chroma, and hue using - /// the OKLCH cylindrical coordinate model. - Oklch(Oklch), - /// Specifies a color in a predefined color space. - ColorFunction(ColorFunction), -} - -impl ToCss for Color { - fn to_css(&self, dest: &mut W) -> fmt::Result - where - W: fmt::Write, - { - match *self { - Color::CurrentColor => dest.write_str("currentcolor"), - Color::Rgba(rgba) => rgba.to_css(dest), - Color::Hsl(hsl) => hsl.to_css(dest), - Color::Hwb(hwb) => hwb.to_css(dest), - Color::Lab(lab) => lab.to_css(dest), - Color::Lch(lch) => lch.to_css(dest), - Color::Oklab(lab) => lab.to_css(dest), - Color::Oklch(lch) => lch.to_css(dest), - Color::ColorFunction(color_function) => color_function.to_css(dest), - } - } -} - -/// Either a number or a percentage. -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 percentage. - pub fn unit_value(&self) -> f32 { - match *self { - NumberOrPercentage::Number { value } => value, - NumberOrPercentage::Percentage { unit_value } => unit_value, - } - } - - /// Return the value as a number with a percentage adjusted to the - /// `percentage_basis`. - pub fn value(&self, percentage_basis: f32) -> f32 { - match *self { - Self::Number { value } => value, - Self::Percentage { unit_value } => unit_value * percentage_basis, - } - } -} - -/// Either an angle or a number. -pub enum AngleOrNumber { - /// ``. - Number { - /// The numeric value parsed, as a float. - value: f32, - }, - /// `` - Angle { - /// The value as a number of degrees. - degrees: f32, - }, -} - -impl AngleOrNumber { - /// Return the angle in degrees. `AngleOrNumber::Number` is returned as - /// degrees, because it is the canonical unit. - pub fn degrees(&self) -> f32 { - match *self { - AngleOrNumber::Number { value } => value, - AngleOrNumber::Angle { degrees } => degrees, - } - } -} - -/// A trait that can be used to hook into how `cssparser` parses color -/// components, with the intention of implementing more complicated behavior. -/// -/// For example, this is used by Servo to support calc() in color. -pub trait ColorParser<'i> { - /// The type that the parser will construct on a successful parse. - type Output: FromParsedColor; - - /// A custom error type that can be returned from the parsing functions. - type Error: 'i; - - /// Parse an `` or ``. - /// - /// Returns the result in degrees. - fn parse_angle_or_number<'t>( - &self, - input: &mut Parser<'i, 't>, - ) -> Result> { - let location = input.current_source_location(); - Ok(match *input.next()? { - Token::Number { value, .. } => AngleOrNumber::Number { value }, - Token::Dimension { - value: v, ref unit, .. - } => { - let degrees = match_ignore_ascii_case! { unit, - "deg" => v, - "grad" => v * 360. / 400., - "rad" => v * 360. / (2. * PI), - "turn" => v * 360., - _ => { - return Err(location.new_unexpected_token_error(Token::Ident(unit.clone()))) - } - }; - - AngleOrNumber::Angle { degrees } - } - ref t => return Err(location.new_unexpected_token_error(t.clone())), - }) - } - - /// Parse a `` value. - /// - /// Returns the result in a number from 0.0 to 1.0. - fn parse_percentage<'t>( - &self, - input: &mut Parser<'i, 't>, - ) -> Result> { - input.expect_percentage().map_err(From::from) - } - - /// Parse a `` value. - fn parse_number<'t>( - &self, - input: &mut Parser<'i, 't>, - ) -> Result> { - input.expect_number().map_err(From::from) - } - - /// Parse a `` value or a `` value. - fn parse_number_or_percentage<'t>( - &self, - input: &mut Parser<'i, 't>, - ) -> Result> { - let location = input.current_source_location(); - Ok(match *input.next()? { - Token::Number { value, .. } => NumberOrPercentage::Number { value }, - Token::Percentage { unit_value, .. } => NumberOrPercentage::Percentage { unit_value }, - ref t => return Err(location.new_unexpected_token_error(t.clone())), - }) - } -} - -/// Default implementation of a [`ColorParser`] -pub struct DefaultColorParser; - -impl<'i> ColorParser<'i> for DefaultColorParser { - type Output = Color; - type Error = (); -} - -impl Color { - /// Parse a value, per CSS Color Module Level 3. - /// - /// FIXME(#2) Deprecated CSS2 System Colors are not supported yet. - pub fn parse<'i>(input: &mut Parser<'i, '_>) -> Result> { - parse_color_with(&DefaultColorParser, input) - } -} - -/// 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: u8, green: u8, blue: u8, alpha: f32) -> Self; - - /// Construct a new color from hue, saturation, lightness and alpha components. - fn from_hsl( - hue: Option, - saturation: Option, - lightness: Option, - alpha: Option, - ) -> Self; - - /// Construct a new color from hue, blackness, whiteness and alpha components. - fn from_hwb( - hue: Option, - whiteness: Option, - blackness: Option, - alpha: Option, - ) -> Self; - - /// Construct a new color from the `lab` notation. - fn from_lab(lightness: Option, a: Option, b: Option, alpha: Option) - -> Self; - - /// Construct a new color from the `lch` notation. - fn from_lch( - lightness: Option, - chroma: Option, - hue: Option, - alpha: Option, - ) -> Self; - - /// Construct a new color from the `oklab` notation. - fn from_oklab( - lightness: Option, - a: Option, - b: Option, - alpha: Option, - ) -> Self; - - /// Construct a new color from the `oklch` notation. - fn from_oklch( - lightness: Option, - chroma: Option, - hue: Option, - alpha: Option, - ) -> Self; - - /// Construct a new color with a predefined color space. - fn from_color_function( - color_space: PredefinedColorSpace, - c1: Option, - c2: Option, - c3: Option, - alpha: Option, - ) -> Self; -} - /// Parse a color hash, without the leading '#' character. #[inline] -pub fn parse_hash_color<'i, O>(value: &[u8]) -> Result -where - O: FromParsedColor, -{ +pub fn parse_hash_color(value: &[u8]) -> Result<(u8, u8, u8, f32), ()> { Ok(match value.len() { - 8 => O::from_rgba( + 8 => ( from_hex(value[0])? * 16 + from_hex(value[1])?, from_hex(value[2])? * 16 + from_hex(value[3])?, from_hex(value[4])? * 16 + from_hex(value[5])?, (from_hex(value[6])? * 16 + from_hex(value[7])?) as f32 / 255.0, ), - 6 => O::from_rgba( + 6 => ( from_hex(value[0])? * 16 + from_hex(value[1])?, from_hex(value[2])? * 16 + from_hex(value[3])?, from_hex(value[4])? * 16 + from_hex(value[5])?, OPAQUE, ), - 4 => O::from_rgba( + 4 => ( from_hex(value[0])? * 17, from_hex(value[1])? * 17, from_hex(value[2])? * 17, (from_hex(value[3])? * 17) as f32 / 255.0, ), - 3 => O::from_rgba( + 3 => ( from_hex(value[0])? * 17, from_hex(value[1])? * 17, from_hex(value[2])? * 17, @@ -898,112 +172,6 @@ where }) } -/// Parse a CSS color using the specified [`ColorParser`] and return a new color -/// value on success. -pub fn parse_color_with<'i, 't, P>( - color_parser: &P, - input: &mut Parser<'i, 't>, -) -> Result> -where - P: ColorParser<'i>, -{ - 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()), - Token::Ident(ref value) => parse_color_keyword(value), - Token::Function(ref name) => { - let name = name.clone(); - return input.parse_nested_block(|arguments| { - parse_color_function(color_parser, name, arguments) - }); - } - _ => Err(()), - } - .map_err(|()| location.new_unexpected_token_error(token.clone())) -} - -impl FromParsedColor for Color { - #[inline] - fn from_current_color() -> Self { - Color::CurrentColor - } - - #[inline] - fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self { - Color::Rgba(RgbaLegacy::new(red, green, blue, alpha)) - } - - fn from_hsl( - hue: Option, - saturation: Option, - lightness: Option, - alpha: Option, - ) -> Self { - Color::Hsl(Hsl::new(hue, saturation, lightness, alpha)) - } - - fn from_hwb( - hue: Option, - blackness: Option, - whiteness: Option, - alpha: Option, - ) -> Self { - Color::Hwb(Hwb::new(hue, blackness, whiteness, alpha)) - } - - #[inline] - fn from_lab( - lightness: Option, - a: Option, - b: Option, - alpha: Option, - ) -> Self { - Color::Lab(Lab::new(lightness, a, b, alpha)) - } - - #[inline] - fn from_lch( - lightness: Option, - chroma: Option, - hue: Option, - alpha: Option, - ) -> Self { - Color::Lch(Lch::new(lightness, chroma, hue, alpha)) - } - - #[inline] - fn from_oklab( - lightness: Option, - a: Option, - b: Option, - alpha: Option, - ) -> Self { - Color::Oklab(Oklab::new(lightness, a, b, alpha)) - } - - #[inline] - fn from_oklch( - lightness: Option, - chroma: Option, - hue: Option, - alpha: Option, - ) -> Self { - Color::Oklch(Oklch::new(lightness, chroma, hue, alpha)) - } - - #[inline] - fn from_color_function( - color_space: PredefinedColorSpace, - c1: Option, - c2: Option, - c3: Option, - alpha: Option, - ) -> Self { - Color::ColorFunction(ColorFunction::new(color_space, c1, c2, c3, alpha)) - } -} - ascii_case_insensitive_phf_map! { named_colors -> (u8, u8, u8) = { "black" => (0, 0, 0), @@ -1161,12 +329,8 @@ ascii_case_insensitive_phf_map! { /// Returns the named color with the given name. /// #[inline] -pub fn parse_named_color(ident: &str) -> Result -where - Output: FromParsedColor, -{ - let &(r, g, b) = named_colors::get(ident).ok_or(())?; - Ok(Output::from_rgba(r, g, b, 1.0)) +pub fn parse_named_color(ident: &str) -> Result<(u8, u8, u8), ()> { + named_colors::get(ident).copied().ok_or(()) } /// Returns an iterator over all named CSS colors. @@ -1176,23 +340,6 @@ pub fn all_named_colors() -> impl Iterator named_colors::entries().map(|(k, v)| (*k, *v)) } -/// 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 -where - Output: FromParsedColor, -{ - Ok(match_ignore_ascii_case! { ident , - "transparent" => Output::from_rgba(0, 0, 0, 0.0), - "currentcolor" => Output::from_current_color(), - _ => return parse_named_color(ident), - }) -} - #[inline] fn from_hex(c: u8) -> Result { match c { @@ -1202,466 +349,3 @@ fn from_hex(c: u8) -> Result { _ => Err(()), } } - -fn clamp_unit_f32(val: f32) -> u8 { - // Whilst scaling by 256 and flooring would provide - // an equal distribution of integers to percentage inputs, - // this is not what Gecko does so we instead multiply by 255 - // and round (adding 0.5 and flooring is equivalent to rounding) - // - // Chrome does something similar for the alpha value, but not - // the rgb values. - // - // See - // - // Clamping to 256 and rounding after would let 1.0 map to 256, and - // `256.0_f32 as u8` is undefined behavior: - // - // - clamp_floor_256_f32(val * 255.) -} - -fn clamp_floor_256_f32(val: f32) -> u8 { - val.round().clamp(0., 255.) as u8 -} - -fn parse_none_or<'i, 't, F, T, E>(input: &mut Parser<'i, 't>, thing: F) -> Result, E> -where - F: FnOnce(&mut Parser<'i, 't>) -> Result, -{ - match input.try_parse(|p| p.expect_ident_matching("none")) { - Ok(_) => Ok(None), - Err(_) => Ok(Some(thing(input)?)), - } -} - -/// Parse one of the color functions: rgba(), lab(), color(), etc. -#[inline] -fn parse_color_function<'i, 't, P>( - color_parser: &P, - name: CowRcStr<'i>, - arguments: &mut Parser<'i, 't>, -) -> Result> -where - P: ColorParser<'i>, -{ - 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), - - // for L: 0% = 0.0, 100% = 100.0 - // for a and b: -100% = -125, 100% = 125 - "lab" => parse_lab_like(color_parser, arguments, 100.0, 125.0, P::Output::from_lab), - - // for L: 0% = 0.0, 100% = 100.0 - // for C: 0% = 0, 100% = 150 - "lch" => parse_lch_like(color_parser, arguments, 100.0, 150.0, P::Output::from_lch), - - // for L: 0% = 0.0, 100% = 1.0 - // for a and b: -100% = -0.4, 100% = 0.4 - "oklab" => parse_lab_like(color_parser, arguments, 1.0, 0.4, P::Output::from_oklab), - - // for L: 0% = 0.0, 100% = 1.0 - // for C: 0% = 0.0 100% = 0.4 - "oklch" => parse_lch_like(color_parser, arguments, 1.0, 0.4, P::Output::from_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) -} - -/// Parse the alpha component by itself from either number or percentage, -/// clipping the result to [0.0..1.0]. -#[inline] -fn parse_alpha_component<'i, 't, P>( - color_parser: &P, - arguments: &mut Parser<'i, 't>, -) -> Result> -where - P: ColorParser<'i>, -{ - Ok(color_parser - .parse_number_or_percentage(arguments)? - .unit_value() - .clamp(0.0, OPAQUE)) -} - -fn parse_legacy_alpha<'i, 't, P>( - color_parser: &P, - arguments: &mut Parser<'i, 't>, -) -> Result> -where - P: ColorParser<'i>, -{ - Ok(if !arguments.is_exhausted() { - arguments.expect_comma()?; - parse_alpha_component(color_parser, arguments)? - } else { - OPAQUE - }) -} - -fn parse_modern_alpha<'i, 't, P>( - color_parser: &P, - arguments: &mut Parser<'i, 't>, -) -> Result, ParseError<'i, P::Error>> -where - P: ColorParser<'i>, -{ - if !arguments.is_exhausted() { - arguments.expect_delim('/')?; - parse_none_or(arguments, |p| parse_alpha_component(color_parser, p)) - } else { - Ok(Some(OPAQUE)) - } -} - -#[inline] -fn parse_rgb<'i, 't, P>( - color_parser: &P, - arguments: &mut Parser<'i, 't>, -) -> Result> -where - P: ColorParser<'i>, -{ - let maybe_red = parse_none_or(arguments, |p| color_parser.parse_number_or_percentage(p))?; - - // If the first component is not "none" and is followed by a comma, then we - // are parsing the legacy syntax. - let is_legacy_syntax = maybe_red.is_some() && arguments.try_parse(|p| p.expect_comma()).is_ok(); - - let (red, green, blue, alpha) = if is_legacy_syntax { - let (red, green, blue) = match maybe_red.unwrap() { - NumberOrPercentage::Number { value } => { - let red = clamp_floor_256_f32(value); - let green = clamp_floor_256_f32(color_parser.parse_number(arguments)?); - arguments.expect_comma()?; - let blue = clamp_floor_256_f32(color_parser.parse_number(arguments)?); - (red, green, blue) - } - NumberOrPercentage::Percentage { unit_value } => { - let red = clamp_unit_f32(unit_value); - let green = clamp_unit_f32(color_parser.parse_percentage(arguments)?); - arguments.expect_comma()?; - let blue = clamp_unit_f32(color_parser.parse_percentage(arguments)?); - (red, green, blue) - } - }; - - let alpha = parse_legacy_alpha(color_parser, arguments)?; - - (red, green, blue, alpha) - } else { - #[inline] - fn get_component_value(c: Option) -> u8 { - c.map(|c| match c { - NumberOrPercentage::Number { value } => clamp_floor_256_f32(value), - NumberOrPercentage::Percentage { unit_value } => clamp_unit_f32(unit_value), - }) - .unwrap_or(0) - } - - let red = get_component_value(maybe_red); - - let green = get_component_value(parse_none_or(arguments, |p| { - color_parser.parse_number_or_percentage(p) - })?); - - let blue = get_component_value(parse_none_or(arguments, |p| { - color_parser.parse_number_or_percentage(p) - })?); - - let alpha = parse_modern_alpha(color_parser, arguments)?.unwrap_or(0.0); - - (red, green, blue, alpha) - }; - - Ok(P::Output::from_rgba(red, green, blue, alpha)) -} - -/// Parses hsl syntax. -/// -/// -#[inline] -fn parse_hsl<'i, 't, P>( - color_parser: &P, - arguments: &mut Parser<'i, 't>, -) -> Result> -where - P: ColorParser<'i>, -{ - let maybe_hue = parse_none_or(arguments, |p| color_parser.parse_angle_or_number(p))?; - - // If the hue is not "none" and is followed by a comma, then we are parsing - // the legacy syntax. - let is_legacy_syntax = maybe_hue.is_some() && arguments.try_parse(|p| p.expect_comma()).is_ok(); - - let saturation: Option; - let lightness: Option; - - let alpha = if is_legacy_syntax { - saturation = Some(color_parser.parse_percentage(arguments)?); - arguments.expect_comma()?; - lightness = Some(color_parser.parse_percentage(arguments)?); - Some(parse_legacy_alpha(color_parser, arguments)?) - } else { - saturation = parse_none_or(arguments, |p| color_parser.parse_percentage(p))?; - lightness = parse_none_or(arguments, |p| color_parser.parse_percentage(p))?; - - parse_modern_alpha(color_parser, arguments)? - }; - - let hue = maybe_hue.map(|h| normalize_hue(h.degrees())); - let saturation = saturation.map(|s| s.clamp(0.0, 1.0)); - let lightness = lightness.map(|l| l.clamp(0.0, 1.0)); - - Ok(P::Output::from_hsl(hue, saturation, lightness, alpha)) -} - -/// Parses hwb syntax. -/// -/// -#[inline] -fn parse_hwb<'i, 't, P>( - color_parser: &P, - arguments: &mut Parser<'i, 't>, -) -> Result> -where - P: ColorParser<'i>, -{ - let (hue, whiteness, blackness, alpha) = parse_components( - color_parser, - arguments, - P::parse_angle_or_number, - P::parse_percentage, - P::parse_percentage, - )?; - - let hue = hue.map(|h| normalize_hue(h.degrees())); - let whiteness = whiteness.map(|w| w.clamp(0.0, 1.0)); - let blackness = blackness.map(|b| b.clamp(0.0, 1.0)); - - Ok(P::Output::from_hwb(hue, whiteness, blackness, alpha)) -} - -/// -#[inline] -pub fn hwb_to_rgb(h: f32, w: f32, b: f32) -> (f32, f32, f32) { - if w + b >= 1.0 { - let gray = w / (w + b); - return (gray, gray, gray); - } - - // hue is expected in the range [0..1]. - let (mut red, mut green, mut blue) = hsl_to_rgb(h, 1.0, 0.5); - let x = 1.0 - w - b; - red = red * x + w; - green = green * x + w; - blue = blue * x + w; - (red, green, blue) -} - -/// -/// except with h pre-multiplied by 3, to avoid some rounding errors. -#[inline] -pub fn hsl_to_rgb(hue: f32, saturation: f32, lightness: f32) -> (f32, f32, f32) { - debug_assert!((0.0..=1.0).contains(&hue)); - - fn hue_to_rgb(m1: f32, m2: f32, mut h3: f32) -> f32 { - if h3 < 0. { - h3 += 3. - } - if h3 > 3. { - h3 -= 3. - } - if h3 * 2. < 1. { - m1 + (m2 - m1) * h3 * 2. - } else if h3 * 2. < 3. { - m2 - } else if h3 < 2. { - m1 + (m2 - m1) * (2. - h3) * 2. - } else { - m1 - } - } - let m2 = if lightness <= 0.5 { - lightness * (saturation + 1.) - } else { - lightness + saturation - lightness * saturation - }; - let m1 = lightness * 2. - m2; - let hue_times_3 = hue * 3.; - let red = hue_to_rgb(m1, m2, hue_times_3 + 1.); - let green = hue_to_rgb(m1, m2, hue_times_3); - let blue = hue_to_rgb(m1, m2, hue_times_3 - 1.); - (red, green, blue) -} - -type IntoColorFn = - fn(l: Option, a: Option, b: Option, alpha: Option) -> Output; - -#[inline] -fn parse_lab_like<'i, 't, P>( - color_parser: &P, - arguments: &mut Parser<'i, 't>, - lightness_range: f32, - a_b_range: f32, - into_color: IntoColorFn, -) -> Result> -where - P: ColorParser<'i>, -{ - let (lightness, a, b, alpha) = parse_components( - color_parser, - arguments, - P::parse_number_or_percentage, - P::parse_number_or_percentage, - P::parse_number_or_percentage, - )?; - - let lightness = lightness.map(|l| l.value(lightness_range)); - let a = a.map(|a| a.value(a_b_range)); - let b = b.map(|b| b.value(a_b_range)); - - Ok(into_color(lightness, a, b, alpha)) -} - -#[inline] -fn parse_lch_like<'i, 't, P>( - color_parser: &P, - arguments: &mut Parser<'i, 't>, - lightness_range: f32, - chroma_range: f32, - into_color: IntoColorFn, -) -> Result> -where - P: ColorParser<'i>, -{ - let (lightness, chroma, hue, alpha) = parse_components( - color_parser, - arguments, - P::parse_number_or_percentage, - P::parse_number_or_percentage, - P::parse_angle_or_number, - )?; - - let lightness = lightness.map(|l| l.value(lightness_range)); - let chroma = chroma.map(|c| c.value(chroma_range)); - let hue = hue.map(|h| normalize_hue(h.degrees())); - - Ok(into_color(lightness, chroma, hue, alpha)) -} - -/// Parse the color() function. -#[inline] -fn parse_color_with_color_space<'i, 't, P>( - color_parser: &P, - arguments: &mut Parser<'i, 't>, -) -> Result> -where - P: ColorParser<'i>, -{ - 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, - P::parse_number_or_percentage, - P::parse_number_or_percentage, - P::parse_number_or_percentage, - )?; - - let c1 = c1.map(|c| c.unit_value()); - let c2 = c2.map(|c| c.unit_value()); - let c3 = c3.map(|c| c.unit_value()); - - Ok(P::Output::from_color_function( - color_space, - c1, - c2, - c3, - alpha, - )) -} - -type ComponentParseResult<'i, R1, R2, R3, Error> = - Result<(Option, Option, Option, Option), ParseError<'i, Error>>; - -/// Parse the color components and alpha with the modern [color-4] syntax. -pub fn parse_components<'i, 't, P, F1, F2, F3, R1, R2, R3>( - color_parser: &P, - input: &mut Parser<'i, 't>, - f1: F1, - f2: F2, - f3: F3, -) -> ComponentParseResult<'i, R1, R2, R3, P::Error> -where - P: ColorParser<'i>, - F1: FnOnce(&P, &mut Parser<'i, 't>) -> Result>, - F2: FnOnce(&P, &mut Parser<'i, 't>) -> Result>, - F3: FnOnce(&P, &mut Parser<'i, 't>) -> Result>, -{ - let r1 = parse_none_or(input, |p| f1(color_parser, p))?; - let r2 = parse_none_or(input, |p| f2(color_parser, p))?; - let r3 = parse_none_or(input, |p| f3(color_parser, p))?; - - let alpha = parse_modern_alpha(color_parser, input)?; - - Ok((r1, r2, r3, alpha)) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn serialize_modern_components() { - // None. - assert_eq!(ModernComponent(&None).to_css_string(), "none".to_string()); - - // Finite values. - assert_eq!( - ModernComponent(&Some(10.0)).to_css_string(), - "10".to_string() - ); - assert_eq!( - ModernComponent(&Some(-10.0)).to_css_string(), - "-10".to_string() - ); - assert_eq!(ModernComponent(&Some(0.0)).to_css_string(), "0".to_string()); - assert_eq!( - ModernComponent(&Some(-0.0)).to_css_string(), - "0".to_string() - ); - - // Infinite values. - assert_eq!( - ModernComponent(&Some(f32::INFINITY)).to_css_string(), - "calc(infinity)".to_string() - ); - assert_eq!( - ModernComponent(&Some(f32::NEG_INFINITY)).to_css_string(), - "calc(-infinity)".to_string() - ); - - // NaN. - assert_eq!( - ModernComponent(&Some(f32::NAN)).to_css_string(), - "calc(NaN)".to_string() - ); - } -} diff --git a/src/lib.rs b/src/lib.rs index 143ec87b..dc44fb74 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,12 +67,6 @@ fn parse_border_spacing(_context: &ParserContext, input: &mut Parser) #![recursion_limit = "200"] // For color::parse_color_keyword -pub use crate::color::{ - all_named_colors, hsl_to_rgb, hwb_to_rgb, parse_color_keyword, parse_color_with, - parse_hash_color, parse_named_color, serialize_color_alpha, AngleOrNumber, Color, - ColorFunction, ColorParser, FromParsedColor, Hsl, Hwb, Lab, Lch, NumberOrPercentage, Oklab, - Oklch, PredefinedColorSpace, RgbaLegacy, -}; pub use crate::cow_rc_str::CowRcStr; pub use crate::from_bytes::{stylesheet_encoding, EncodingSupport}; #[doc(hidden)] @@ -100,7 +94,7 @@ mod macros; mod rules_and_declarations; mod tokenizer; -mod color; +pub mod color; mod cow_rc_str; mod from_bytes; mod nth; diff --git a/src/tests.rs b/src/tests.rs index ae17bd81..d4bc5f51 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -8,18 +8,15 @@ extern crate test; use encoding_rs; use serde_json::{self, json, Map, Value}; -use crate::color::{parse_color_with, FromParsedColor}; -use crate::{ColorParser, PredefinedColorSpace, Delimiters}; - #[cfg(feature = "bench")] use self::test::Bencher; use super::{ parse_important, parse_nth, parse_one_declaration, parse_one_rule, stylesheet_encoding, - AtRuleParser, BasicParseError, BasicParseErrorKind, Color, CowRcStr, DeclarationParser, - Delimiter, EncodingSupport, ParseError, ParseErrorKind, Parser, ParserInput, ParserState, - QualifiedRuleParser, RgbaLegacy, RuleBodyItemParser, RuleBodyParser, SourceLocation, - StyleSheetParser, ToCss, Token, TokenSerializationType, UnicodeRange, + AtRuleParser, BasicParseError, BasicParseErrorKind, CowRcStr, DeclarationParser, Delimiter, + EncodingSupport, ParseError, ParseErrorKind, Parser, ParserInput, ParserState, + QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, SourceLocation, StyleSheetParser, + ToCss, Token, TokenSerializationType, UnicodeRange, }; macro_rules! JArray { @@ -356,96 +353,6 @@ fn test_expect_url() { assert!(parse(&mut input).is_err()); } -fn run_color_tests) -> Value>(json_data: &str, to_json: F) { - run_json_tests(json_data, |input| { - let result: Result<_, ParseError<()>> = - input.parse_entirely(|i| Color::parse(i).map_err(Into::into)); - to_json(result.map_err(|_| ())) - }); -} - -#[test] -fn color3() { - run_color_tests(include_str!("css-parsing-tests/color3.json"), |c| { - c.ok() - .map(|v| v.to_css_string().to_json()) - .unwrap_or(Value::Null) - }) -} - -#[cfg_attr(all(miri, feature = "skip_long_tests"), ignore)] -#[test] -fn color3_hsl() { - run_color_tests(include_str!("css-parsing-tests/color3_hsl.json"), |c| { - c.ok() - .map(|v| v.to_css_string().to_json()) - .unwrap_or(Value::Null) - }) -} - -/// color3_keywords.json is different: R, G and B are in 0..255 rather than 0..1 -#[test] -fn color3_keywords() { - run_color_tests( - include_str!("css-parsing-tests/color3_keywords.json"), - |c| { - c.ok() - .map(|v| v.to_css_string().to_json()) - .unwrap_or(Value::Null) - }, - ) -} - -#[cfg_attr(all(miri, feature = "skip_long_tests"), ignore)] -#[test] -fn color4_hwb() { - run_color_tests(include_str!("css-parsing-tests/color4_hwb.json"), |c| { - c.ok() - .map(|v| v.to_css_string().to_json()) - .unwrap_or(Value::Null) - }) -} - -#[cfg_attr(all(miri, feature = "skip_long_tests"), ignore)] -#[test] -fn color4_lab_lch_oklab_oklch() { - run_color_tests( - include_str!("css-parsing-tests/color4_lab_lch_oklab_oklch.json"), - |c| { - c.ok() - .map(|v| v.to_css_string().to_json()) - .unwrap_or(Value::Null) - }, - ) -} - -#[test] -fn color4_color_function() { - run_color_tests( - include_str!("css-parsing-tests/color4_color_function.json"), - |c| { - c.ok() - .map(|v| v.to_css_string().to_json()) - .unwrap_or(Value::Null) - }, - ) -} - -macro_rules! parse_single_color { - ($i:expr) => {{ - let input = $i; - let mut input = ParserInput::new(input); - let mut input = Parser::new(&mut input); - Color::parse(&mut input).map_err(Into::>::into) - }}; -} - -#[test] -fn color4_invalid_color_space() { - let result = parse_single_color!("color(invalid 1 1 1)"); - assert!(result.is_err()); -} - #[test] fn nth() { run_json_tests(include_str!("css-parsing-tests/An+B.json"), |input| { @@ -466,12 +373,16 @@ fn parse_comma_separated_ignoring_errors() { let mut input = ParserInput::new(input); let mut input = Parser::new(&mut input); let result = input.parse_comma_separated_ignoring_errors(|input| { - Color::parse(input).map_err(Into::>::into) + let loc = input.current_source_location(); + let ident = input.expect_ident()?; + crate::color::parse_named_color(ident).map_err(|()| { + loc.new_unexpected_token_error::>(Token::Ident(ident.clone())) + }) }); assert_eq!(result.len(), 3); - assert_eq!(result[0].to_css_string(), "rgb(255, 0, 0)"); - assert_eq!(result[1].to_css_string(), "rgb(255, 255, 0)"); - assert_eq!(result[2].to_css_string(), "rgb(0, 0, 255)"); + assert_eq!(result[0], (255, 0, 0)); + assert_eq!(result[1], (255, 255, 0)); + assert_eq!(result[2], (0, 0, 255)); } #[test] @@ -587,30 +498,6 @@ fn serialize_bad_tokens() { assert!(parser.next().is_err()); } -#[test] -fn serialize_current_color() { - let c = Color::CurrentColor; - assert!(c.to_css_string() == "currentcolor"); -} - -#[test] -fn serialize_rgb_full_alpha() { - let c = Color::Rgba(RgbaLegacy::new(255, 230, 204, 1.0)); - assert_eq!(c.to_css_string(), "rgb(255, 230, 204)"); -} - -#[test] -fn serialize_rgba() { - let c = Color::Rgba(RgbaLegacy::new(26, 51, 77, 0.125)); - assert_eq!(c.to_css_string(), "rgba(26, 51, 77, 0.125)"); -} - -#[test] -fn serialize_rgba_two_digit_float_if_roundtrips() { - let c = Color::Rgba(RgbaLegacy::from_floats(0., 0., 0., 0.5)); - assert_eq!(c.to_css_string(), "rgba(0, 0, 0, 0.5)"); -} - #[test] fn line_numbers() { let mut input = ParserInput::new(concat!( @@ -895,26 +782,6 @@ where } } -impl ToJson for Color { - fn to_json(&self) -> Value { - match *self { - Color::CurrentColor => "currentcolor".to_json(), - Color::Rgba(ref rgba) => { - json!([rgba.red, rgba.green, rgba.blue, rgba.alpha]) - } - Color::Hsl(ref c) => json!([c.hue, c.saturation, c.lightness, c.alpha]), - Color::Hwb(ref c) => json!([c.hue, c.whiteness, c.blackness, c.alpha]), - Color::Lab(ref c) => json!([c.lightness, c.a, c.b, c.alpha]), - Color::Lch(ref c) => json!([c.lightness, c.chroma, c.hue, c.alpha]), - Color::Oklab(ref c) => json!([c.lightness, c.a, c.b, c.alpha]), - Color::Oklch(ref c) => json!([c.lightness, c.chroma, c.hue, c.alpha]), - Color::ColorFunction(ref c) => { - json!([c.color_space.as_str(), c.c1, c.c2, c.c3, c.alpha]) - } - } - } -} - impl<'a> ToJson for CowRcStr<'a> { fn to_json(&self) -> Value { let s: &str = &*self; @@ -925,6 +792,7 @@ impl<'a> ToJson for CowRcStr<'a> { #[bench] #[cfg(feature = "bench")] fn delimiter_from_byte(b: &mut Bencher) { + use crate::Delimiters; b.iter(|| { for _ in 0..1000 { for i in 0..256 { @@ -1492,146 +1360,3 @@ fn servo_define_css_keyword_enum() { assert_eq!(UserZoom::from_ident("fixed"), Ok(UserZoom::Fixed)); } - -#[test] -fn generic_parser() { - #[derive(Debug, PartialEq)] - enum OutputType { - CurrentColor, - Rgba(u8, u8, u8, f32), - Hsl(Option, Option, Option, Option), - Hwb(Option, Option, Option, Option), - Lab(Option, Option, Option, Option), - Lch(Option, Option, Option, Option), - Oklab(Option, Option, Option, Option), - Oklch(Option, Option, Option, Option), - ColorFunction( - PredefinedColorSpace, - Option, - Option, - Option, - Option, - ), - } - - impl FromParsedColor for OutputType { - fn from_current_color() -> Self { - OutputType::CurrentColor - } - - fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self { - OutputType::Rgba(red, green, blue, alpha) - } - - fn from_hsl( - hue: Option, - saturation: Option, - lightness: Option, - alpha: Option, - ) -> Self { - OutputType::Hsl(hue, saturation, lightness, alpha) - } - - fn from_hwb( - hue: Option, - blackness: Option, - whiteness: Option, - alpha: Option, - ) -> Self { - OutputType::Hwb(hue, blackness, whiteness, alpha) - } - - fn from_lab( - lightness: Option, - a: Option, - b: Option, - alpha: Option, - ) -> Self { - OutputType::Lab(lightness, a, b, alpha) - } - - fn from_lch( - lightness: Option, - chroma: Option, - hue: Option, - alpha: Option, - ) -> Self { - OutputType::Lch(lightness, chroma, hue, alpha) - } - - fn from_oklab( - lightness: Option, - a: Option, - b: Option, - alpha: Option, - ) -> Self { - OutputType::Oklab(lightness, a, b, alpha) - } - - fn from_oklch( - lightness: Option, - chroma: Option, - hue: Option, - alpha: Option, - ) -> Self { - OutputType::Oklch(lightness, chroma, hue, alpha) - } - - fn from_color_function( - color_space: PredefinedColorSpace, - c1: Option, - c2: Option, - c3: Option, - alpha: Option, - ) -> Self { - OutputType::ColorFunction(color_space, c1, c2, c3, alpha) - } - } - - struct TestColorParser; - impl<'i> ColorParser<'i> for TestColorParser { - type Output = OutputType; - type Error = (); - } - - #[rustfmt::skip] - const TESTS: &[(&str, OutputType)] = &[ - ("currentColor", OutputType::CurrentColor), - ("rgb(1, 2, 3)", OutputType::Rgba(1, 2, 3, 1.0)), - ("rgba(1, 2, 3, 0.4)", OutputType::Rgba(1, 2, 3, 0.4)), - ("rgb(none none none / none)", OutputType::Rgba(0, 0, 0, 0.0)), - ("rgb(1 none 3 / none)", OutputType::Rgba(1, 0, 3, 0.0)), - - ("hsla(45deg, 20%, 30%, 0.4)", OutputType::Hsl(Some(45.0), Some(0.2), Some(0.3), Some(0.4))), - ("hsl(45deg none none)", OutputType::Hsl(Some(45.0), None, None, Some(1.0))), - ("hsl(none 10% none / none)", OutputType::Hsl(None, Some(0.1), None, None)), - ("hsl(120 100.0% 50.0%)", OutputType::Hsl(Some(120.0), Some(1.0), Some(0.5), Some(1.0))), - - ("hwb(45deg 20% 30% / 0.4)", OutputType::Hwb(Some(45.0), Some(0.2), Some(0.3), Some(0.4))), - - ("lab(100 20 30 / 0.4)", OutputType::Lab(Some(100.0), Some(20.0), Some(30.0), Some(0.4))), - ("lch(100 20 30 / 0.4)", OutputType::Lch(Some(100.0), Some(20.0), Some(30.0), Some(0.4))), - - ("oklab(100 20 30 / 0.4)", OutputType::Oklab(Some(100.0), Some(20.0), Some(30.0), Some(0.4))), - ("oklch(100 20 30 / 0.4)", OutputType::Oklch(Some(100.0), Some(20.0), Some(30.0), Some(0.4))), - - ("color(srgb 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::Srgb, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), - ("color(srgb none none none)", OutputType::ColorFunction(PredefinedColorSpace::Srgb, None, None, None, Some(1.0))), - ("color(srgb none none none / none)", OutputType::ColorFunction(PredefinedColorSpace::Srgb, None, None, None, None)), - ("color(srgb-linear 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::SrgbLinear, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), - ("color(display-p3 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::DisplayP3, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), - ("color(a98-rgb 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::A98Rgb, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), - ("color(prophoto-rgb 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::ProphotoRgb, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), - ("color(rec2020 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::Rec2020, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), - ("color(xyz-d50 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::XyzD50, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), - ("color(xyz-d65 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::XyzD65, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), - ]; - - for (input, expected) in TESTS { - let mut input = ParserInput::new(*input); - let mut input = Parser::new(&mut input); - - let actual: OutputType = parse_color_with(&TestColorParser, &mut input).unwrap(); - assert_eq!(actual, *expected); - } -}