use crate::spaces::Rgb;
pub(crate) fn parse_hex(input: &str) -> Option<Rgb> {
let body = input.strip_prefix('#').unwrap_or(input);
if !body.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
let value = u64::from_str_radix(body, 16).ok()?;
match body.len() {
3 => {
let r = (((value >> 8) & 0xf) | ((value >> 4) & 0xf0)) as f64 / 255.0;
let g = (((value >> 4) & 0xf) | (value & 0xf0)) as f64 / 255.0;
let b = ((value & 0xf) | ((value << 4) & 0xf0)) as f64 / 255.0;
Some(Rgb {
r,
g,
b,
alpha: None,
})
}
4 => {
let r = (((value >> 12) & 0xf) | ((value >> 8) & 0xf0)) as f64 / 255.0;
let g = (((value >> 8) & 0xf) | ((value >> 4) & 0xf0)) as f64 / 255.0;
let b = (((value >> 4) & 0xf) | (value & 0xf0)) as f64 / 255.0;
let a = ((value & 0xf) | ((value << 4) & 0xf0)) as f64 / 255.0;
Some(Rgb {
r,
g,
b,
alpha: Some(a),
})
}
6 => Some(Rgb {
r: ((value >> 16) & 0xff) as f64 / 255.0,
g: ((value >> 8) & 0xff) as f64 / 255.0,
b: (value & 0xff) as f64 / 255.0,
alpha: None,
}),
8 => Some(Rgb {
r: ((value >> 24) & 0xff) as f64 / 255.0,
g: ((value >> 16) & 0xff) as f64 / 255.0,
b: ((value >> 8) & 0xff) as f64 / 255.0,
alpha: Some((value & 0xff) as f64 / 255.0),
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rgb(r: f64, g: f64, b: f64, alpha: Option<f64>) -> Rgb {
Rgb { r, g, b, alpha }
}
#[test]
fn three_digit() {
assert_eq!(parse_hex("#f00"), Some(rgb(1.0, 0.0, 0.0, None)));
let expected = rgb(170.0 / 255.0, 187.0 / 255.0, 204.0 / 255.0, None);
assert_eq!(parse_hex("#abc"), Some(expected));
}
#[test]
fn four_digit() {
assert_eq!(parse_hex("#0000"), Some(rgb(0.0, 0.0, 0.0, Some(0.0))));
assert_eq!(parse_hex("#f00f"), Some(rgb(1.0, 0.0, 0.0, Some(1.0))));
}
#[test]
fn six_digit() {
assert_eq!(parse_hex("#ff0000"), Some(rgb(1.0, 0.0, 0.0, None)));
assert_eq!(parse_hex("#000000"), Some(rgb(0.0, 0.0, 0.0, None)));
}
#[test]
fn eight_digit() {
assert_eq!(parse_hex("#ff0000ff"), Some(rgb(1.0, 0.0, 0.0, Some(1.0))));
assert_eq!(
parse_hex("#ff000080"),
Some(rgb(1.0, 0.0, 0.0, Some(128.0 / 255.0)))
);
}
#[test]
fn case_insensitive() {
assert_eq!(parse_hex("#FF0000"), parse_hex("#ff0000"));
assert_eq!(parse_hex("#FfA500"), parse_hex("#ffa500"));
}
#[test]
fn leading_hash_optional() {
assert_eq!(parse_hex("ff0000"), parse_hex("#ff0000"));
assert_eq!(parse_hex("f00"), parse_hex("#f00"));
}
#[test]
fn invalid_lengths_rejected() {
assert_eq!(parse_hex("#"), None);
assert_eq!(parse_hex("#1"), None);
assert_eq!(parse_hex("#12"), None);
assert_eq!(parse_hex("#12345"), None);
assert_eq!(parse_hex("#1234567"), None);
assert_eq!(parse_hex("#123456789"), None);
assert_eq!(parse_hex(""), None);
}
#[test]
fn non_hex_chars_rejected() {
assert_eq!(parse_hex("#xyz"), None);
assert_eq!(parse_hex("#gg0000"), None);
assert_eq!(parse_hex("# f00"), None);
assert_eq!(parse_hex("#f00 "), None);
}
}