From 5a345d6c021b99099b8be2a95168749b39857821 Mon Sep 17 00:00:00 2001 From: Tiaan Louw Date: Tue, 31 Jan 2023 10:13:29 +0100 Subject: [PATCH] Refactor color parser to take a generic as output This refactor allows the color parser to construct new color values directly, in stead of constructing a predefined color value and then forcing lib users to convert that type into whichever format they want to use in the end. Most common use case is that the color included in the lib, cssparser::Color, is good for serialization, but not well suited for doing calculations on. --- src/color.rs | 661 +++++++++++++++++++++++++++++---------------------- src/lib.rs | 4 +- src/tests.rs | 121 ++++++++++ 3 files changed, 498 insertions(+), 288 deletions(-) diff --git a/src/color.rs b/src/color.rs index b8721bf4..183cb813 100644 --- a/src/color.rs +++ b/src/color.rs @@ -6,7 +6,7 @@ use std::f32::consts::PI; use std::fmt; use std::str::FromStr; -use super::{BasicParseError, ParseError, Parser, ToCss, Token}; +use super::{ParseError, Parser, ToCss, Token}; #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -561,7 +561,10 @@ impl AngleOrNumber { /// components, with the intention of implementing more complicated behavior. /// /// For example, this is used by Servo to support calc() in color. -pub trait ColorComponentParser<'i> { +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; @@ -624,8 +627,11 @@ pub trait ColorComponentParser<'i> { } } -struct DefaultComponentParser; -impl<'i> ColorComponentParser<'i> for DefaultComponentParser { +/// Default implementation of a [`ColorParser`] +pub struct DefaultColorParser; + +impl<'i> ColorParser<'i> for DefaultColorParser { + type Output = Color; type Error = (); } @@ -633,212 +639,280 @@ impl Color { /// Parse a value, per CSS Color Module Level 3. /// /// FIXME(#2) Deprecated CSS2 System Colors are not supported yet. - pub fn parse_with<'i, 't, ComponentParser>( - component_parser: &ComponentParser, - input: &mut Parser<'i, 't>, - ) -> Result> - where - ComponentParser: ColorComponentParser<'i>, - { - let location = input.current_source_location(); - let token = input.next()?; - match *token { - Token::Hash(ref value) | Token::IDHash(ref value) => { - RGBA::parse_hash(value.as_bytes()).map(|rgba| Color::Rgba(rgba)) - } - Token::Ident(ref value) => parse_color_keyword(&*value), - Token::Function(ref name) => { - let name = name.clone(); - return input.parse_nested_block(|arguments| { - parse_color(component_parser, &*name, arguments) - }); - } - _ => Err(()), - } - .map_err(|()| location.new_unexpected_token_error(token.clone())) + pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result> { + parse_color_with(&DefaultColorParser, input) } +} - /// Parse a value, per CSS Color Module Level 3. - pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result> { - let component_parser = DefaultComponentParser; - Self::parse_with(&component_parser, input).map_err(ParseError::basic) +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 the `lab` notation. + fn from_lab(lightness: f32, a: f32, b: f32, alpha: f32) -> Self; + + /// Construct a new color from the `lch` notation. + fn from_lch(lightness: f32, chroma: f32, hue: f32, alpha: f32) -> Self; + + /// Construct a new color from the `oklab` notation. + fn from_oklab(lightness: f32, a: f32, b: f32, alpha: f32) -> Self; + + /// Construct a new color from the `oklch` notation. + fn from_oklch(lightness: f32, chroma: f32, hue: f32, alpha: f32) -> Self; + + /// Construct a new color with a predefined color space. + fn from_color_function( + color_space: PredefinedColorSpace, + c1: f32, + c2: f32, + c3: f32, + alpha: f32, + ) -> Self; +} + +/// Parse a CSS color with the specified [`ColorComponentParser`] 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) => RGBA::parse_hash(value.as_bytes()) + .map(|rgba| P::Output::from_rgba(rgba.red, rgba.green, rgba.blue, rgba.alpha)), + 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())) } -/// 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 { +impl FromParsedColor for Color { #[inline] - pub(crate) const fn rgb(red: u8, green: u8, blue: u8) -> Color { - rgba(red, green, blue, OPAQUE) + fn from_current_color() -> Self { + Color::CurrentColor } #[inline] - pub(crate) const fn rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Color { + fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self { Color::Rgba(RGBA::new(red, green, blue, alpha)) } + #[inline] + fn from_lab(lightness: f32, a: f32, b: f32, alpha: f32) -> Self { + Color::Lab(Lab::new(lightness, a, b, alpha)) + } + + #[inline] + fn from_lch(lightness: f32, chroma: f32, hue: f32, alpha: f32) -> Self { + Color::Lch(Lch::new(lightness, chroma, hue, alpha)) + } + + #[inline] + fn from_oklab(lightness: f32, a: f32, b: f32, alpha: f32) -> Self { + Color::Oklab(Oklab::new(lightness, a, b, alpha)) + } + + #[inline] + fn from_oklch(lightness: f32, chroma: f32, hue: f32, alpha: f32) -> Self { + Color::Oklch(Oklch::new(lightness, chroma, hue, alpha)) + } + + #[inline] + fn from_color_function( + color_space: PredefinedColorSpace, + c1: f32, + c2: f32, + c3: f32, + alpha: f32, + ) -> Self { + Color::ColorFunction(ColorFunction::new(color_space, c1, c2, c3, alpha)) + } +} + +/// 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, +{ ascii_case_insensitive_phf_map! { - keyword -> Color = { - "black" => rgb(0, 0, 0), - "silver" => rgb(192, 192, 192), - "gray" => rgb(128, 128, 128), - "white" => rgb(255, 255, 255), - "maroon" => rgb(128, 0, 0), - "red" => rgb(255, 0, 0), - "purple" => rgb(128, 0, 128), - "fuchsia" => rgb(255, 0, 255), - "green" => rgb(0, 128, 0), - "lime" => rgb(0, 255, 0), - "olive" => rgb(128, 128, 0), - "yellow" => rgb(255, 255, 0), - "navy" => rgb(0, 0, 128), - "blue" => rgb(0, 0, 255), - "teal" => rgb(0, 128, 128), - "aqua" => rgb(0, 255, 255), - - "aliceblue" => rgb(240, 248, 255), - "antiquewhite" => rgb(250, 235, 215), - "aquamarine" => rgb(127, 255, 212), - "azure" => rgb(240, 255, 255), - "beige" => rgb(245, 245, 220), - "bisque" => rgb(255, 228, 196), - "blanchedalmond" => rgb(255, 235, 205), - "blueviolet" => rgb(138, 43, 226), - "brown" => rgb(165, 42, 42), - "burlywood" => rgb(222, 184, 135), - "cadetblue" => rgb(95, 158, 160), - "chartreuse" => rgb(127, 255, 0), - "chocolate" => rgb(210, 105, 30), - "coral" => rgb(255, 127, 80), - "cornflowerblue" => rgb(100, 149, 237), - "cornsilk" => rgb(255, 248, 220), - "crimson" => rgb(220, 20, 60), - "cyan" => rgb(0, 255, 255), - "darkblue" => rgb(0, 0, 139), - "darkcyan" => rgb(0, 139, 139), - "darkgoldenrod" => rgb(184, 134, 11), - "darkgray" => rgb(169, 169, 169), - "darkgreen" => rgb(0, 100, 0), - "darkgrey" => rgb(169, 169, 169), - "darkkhaki" => rgb(189, 183, 107), - "darkmagenta" => rgb(139, 0, 139), - "darkolivegreen" => rgb(85, 107, 47), - "darkorange" => rgb(255, 140, 0), - "darkorchid" => rgb(153, 50, 204), - "darkred" => rgb(139, 0, 0), - "darksalmon" => rgb(233, 150, 122), - "darkseagreen" => rgb(143, 188, 143), - "darkslateblue" => rgb(72, 61, 139), - "darkslategray" => rgb(47, 79, 79), - "darkslategrey" => rgb(47, 79, 79), - "darkturquoise" => rgb(0, 206, 209), - "darkviolet" => rgb(148, 0, 211), - "deeppink" => rgb(255, 20, 147), - "deepskyblue" => rgb(0, 191, 255), - "dimgray" => rgb(105, 105, 105), - "dimgrey" => rgb(105, 105, 105), - "dodgerblue" => rgb(30, 144, 255), - "firebrick" => rgb(178, 34, 34), - "floralwhite" => rgb(255, 250, 240), - "forestgreen" => rgb(34, 139, 34), - "gainsboro" => rgb(220, 220, 220), - "ghostwhite" => rgb(248, 248, 255), - "gold" => rgb(255, 215, 0), - "goldenrod" => rgb(218, 165, 32), - "greenyellow" => rgb(173, 255, 47), - "grey" => rgb(128, 128, 128), - "honeydew" => rgb(240, 255, 240), - "hotpink" => rgb(255, 105, 180), - "indianred" => rgb(205, 92, 92), - "indigo" => rgb(75, 0, 130), - "ivory" => rgb(255, 255, 240), - "khaki" => rgb(240, 230, 140), - "lavender" => rgb(230, 230, 250), - "lavenderblush" => rgb(255, 240, 245), - "lawngreen" => rgb(124, 252, 0), - "lemonchiffon" => rgb(255, 250, 205), - "lightblue" => rgb(173, 216, 230), - "lightcoral" => rgb(240, 128, 128), - "lightcyan" => rgb(224, 255, 255), - "lightgoldenrodyellow" => rgb(250, 250, 210), - "lightgray" => rgb(211, 211, 211), - "lightgreen" => rgb(144, 238, 144), - "lightgrey" => rgb(211, 211, 211), - "lightpink" => rgb(255, 182, 193), - "lightsalmon" => rgb(255, 160, 122), - "lightseagreen" => rgb(32, 178, 170), - "lightskyblue" => rgb(135, 206, 250), - "lightslategray" => rgb(119, 136, 153), - "lightslategrey" => rgb(119, 136, 153), - "lightsteelblue" => rgb(176, 196, 222), - "lightyellow" => rgb(255, 255, 224), - "limegreen" => rgb(50, 205, 50), - "linen" => rgb(250, 240, 230), - "magenta" => rgb(255, 0, 255), - "mediumaquamarine" => rgb(102, 205, 170), - "mediumblue" => rgb(0, 0, 205), - "mediumorchid" => rgb(186, 85, 211), - "mediumpurple" => rgb(147, 112, 219), - "mediumseagreen" => rgb(60, 179, 113), - "mediumslateblue" => rgb(123, 104, 238), - "mediumspringgreen" => rgb(0, 250, 154), - "mediumturquoise" => rgb(72, 209, 204), - "mediumvioletred" => rgb(199, 21, 133), - "midnightblue" => rgb(25, 25, 112), - "mintcream" => rgb(245, 255, 250), - "mistyrose" => rgb(255, 228, 225), - "moccasin" => rgb(255, 228, 181), - "navajowhite" => rgb(255, 222, 173), - "oldlace" => rgb(253, 245, 230), - "olivedrab" => rgb(107, 142, 35), - "orange" => rgb(255, 165, 0), - "orangered" => rgb(255, 69, 0), - "orchid" => rgb(218, 112, 214), - "palegoldenrod" => rgb(238, 232, 170), - "palegreen" => rgb(152, 251, 152), - "paleturquoise" => rgb(175, 238, 238), - "palevioletred" => rgb(219, 112, 147), - "papayawhip" => rgb(255, 239, 213), - "peachpuff" => rgb(255, 218, 185), - "peru" => rgb(205, 133, 63), - "pink" => rgb(255, 192, 203), - "plum" => rgb(221, 160, 221), - "powderblue" => rgb(176, 224, 230), - "rebeccapurple" => rgb(102, 51, 153), - "rosybrown" => rgb(188, 143, 143), - "royalblue" => rgb(65, 105, 225), - "saddlebrown" => rgb(139, 69, 19), - "salmon" => rgb(250, 128, 114), - "sandybrown" => rgb(244, 164, 96), - "seagreen" => rgb(46, 139, 87), - "seashell" => rgb(255, 245, 238), - "sienna" => rgb(160, 82, 45), - "skyblue" => rgb(135, 206, 235), - "slateblue" => rgb(106, 90, 205), - "slategray" => rgb(112, 128, 144), - "slategrey" => rgb(112, 128, 144), - "snow" => rgb(255, 250, 250), - "springgreen" => rgb(0, 255, 127), - "steelblue" => rgb(70, 130, 180), - "tan" => rgb(210, 180, 140), - "thistle" => rgb(216, 191, 216), - "tomato" => rgb(255, 99, 71), - "turquoise" => rgb(64, 224, 208), - "violet" => rgb(238, 130, 238), - "wheat" => rgb(245, 222, 179), - "whitesmoke" => rgb(245, 245, 245), - "yellowgreen" => rgb(154, 205, 50), - - "transparent" => rgba(0, 0, 0, 0.0), - "currentcolor" => Color::CurrentColor, + keyword -> (u8, u8, u8) = { + "black" => (0, 0, 0), + "silver" => (192, 192, 192), + "gray" => (128, 128, 128), + "white" => (255, 255, 255), + "maroon" => (128, 0, 0), + "red" => (255, 0, 0), + "purple" => (128, 0, 128), + "fuchsia" => (255, 0, 255), + "green" => (0, 128, 0), + "lime" => (0, 255, 0), + "olive" => (128, 128, 0), + "yellow" => (255, 255, 0), + "navy" => (0, 0, 128), + "blue" => (0, 0, 255), + "teal" => (0, 128, 128), + "aqua" => (0, 255, 255), + + "aliceblue" => (240, 248, 255), + "antiquewhite" => (250, 235, 215), + "aquamarine" => (127, 255, 212), + "azure" => (240, 255, 255), + "beige" => (245, 245, 220), + "bisque" => (255, 228, 196), + "blanchedalmond" => (255, 235, 205), + "blueviolet" => (138, 43, 226), + "brown" => (165, 42, 42), + "burlywood" => (222, 184, 135), + "cadetblue" => (95, 158, 160), + "chartreuse" => (127, 255, 0), + "chocolate" => (210, 105, 30), + "coral" => (255, 127, 80), + "cornflowerblue" => (100, 149, 237), + "cornsilk" => (255, 248, 220), + "crimson" => (220, 20, 60), + "cyan" => (0, 255, 255), + "darkblue" => (0, 0, 139), + "darkcyan" => (0, 139, 139), + "darkgoldenrod" => (184, 134, 11), + "darkgray" => (169, 169, 169), + "darkgreen" => (0, 100, 0), + "darkgrey" => (169, 169, 169), + "darkkhaki" => (189, 183, 107), + "darkmagenta" => (139, 0, 139), + "darkolivegreen" => (85, 107, 47), + "darkorange" => (255, 140, 0), + "darkorchid" => (153, 50, 204), + "darkred" => (139, 0, 0), + "darksalmon" => (233, 150, 122), + "darkseagreen" => (143, 188, 143), + "darkslateblue" => (72, 61, 139), + "darkslategray" => (47, 79, 79), + "darkslategrey" => (47, 79, 79), + "darkturquoise" => (0, 206, 209), + "darkviolet" => (148, 0, 211), + "deeppink" => (255, 20, 147), + "deepskyblue" => (0, 191, 255), + "dimgray" => (105, 105, 105), + "dimgrey" => (105, 105, 105), + "dodgerblue" => (30, 144, 255), + "firebrick" => (178, 34, 34), + "floralwhite" => (255, 250, 240), + "forestgreen" => (34, 139, 34), + "gainsboro" => (220, 220, 220), + "ghostwhite" => (248, 248, 255), + "gold" => (255, 215, 0), + "goldenrod" => (218, 165, 32), + "greenyellow" => (173, 255, 47), + "grey" => (128, 128, 128), + "honeydew" => (240, 255, 240), + "hotpink" => (255, 105, 180), + "indianred" => (205, 92, 92), + "indigo" => (75, 0, 130), + "ivory" => (255, 255, 240), + "khaki" => (240, 230, 140), + "lavender" => (230, 230, 250), + "lavenderblush" => (255, 240, 245), + "lawngreen" => (124, 252, 0), + "lemonchiffon" => (255, 250, 205), + "lightblue" => (173, 216, 230), + "lightcoral" => (240, 128, 128), + "lightcyan" => (224, 255, 255), + "lightgoldenrodyellow" => (250, 250, 210), + "lightgray" => (211, 211, 211), + "lightgreen" => (144, 238, 144), + "lightgrey" => (211, 211, 211), + "lightpink" => (255, 182, 193), + "lightsalmon" => (255, 160, 122), + "lightseagreen" => (32, 178, 170), + "lightskyblue" => (135, 206, 250), + "lightslategray" => (119, 136, 153), + "lightslategrey" => (119, 136, 153), + "lightsteelblue" => (176, 196, 222), + "lightyellow" => (255, 255, 224), + "limegreen" => (50, 205, 50), + "linen" => (250, 240, 230), + "magenta" => (255, 0, 255), + "mediumaquamarine" => (102, 205, 170), + "mediumblue" => (0, 0, 205), + "mediumorchid" => (186, 85, 211), + "mediumpurple" => (147, 112, 219), + "mediumseagreen" => (60, 179, 113), + "mediumslateblue" => (123, 104, 238), + "mediumspringgreen" => (0, 250, 154), + "mediumturquoise" => (72, 209, 204), + "mediumvioletred" => (199, 21, 133), + "midnightblue" => (25, 25, 112), + "mintcream" => (245, 255, 250), + "mistyrose" => (255, 228, 225), + "moccasin" => (255, 228, 181), + "navajowhite" => (255, 222, 173), + "oldlace" => (253, 245, 230), + "olivedrab" => (107, 142, 35), + "orange" => (255, 165, 0), + "orangered" => (255, 69, 0), + "orchid" => (218, 112, 214), + "palegoldenrod" => (238, 232, 170), + "palegreen" => (152, 251, 152), + "paleturquoise" => (175, 238, 238), + "palevioletred" => (219, 112, 147), + "papayawhip" => (255, 239, 213), + "peachpuff" => (255, 218, 185), + "peru" => (205, 133, 63), + "pink" => (255, 192, 203), + "plum" => (221, 160, 221), + "powderblue" => (176, 224, 230), + "rebeccapurple" => (102, 51, 153), + "rosybrown" => (188, 143, 143), + "royalblue" => (65, 105, 225), + "saddlebrown" => (139, 69, 19), + "salmon" => (250, 128, 114), + "sandybrown" => (244, 164, 96), + "seagreen" => (46, 139, 87), + "seashell" => (255, 245, 238), + "sienna" => (160, 82, 45), + "skyblue" => (135, 206, 235), + "slateblue" => (106, 90, 205), + "slategray" => (112, 128, 144), + "slategrey" => (112, 128, 144), + "snow" => (255, 250, 250), + "springgreen" => (0, 255, 127), + "steelblue" => (70, 130, 180), + "tan" => (210, 180, 140), + "thistle" => (216, 191, 216), + "tomato" => (255, 99, 71), + "turquoise" => (64, 224, 208), + "violet" => (238, 130, 238), + "wheat" => (245, 222, 179), + "whitesmoke" => (245, 245, 245), + "yellowgreen" => (154, 205, 50), } } - keyword(ident).cloned().ok_or(()) + + match_ignore_ascii_case! { ident , + "transparent" => Ok(Output::from_rgba(0, 0, 0, 0.0)), + "currentcolor" => Ok(Output::from_current_color()), + _ => keyword(ident) + .map(|(r, g, b)| Output::from_rgba(*r, *g, *b, 1.0)) + .ok_or(()), + } } #[inline] @@ -873,50 +947,77 @@ fn clamp_floor_256_f32(val: f32) -> u8 { val.round().max(0.).min(255.) as u8 } +/// Parse one of the color functions: rgba(), lab(), color(), etc. #[inline] -fn parse_color<'i, 't, ComponentParser>( - component_parser: &ComponentParser, +fn parse_color_function<'i, 't, P>( + color_parser: &P, name: &str, arguments: &mut Parser<'i, 't>, -) -> Result> +) -> Result> where - ComponentParser: ColorComponentParser<'i>, + P: ColorParser<'i>, { // FIXME: Should the parser clamp values? or should specified/computed // value processing handle clamping? let color = match_ignore_ascii_case! { name, - "rgb" | "rgba" => parse_rgb(component_parser, arguments), - - "hsl" | "hsla" => parse_hsl_hwb(component_parser, arguments, hsl_to_rgb, /* allow_comma = */ true), - - "hwb" => parse_hsl_hwb(component_parser, arguments, hwb_to_rgb, /* allow_comma = */ false), + "rgb" | "rgba" => parse_rgb(color_parser, arguments), + + "hsl" | "hsla" => parse_hsl_hwb( + color_parser, + arguments, + hsl_to_rgb, + /* allow_comma = */ true, + ), + + "hwb" => parse_hsl_hwb( + color_parser, + arguments, + hwb_to_rgb, + /* allow_comma = */ false, + ), // for L: 0% = 0.0, 100% = 100.0 // for a and b: -100% = -125, 100% = 125 - "lab" => parse_lab_like(component_parser, arguments, 100.0, 125.0, |l, a, b, alpha| { - Color::Lab(Lab::new(l.max(0.), a , b , alpha)) - }), + "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(component_parser, arguments, 100.0, 150.0, |l, c, h, alpha| { - Color::Lch(Lch::new(l.max(0.), c.max(0.), h, alpha)) - }), + "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(component_parser, arguments, 1.0, 0.4, |l, a, b, alpha| { - Color::Oklab(Oklab::new(l.max(0.), a, b, alpha)) - }), + "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(component_parser, arguments, 1.0, 0.4, |l, c, h, alpha| { - Color::Oklch(Oklch::new(l.max(0.), c.max(0.), h, alpha)) - }), + "oklch" => parse_lch_like( + color_parser, + arguments, + 1.0, + 0.4, + P::Output::from_oklch, + ), - "color" => parse_color_function(component_parser, arguments), + "color" => parse_color_color_function(color_parser, arguments), _ => return Err(arguments.new_unexpected_token_error(Token::Ident(name.to_owned().into()))), }?; @@ -927,13 +1028,13 @@ where } #[inline] -fn parse_alpha<'i, 't, ComponentParser>( - component_parser: &ComponentParser, +fn parse_alpha<'i, 't, P>( + color_parser: &P, arguments: &mut Parser<'i, 't>, uses_commas: bool, -) -> Result> +) -> Result> where - ComponentParser: ColorComponentParser<'i>, + P: ColorParser<'i>, { Ok(if !arguments.is_exhausted() { if uses_commas { @@ -941,7 +1042,7 @@ where } else { arguments.expect_delim('/')?; }; - component_parser + color_parser .parse_number_or_percentage(arguments)? .unit_value() .clamp(0.0, OPAQUE) @@ -951,16 +1052,16 @@ where } #[inline] -fn parse_rgb_components_rgb<'i, 't, ComponentParser>( - component_parser: &ComponentParser, +fn parse_rgb<'i, 't, P>( + color_parser: &P, arguments: &mut Parser<'i, 't>, -) -> Result<(u8, u8, u8, f32), ParseError<'i, ComponentParser::Error>> +) -> Result> where - ComponentParser: ColorComponentParser<'i>, + P: ColorParser<'i>, { // Either integers or percentages, but all the same type. // https://drafts.csswg.org/css-color/#rgb-functions - let (red, is_number) = match component_parser.parse_number_or_percentage(arguments)? { + let (red, is_number) = match color_parser.parse_number_or_percentage(arguments)? { NumberOrPercentage::Number { value } => (clamp_floor_256_f32(value), true), NumberOrPercentage::Percentage { unit_value } => (clamp_unit_f32(unit_value), false), }; @@ -970,34 +1071,22 @@ where let green; let blue; if is_number { - green = clamp_floor_256_f32(component_parser.parse_number(arguments)?); + green = clamp_floor_256_f32(color_parser.parse_number(arguments)?); if uses_commas { arguments.expect_comma()?; } - blue = clamp_floor_256_f32(component_parser.parse_number(arguments)?); + blue = clamp_floor_256_f32(color_parser.parse_number(arguments)?); } else { - green = clamp_unit_f32(component_parser.parse_percentage(arguments)?); + green = clamp_unit_f32(color_parser.parse_percentage(arguments)?); if uses_commas { arguments.expect_comma()?; } - blue = clamp_unit_f32(component_parser.parse_percentage(arguments)?); + blue = clamp_unit_f32(color_parser.parse_percentage(arguments)?); } - let alpha = parse_alpha(component_parser, arguments, uses_commas)?; + let alpha = parse_alpha(color_parser, arguments, uses_commas)?; - Ok((red, green, blue, alpha)) -} - -#[inline] -fn parse_rgb<'i, 't, ComponentParser>( - component_parser: &ComponentParser, - arguments: &mut Parser<'i, 't>, -) -> Result> -where - ComponentParser: ColorComponentParser<'i>, -{ - let (red, green, blue, alpha) = parse_rgb_components_rgb(component_parser, arguments)?; - Ok(Color::Rgba(RGBA::new(red, green, blue, alpha))) + Ok(P::Output::from_rgba(red, green, blue, alpha)) } /// Parses hsl and hbw syntax, which happens to be identical. @@ -1005,18 +1094,18 @@ where /// https://drafts.csswg.org/css-color/#the-hsl-notation /// https://drafts.csswg.org/css-color/#the-hbw-notation #[inline] -fn parse_hsl_hwb<'i, 't, ComponentParser>( - component_parser: &ComponentParser, +fn parse_hsl_hwb<'i, 't, P>( + color_parser: &P, arguments: &mut Parser<'i, 't>, to_rgb: impl FnOnce(f32, f32, f32) -> (f32, f32, f32), allow_comma: bool, -) -> Result> +) -> Result> where - ComponentParser: ColorComponentParser<'i>, + P: ColorParser<'i>, { // Hue given as an angle // https://drafts.csswg.org/css-values/#angles - let hue_degrees = component_parser.parse_angle_or_number(arguments)?.degrees(); + let hue_degrees = color_parser.parse_angle_or_number(arguments)?.degrees(); // Subtract an integer before rounding, to avoid some rounding errors: let hue_normalized_degrees = hue_degrees - 360. * (hue_degrees / 360.).floor(); @@ -1025,26 +1114,22 @@ where // Saturation and lightness are clamped to 0% ... 100% let uses_commas = allow_comma && arguments.try_parse(|i| i.expect_comma()).is_ok(); - let first_percentage = component_parser - .parse_percentage(arguments)? - .clamp(0.0, 1.0); + let first_percentage = color_parser.parse_percentage(arguments)?.clamp(0.0, 1.0); if uses_commas { arguments.expect_comma()?; } - let second_percentage = component_parser - .parse_percentage(arguments)? - .clamp(0.0, 1.0); + let second_percentage = color_parser.parse_percentage(arguments)?.clamp(0.0, 1.0); let (red, green, blue) = to_rgb(hue, first_percentage, second_percentage); let red = clamp_unit_f32(red); let green = clamp_unit_f32(green); let blue = clamp_unit_f32(blue); - let alpha = parse_alpha(component_parser, arguments, uses_commas)?; + let alpha = parse_alpha(color_parser, arguments, uses_commas)?; - Ok(Color::Rgba(RGBA::new(red, green, blue, alpha))) + Ok(P::Output::from_rgba(red, green, blue, alpha)) } /// https://drafts.csswg.org/css-color-4/#hwb-to-rgb @@ -1098,24 +1183,25 @@ pub fn hsl_to_rgb(hue: f32, saturation: f32, lightness: f32) -> (f32, f32, f32) } #[inline] -fn parse_lab_like<'i, 't, ComponentParser>( - component_parser: &ComponentParser, +fn parse_lab_like<'i, 't, P>( + color_parser: &P, arguments: &mut Parser<'i, 't>, lightness_range: f32, a_b_range: f32, - into_color: fn(l: f32, a: f32, b: f32, alpha: f32) -> Color, -) -> Result> + into_color: fn(l: f32, a: f32, b: f32, alpha: f32) -> P::Output, +) -> Result> where - ComponentParser: ColorComponentParser<'i>, + P: ColorParser<'i>, { - let lightness = match component_parser.parse_number_or_percentage(arguments)? { + let lightness = match color_parser.parse_number_or_percentage(arguments)? { NumberOrPercentage::Number { value } => value, NumberOrPercentage::Percentage { unit_value } => unit_value * lightness_range, - }; + } + .max(0.); macro_rules! parse_a_b { () => {{ - match component_parser.parse_number_or_percentage(arguments)? { + match color_parser.parse_number_or_percentage(arguments)? { NumberOrPercentage::Number { value } => value, NumberOrPercentage::Percentage { unit_value } => unit_value * a_b_range, } @@ -1125,49 +1211,52 @@ where let a = parse_a_b!(); let b = parse_a_b!(); - let alpha = parse_alpha(component_parser, arguments, false)?; + let alpha = parse_alpha(color_parser, arguments, false)?; Ok(into_color(lightness, a, b, alpha)) } #[inline] -fn parse_lch_like<'i, 't, ComponentParser>( - component_parser: &ComponentParser, +fn parse_lch_like<'i, 't, P>( + color_parser: &P, arguments: &mut Parser<'i, 't>, lightness_range: f32, chroma_range: f32, - into_color: fn(l: f32, c: f32, h: f32, alpha: f32) -> Color, -) -> Result> + into_color: fn(l: f32, c: f32, h: f32, alpha: f32) -> P::Output, +) -> Result> where - ComponentParser: ColorComponentParser<'i>, + P: ColorParser<'i>, { // for L: 0% = 0.0, 100% = 100.0 - let lightness = match component_parser.parse_number_or_percentage(arguments)? { + let lightness = match color_parser.parse_number_or_percentage(arguments)? { NumberOrPercentage::Number { value } => value, NumberOrPercentage::Percentage { unit_value } => unit_value * lightness_range, - }; + } + .max(0.); // for C: 0% = 0, 100% = 150 - let chroma = match component_parser.parse_number_or_percentage(arguments)? { + let chroma = match color_parser.parse_number_or_percentage(arguments)? { NumberOrPercentage::Number { value } => value, NumberOrPercentage::Percentage { unit_value } => unit_value * chroma_range, - }; + } + .max(0.); - let hue_degrees = component_parser.parse_angle_or_number(arguments)?.degrees(); + let hue_degrees = color_parser.parse_angle_or_number(arguments)?.degrees(); let hue = hue_degrees - 360. * (hue_degrees / 360.).floor(); - let alpha = parse_alpha(component_parser, arguments, false)?; + let alpha = parse_alpha(color_parser, arguments, false)?; Ok(into_color(lightness, chroma, hue, alpha)) } +/// Parse the color() function. #[inline] -fn parse_color_function<'i, 't, ComponentParser>( - component_parser: &ComponentParser, +fn parse_color_color_function<'i, 't, P>( + color_parser: &P, arguments: &mut Parser<'i, 't>, -) -> Result> +) -> Result> where - ComponentParser: ColorComponentParser<'i>, + P: ColorParser<'i>, { let color_space = { let location = arguments.current_source_location(); @@ -1179,7 +1268,7 @@ where macro_rules! parse_component { () => {{ - component_parser + color_parser .parse_number_or_percentage(arguments)? .unit_value() }}; @@ -1189,13 +1278,13 @@ where let c2 = parse_component!(); let c3 = parse_component!(); - let alpha = parse_alpha(component_parser, arguments, false)?; + let alpha = parse_alpha(color_parser, arguments, false)?; - Ok(Color::ColorFunction(ColorFunction { + Ok(P::Output::from_color_function( color_space, c1, c2, c3, alpha, - })) + )) } diff --git a/src/lib.rs b/src/lib.rs index 9aa404de..2bfe92a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,8 +68,8 @@ fn parse_border_spacing(_context: &ParserContext, input: &mut Parser) #![recursion_limit = "200"] // For color::parse_color_keyword pub use crate::color::{ - hsl_to_rgb, hwb_to_rgb, parse_color_keyword, AngleOrNumber, Color, ColorComponentParser, - ColorFunction, Lab, Lch, NumberOrPercentage, Oklab, Oklch, PredefinedColorSpace, RGBA, + hsl_to_rgb, hwb_to_rgb, parse_color_keyword, AngleOrNumber, Color, ColorFunction, ColorParser, + Lab, Lch, NumberOrPercentage, Oklab, Oklch, PredefinedColorSpace, RGBA, }; pub use crate::cow_rc_str::CowRcStr; pub use crate::from_bytes::{stylesheet_encoding, EncodingSupport}; diff --git a/src/tests.rs b/src/tests.rs index 19e779ca..79b0cb23 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -8,6 +8,9 @@ extern crate test; use encoding_rs; use serde_json::{self, json, Map, Value}; +use crate::color::{parse_color_with, FromParsedColor}; +use crate::{ColorParser, PredefinedColorSpace}; + #[cfg(feature = "bench")] use self::test::Bencher; @@ -1507,3 +1510,121 @@ 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), + Lab(f32, f32, f32, f32), + Lch(f32, f32, f32, f32), + Oklab(f32, f32, f32, f32), + Oklch(f32, f32, f32, f32), + ColorFunction(PredefinedColorSpace, f32, f32, f32, f32), + } + + 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_lab(lightness: f32, a: f32, b: f32, alpha: f32) -> Self { + OutputType::Lab(lightness, a, b, alpha) + } + + fn from_lch(lightness: f32, chroma: f32, hue: f32, alpha: f32) -> Self { + OutputType::Lch(lightness, chroma, hue, alpha) + } + + fn from_oklab(lightness: f32, a: f32, b: f32, alpha: f32) -> Self { + OutputType::Oklab(lightness, a, b, alpha) + } + + fn from_oklch(lightness: f32, chroma: f32, hue: f32, alpha: f32) -> Self { + OutputType::Oklch(lightness, chroma, hue, alpha) + } + + fn from_color_function( + color_space: PredefinedColorSpace, + c1: f32, + c2: f32, + c3: f32, + alpha: f32, + ) -> Self { + OutputType::ColorFunction(color_space, c1, c2, c3, alpha) + } + } + + struct ComponentParser; + impl<'i> ColorParser<'i> for ComponentParser { + type Output = OutputType; + type Error = (); + } + + 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)), + ( + "lab(100 20 30 / 0.4)", + OutputType::Lab(100.0, 20.0, 30.0, 0.4), + ), + ( + "lch(100 20 30 / 0.4)", + OutputType::Lch(100.0, 20.0, 30.0, 0.4), + ), + ( + "oklab(100 20 30 / 0.4)", + OutputType::Oklab(100.0, 20.0, 30.0, 0.4), + ), + ( + "oklch(100 20 30 / 0.4)", + OutputType::Oklch(100.0, 20.0, 30.0, 0.4), + ), + ( + "color(srgb 0.1 0.2 0.3 / 0.4)", + OutputType::ColorFunction(PredefinedColorSpace::Srgb, 0.1, 0.2, 0.3, 0.4), + ), + ( + "color(srgb-linear 0.1 0.2 0.3 / 0.4)", + OutputType::ColorFunction(PredefinedColorSpace::SrgbLinear, 0.1, 0.2, 0.3, 0.4), + ), + ( + "color(display-p3 0.1 0.2 0.3 / 0.4)", + OutputType::ColorFunction(PredefinedColorSpace::DisplayP3, 0.1, 0.2, 0.3, 0.4), + ), + ( + "color(a98-rgb 0.1 0.2 0.3 / 0.4)", + OutputType::ColorFunction(PredefinedColorSpace::A98Rgb, 0.1, 0.2, 0.3, 0.4), + ), + ( + "color(prophoto-rgb 0.1 0.2 0.3 / 0.4)", + OutputType::ColorFunction(PredefinedColorSpace::ProphotoRgb, 0.1, 0.2, 0.3, 0.4), + ), + ( + "color(rec2020 0.1 0.2 0.3 / 0.4)", + OutputType::ColorFunction(PredefinedColorSpace::Rec2020, 0.1, 0.2, 0.3, 0.4), + ), + ( + "color(xyz-d50 0.1 0.2 0.3 / 0.4)", + OutputType::ColorFunction(PredefinedColorSpace::XyzD50, 0.1, 0.2, 0.3, 0.4), + ), + ( + "color(xyz-d65 0.1 0.2 0.3 / 0.4)", + OutputType::ColorFunction(PredefinedColorSpace::XyzD65, 0.1, 0.2, 0.3, 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(&ComponentParser, &mut input).unwrap(); + assert_eq!(actual, *expected); + } +}