Merge PR #2249: fix(formats): support 4/8-digit hex, space-separated rgb and hsl in parse_color

This commit is contained in:
aiongg
2026-10-22 01:19:47 +00:00
+81 -5
View File
@@ -1296,20 +1296,60 @@ impl ListCounter {
}
}
/// Parse a CSS / HTML colour: `#rgb`, `#rrggbb`, `rgb(r,g,b)` or a basic name.
fn hsl_to_rgb(h: f64, s: f64, l: f64) -> Rgb {
let h = (h / 360.0).rem_euclid(1.0);
let s = s.clamp(0.0, 1.0);
let l = l.clamp(0.0, 1.0);
if s == 0.0 {
let v = (l * 255.0).round().clamp(0.0, 255.0) as u8;
return Rgb(v, v, v);
}
let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s };
let p = 2.0 * l - q;
let hue_to_rgb = |mut t: f64| -> f64 {
if t < 0.0 {
t += 1.0;
}
if t > 1.0 {
t -= 1.0;
}
if t < 1.0 / 6.0 {
return p + (q - p) * 6.0 * t;
}
if t < 1.0 / 2.0 {
return q;
}
if t < 2.0 / 3.0 {
return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
}
p
};
let r = (hue_to_rgb(h + 1.0 / 3.0) * 255.0).round().clamp(0.0, 255.0) as u8;
let g = (hue_to_rgb(h) * 255.0).round().clamp(0.0, 255.0) as u8;
let b = (hue_to_rgb(h - 1.0 / 3.0) * 255.0).round().clamp(0.0, 255.0) as u8;
Rgb(r, g, b)
}
/// Parse a CSS / HTML colour: `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `rgb(...)`, `hsl(...)` or a basic name.
pub fn parse_color(s: &str) -> Option<Rgb> {
let s = s.trim().to_ascii_lowercase();
if let Some(h) = s.strip_prefix('#') {
if h.len() == 3 && h.is_ascii() {
if (h.len() == 3 || h.len() == 4) && h.is_ascii() {
let d = |i: usize| h.get(i..i + 1).and_then(|x| u8::from_str_radix(x, 16).ok()).map(|v| v * 17);
return Some(Rgb(d(0)?, d(1)?, d(2)?));
}
if h.len() == 8 && h.is_ascii() {
let p = |i: usize| h.get(i..i + 2).and_then(|x| u8::from_str_radix(x, 16).ok());
return Some(Rgb(p(0)?, p(2)?, p(4)?));
}
return Rgb::parse(h);
}
if let Some(inner) = s.strip_prefix("rgb(").or_else(|| s.strip_prefix("rgba(")) {
let v: Vec<u8> = inner
.trim_end_matches(')')
.split(',')
let inner = inner.trim_end_matches(')').trim();
let before_slash = inner.split('/').next().unwrap_or(inner);
let parts: Vec<&str> = if before_slash.contains(',') { before_slash.split(',').collect() } else { before_slash.split_whitespace().collect() };
let v: Vec<u8> = parts
.iter()
.take(3)
.filter_map(|x| {
let x = x.trim();
@@ -1325,6 +1365,35 @@ pub fn parse_color(s: &str) -> Option<Rgb> {
}
return None;
}
if let Some(inner) = s.strip_prefix("hsl(").or_else(|| s.strip_prefix("hsla(")) {
let inner = inner.trim_end_matches(')').trim();
let before_slash = inner.split('/').next().unwrap_or(inner);
let parts: Vec<&str> = if before_slash.contains(',') { before_slash.split(',').collect() } else { before_slash.split_whitespace().collect() };
if parts.len() < 3 {
return None;
}
let h_str = parts[0].trim();
let h: f64 = if let Some(deg) = h_str.strip_suffix("deg") {
deg.trim().parse().ok()?
} else if let Some(rad) = h_str.strip_suffix("rad") {
rad.trim().parse::<f64>().ok()? * 180.0 / std::f64::consts::PI
} else if let Some(turn) = h_str.strip_suffix("turn") {
turn.trim().parse::<f64>().ok()? * 360.0
} else {
h_str.parse().ok()?
};
let parse_pct = |x: &str| -> Option<f64> {
let x = x.trim();
if let Some(p) = x.strip_suffix('%') {
Some((p.trim().parse::<f64>().ok()? / 100.0).clamp(0.0, 1.0))
} else {
Some(x.parse::<f64>().ok()?.clamp(0.0, 1.0))
}
};
let s = parse_pct(parts[1])?;
let l = parse_pct(parts[2])?;
return Some(hsl_to_rgb(h, s, l));
}
Some(match s.as_str() {
"black" => Rgb(0, 0, 0),
"white" => Rgb(255, 255, 255),
@@ -1375,8 +1444,15 @@ mod tests {
#[test]
fn colors() {
assert_eq!(parse_color("#f00"), Some(Rgb(255, 0, 0)));
assert_eq!(parse_color("#f00f"), Some(Rgb(255, 0, 0)));
assert_eq!(parse_color("#0080FF"), Some(Rgb(0, 128, 255)));
assert_eq!(parse_color("#0080ff80"), Some(Rgb(0, 128, 255)));
assert_eq!(parse_color("rgb(1, 2, 3)"), Some(Rgb(1, 2, 3)));
assert_eq!(parse_color("rgb(1 2 3)"), Some(Rgb(1, 2, 3)));
assert_eq!(parse_color("rgb(1 2 3 / 50%)"), Some(Rgb(1, 2, 3)));
assert_eq!(parse_color("hsl(0, 100%, 50%)"), Some(Rgb(255, 0, 0)));
assert_eq!(parse_color("hsl(120deg 100% 50%)"), Some(Rgb(0, 255, 0)));
assert_eq!(parse_color("hsla(240, 100%, 50%, 0.8)"), Some(Rgb(0, 0, 255)));
assert_eq!(parse_color("navy"), Some(Rgb(0, 0, 128)));
assert_eq!(parse_color("#zz"), None);
assert_eq!(parse_color("rgb(1)"), None);