From b3338852280f6380d23a3791dbcdb96ec315fb75 Mon Sep 17 00:00:00 2001 From: Divyanshu Gupta Date: Fri, 16 Oct 2026 02:53:39 +0530 Subject: [PATCH] fix(formats): support 4/8-digit hex, space-separated rgb and hsl in parse_color --- crates/formats/src/model.rs | 86 ++++++++++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/crates/formats/src/model.rs b/crates/formats/src/model.rs index fc3642cd1..fb338a071 100644 --- a/crates/formats/src/model.rs +++ b/crates/formats/src/model.rs @@ -1201,20 +1201,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 { 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 = 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 = parts + .iter() .take(3) .filter_map(|x| { let x = x.trim(); @@ -1230,6 +1270,35 @@ pub fn parse_color(s: &str) -> Option { } 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::().ok()? * 180.0 / std::f64::consts::PI + } else if let Some(turn) = h_str.strip_suffix("turn") { + turn.trim().parse::().ok()? * 360.0 + } else { + h_str.parse().ok()? + }; + let parse_pct = |x: &str| -> Option { + let x = x.trim(); + if let Some(p) = x.strip_suffix('%') { + Some((p.trim().parse::().ok()? / 100.0).clamp(0.0, 1.0)) + } else { + Some(x.parse::().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), @@ -1280,8 +1349,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);