diff --git a/crates/anstyle-git/src/lib.rs b/crates/anstyle-git/src/lib.rs index a8aacae7..1d696155 100644 --- a/crates/anstyle-git/src/lib.rs +++ b/crates/anstyle-git/src/lib.rs @@ -111,19 +111,9 @@ fn parse_color(word: &str) -> Result, ()> { "white" => Some(anstyle::AnsiColor::White.into()), _ => { if let Some(hex) = word.strip_prefix('#') { - let l = hex.len(); - if l != 3 && l != 6 { - return Err(()); - } - let l = l / 3; - if let (Ok(r), Ok(g), Ok(b)) = ( - u8::from_str_radix(&hex[0..l], 16), - u8::from_str_radix(&hex[l..(2 * l)], 16), - u8::from_str_radix(&hex[(2 * l)..(3 * l)], 16), - ) { - Some(anstyle::Color::from((r, g, b))) - } else { - return Err(()); + match anstyle::Color::from_hex_str(hex) { + None => return Err(()), + c => c, } } else if let Ok(n) = word.parse::() { Some(anstyle::Color::from(n)) @@ -217,12 +207,12 @@ mod tests { test!("#204060" => RgbColor(0x20,0x40,0x60).on_default()); test!("#1a2b3c" => RgbColor(0x1a,0x2b,0x3c).on_default()); test!("#000" => RgbColor(0,0,0).on_default()); - test!("#cba" => RgbColor(0xc,0xb,0xa).on_default()); - test!("#cba " => RgbColor(0xc,0xb,0xa).on_default()); - test!("#987 #135" => RgbColor(9,8,7).on(RgbColor(1, 3, 5))); - test!("#987 #135 " => RgbColor(9,8,7).on(RgbColor(1, 3, 5))); - test!("#123 #abcdef" => RgbColor(1,2,3).on(RgbColor(0xab, 0xcd, 0xef))); - test!("#654321 #a9b" => RgbColor(0x65,0x43,0x21).on(RgbColor(0xa, 0x9, 0xb))); + test!("#cba" => RgbColor(0xcc, 0xbb, 0xaa).on_default()); + test!("#cba " => RgbColor(0xcc, 0xbb, 0xaa).on_default()); + test!("#987 #135" => RgbColor(0x99, 0x88, 0x77).on(RgbColor(0x11, 0x33, 0x55))); + test!("#987 #135 " => RgbColor(0x99, 0x88, 0x77).on(RgbColor(0x11, 0x33, 0x55))); + test!("#123 #abcdef" => RgbColor(0x11, 0x22, 0x33).on(RgbColor(0xab, 0xcd, 0xef))); + test!("#654321 #a9b" => RgbColor(0x65, 0x43, 0x21).on(RgbColor(0xaa, 0x99, 0xbb))); test!("bold cyan white" => Cyan.on(White).bold()); test!("bold cyan nobold white" => Cyan.on(White)); @@ -232,8 +222,8 @@ mod tests { test!("italic cyan white" => Cyan.on(White).italic()); test!("strike cyan white" => Cyan.on(White).strikethrough()); test!("blink #050505 white" => RgbColor(5,5,5).on(White).blink()); - test!("bold #987 green" => RgbColor(9,8,7).on(Green).bold()); - test!("strike #147 #cba" => RgbColor(1,4,7).on(RgbColor(0xc, 0xb, 0xa)).strikethrough()); + test!("bold #987 green" => RgbColor(0x99, 0x88, 0x77).on(Green).bold()); + test!("strike #147 #cba" => RgbColor(0x11, 0x44, 0x77).on(RgbColor(0xcc, 0xbb, 0xaa)).strikethrough()); } #[test] diff --git a/crates/anstyle/src/color.rs b/crates/anstyle/src/color.rs index 5e3b46e8..e19bb3f7 100644 --- a/crates/anstyle/src/color.rs +++ b/crates/anstyle/src/color.rs @@ -17,6 +17,23 @@ pub enum Color { } impl Color { + /// Parse a `Color` from a hex string of length 3 or 6. + /// + /// Strings of length 3 (`abc`) will get interpreted as though each character were doubled + /// (`aabbcc`), as in HTML or Git. + /// + /// This does not handle an initial `#`; strip it before calling this function. This allows the + /// caller to distinguish hex colors from other possible inputs, such as named or numbered + /// colors. + /// + /// The result will always be a 24-bit `RgbColor`. + /// + /// Returns `None` on parse error. + #[inline] + pub fn from_hex_str(hex: &str) -> Option { + Some(RgbColor::from_hex_str(hex)?.into()) + } + /// Create a [`Style`][crate::Style] with this as the foreground #[inline] pub fn on(self, background: impl Into) -> crate::Style { @@ -476,6 +493,33 @@ impl From for Ansi256Color { pub struct RgbColor(pub u8, pub u8, pub u8); impl RgbColor { + /// Parse an `RgbColor` from a hex string of length 3 or 6. + /// + /// Strings of length 3 (`abc`) will get interpreted as though each character were doubled + /// (`aabbcc`), as in HTML or Git. + /// + /// This does not handle an initial `#`; strip it before calling this function. This allows the + /// caller to distinguish hex colors from other possible inputs, such as named or numbered + /// colors. + /// + /// Returns `None` on parse error. + pub fn from_hex_str(hex: &str) -> Option { + let l = hex.len(); + if l != 3 && l != 6 { + return None; + } + let l = l / 3; + let (Ok(r), Ok(g), Ok(b)) = ( + u8::from_str_radix(&hex[0..l], 16), + u8::from_str_radix(&hex[l..(2 * l)], 16), + u8::from_str_radix(&hex[(2 * l)..(3 * l)], 16), + ) else { + return None; + }; + let m = if l == 1 { 0x11 } else { 1 }; + Some(Self(r * m, g * m, b * m)) + } + /// Create a [`Style`][crate::Style] with this as the foreground #[inline] pub fn on(self, background: impl Into) -> crate::Style {