use super::*;
pub(crate) fn parse_css_color(input: &str) -> Option<Color> {
let text: &str = input.trim();
if text.is_empty() {
return None;
}
let lower: String = text.to_ascii_lowercase();
if let Some(hex) = lower.strip_prefix('#') {
return parse_hex(hex);
}
if lower == "transparent" {
return Some(Color::rgba(0, 0, 0, 0));
}
if let Some(body) = strip_function(&lower, "rgba(") {
let parts: Vec<String> = split_arguments(body);
if parts.len() == 4 {
let red: u8 = parse_channel(&parts[0])?;
let green: u8 = parse_channel(&parts[1])?;
let blue: u8 = parse_channel(&parts[2])?;
let alpha: u8 = parse_alpha(&parts[3])?;
return Some(Color::rgba(red, green, blue, alpha));
}
return None;
}
if let Some(body) = strip_function(&lower, "rgb(") {
let parts: Vec<String> = split_arguments(body);
if parts.len() == 3 {
let red: u8 = parse_channel(&parts[0])?;
let green: u8 = parse_channel(&parts[1])?;
let blue: u8 = parse_channel(&parts[2])?;
return Some(Color::rgb(red, green, blue));
}
return None;
}
if let Some(body) = strip_function(&lower, "hsla(") {
let parts: Vec<String> = split_arguments(body);
if parts.len() == 4 {
let (red, green, blue) = parse_hsl(&parts[0], &parts[1], &parts[2])?;
let alpha: u8 = parse_alpha(&parts[3])?;
return Some(Color::rgba(red, green, blue, alpha));
}
return None;
}
if let Some(body) = strip_function(&lower, "hsl(") {
let parts: Vec<String> = split_arguments(body);
if parts.len() == 3 {
let (red, green, blue) = parse_hsl(&parts[0], &parts[1], &parts[2])?;
return Some(Color::rgb(red, green, blue));
}
return None;
}
named_color(&lower)
}
fn strip_function<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
let body: &str = text.strip_prefix(prefix)?;
body.strip_suffix(')')
}
fn split_arguments(body: &str) -> Vec<String> {
let mut parts: Vec<String> = Vec::new();
let mut current: String = String::new();
let mut depth: usize = 0usize;
for character in body.chars() {
match character {
'(' => {
depth += 1;
current.push(character);
}
')' => {
depth = depth.saturating_sub(1);
current.push(character);
}
',' if depth == 0 => {
parts.push(current.trim().to_string());
current.clear();
}
'/' if depth == 0 => {
parts.push(current.trim().to_string());
current.clear();
}
_ => current.push(character),
}
}
let tail: &str = current.trim();
if !tail.is_empty() {
parts.push(tail.to_string());
}
parts
}
fn parse_hex(hex: &str) -> Option<Color> {
if !hex
.chars()
.all(|character: char| character.is_ascii_hexdigit())
{
return None;
}
match hex.len() {
3 => {
let red: u8 = expand_hex(hex.as_bytes()[0])?;
let green: u8 = expand_hex(hex.as_bytes()[1])?;
let blue: u8 = expand_hex(hex.as_bytes()[2])?;
Some(Color::rgb(red, green, blue))
}
4 => {
let red: u8 = expand_hex(hex.as_bytes()[0])?;
let green: u8 = expand_hex(hex.as_bytes()[1])?;
let blue: u8 = expand_hex(hex.as_bytes()[2])?;
let alpha: u8 = expand_hex(hex.as_bytes()[3])?;
Some(Color::rgba(red, green, blue, alpha))
}
6 => {
let red: u8 = hex_byte(&hex[0..2])?;
let green: u8 = hex_byte(&hex[2..4])?;
let blue: u8 = hex_byte(&hex[4..6])?;
Some(Color::rgb(red, green, blue))
}
8 => {
let red: u8 = hex_byte(&hex[0..2])?;
let green: u8 = hex_byte(&hex[2..4])?;
let blue: u8 = hex_byte(&hex[4..6])?;
let alpha: u8 = hex_byte(&hex[6..8])?;
Some(Color::rgba(red, green, blue, alpha))
}
_ => None,
}
}
fn expand_hex(digit: u8) -> Option<u8> {
let value: u8 = (digit as char).to_digit(16)? as u8;
Some(value * 17)
}
fn hex_byte(text: &str) -> Option<u8> {
u8::from_str_radix(text, 16).ok()
}
fn parse_channel(text: &str) -> Option<u8> {
let trimmed: &str = text.trim();
if let Some(percent) = trimmed.strip_suffix('%') {
let value: f64 = percent.trim().parse::<f64>().ok()?;
return Some((value / 100.0 * 255.0).round().clamp(0.0, 255.0) as u8);
}
let value: f64 = trimmed.parse::<f64>().ok()?;
Some(value.round().clamp(0.0, 255.0) as u8)
}
fn parse_alpha(text: &str) -> Option<u8> {
let trimmed: &str = text.trim();
if let Some(percent) = trimmed.strip_suffix('%') {
let value: f64 = percent.trim().parse::<f64>().ok()?;
return Some((value / 100.0 * 255.0).round().clamp(0.0, 255.0) as u8);
}
let value: f64 = trimmed.parse::<f64>().ok()?;
Some((value.clamp(0.0, 1.0) * 255.0).round() as u8)
}
fn parse_hsl(hue: &str, saturation: &str, lightness: &str) -> Option<(u8, u8, u8)> {
let hue: f64 = hue.trim().parse::<f64>().ok()?;
let saturation: f64 = percent_value(saturation)?;
let lightness: f64 = percent_value(lightness)?;
let hue: f64 = ((hue % 360.0) + 360.0) % 360.0;
let saturation: f64 = saturation.clamp(0.0, 1.0);
let lightness: f64 = lightness.clamp(0.0, 1.0);
let chroma: f64 = (1.0 - (2.0 * lightness - 1.0).abs()) * saturation;
let secondary: f64 = chroma * (1.0 - (((hue / 60.0) % 2.0) - 1.0).abs());
let match_value: f64 = lightness - chroma / 2.0;
let (red, green, blue) = match hue as u32 / 60 {
0 => (chroma, secondary, 0.0),
1 => (secondary, chroma, 0.0),
2 => (0.0, chroma, secondary),
3 => (0.0, secondary, chroma),
4 => (secondary, 0.0, chroma),
_ => (chroma, 0.0, secondary),
};
Some((
((red + match_value) * 255.0).round().clamp(0.0, 255.0) as u8,
((green + match_value) * 255.0).round().clamp(0.0, 255.0) as u8,
((blue + match_value) * 255.0).round().clamp(0.0, 255.0) as u8,
))
}
fn percent_value(text: &str) -> Option<f64> {
let trimmed: &str = text.trim();
let value = match trimmed.strip_suffix('%') {
Some(percent) => percent.trim().parse::<f64>().ok()? / 100.0,
None => trimmed.parse::<f64>().ok()?,
};
Some(value)
}
pub(crate) fn named_color(name: &str) -> Option<Color> {
NAMED_COLORS
.binary_search_by(|(candidate, _): &(&str, u32)| (*candidate).cmp(name))
.ok()
.map(|index: usize| Color::from_u32((NAMED_COLORS[index].1 << 8) | 0xFF))
}