mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-28 04:17:06 -06:00
Merge PR #2249: fix(formats): support 4/8-digit hex, space-separated rgb and hsl in parse_color
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user