use crate::utils::parse_values;
use crate::utils::remap;
use crate::utils::ParamParser;
use crate::{Color, ParseColorError};
#[cfg(feature = "named-colors")]
use crate::NAMED_COLORS;
#[inline(never)]
pub fn parse(s: &str) -> Result<Color, ParseColorError> {
let s = s.trim();
let err = match parse_abs(s) {
Ok(c) => return Ok(c),
Err(e @ ParseColorError::InvalidHex) => return Err(e),
Err(e @ ParseColorError::InvalidFunction) => return Err(e),
Err(e @ ParseColorError::InvalidUnknown) => return Err(e),
Err(e) => e,
};
if let (Some(idx), Some(s)) = (s.find('('), s.strip_suffix(')')) {
if !s.is_ascii() {
return Err(err);
}
let mut pp = ParamParser::new(&s[idx + 1..]);
pp.space();
#[rustfmt::skip]
let (
Some(from), true,
Some(color), true,
Some(val1), true,
Some(val2), true,
Some(val3),
) = (
pp.value(), pp.space(),
pp.value(), pp.space(),
pp.value(), pp.space(),
pp.value(), pp.space(),
pp.value(),
) else {
return Err(err);
};
if !from.eq_ignore_ascii_case("from") {
return Err(err);
}
let Ok(color) = parse(color) else {
return Err(err);
};
pp.space();
let val4 = if pp.is_end() {
"alpha"
} else if let (true, Some(alpha), _, true) =
(pp.slash(), pp.value(), pp.space(), pp.is_end())
{
alpha
} else {
return Err(err);
};
let values = [val1, val2, val3, val4];
match err {
ParseColorError::InvalidRgb => {
let variables = [
("r", color.r * 255.0),
("g", color.g * 255.0),
("b", color.b * 255.0),
("alpha", color.a),
];
if let Some([r, g, b, a]) = parse_values(values, variables) {
return Ok(Color::new(r / 255.0, g / 255.0, b / 255.0, a));
};
}
ParseColorError::InvalidHwb => {
let [h, w, b, a] = color.to_hwba();
let variables = [("h", h), ("w", w * 100.0), ("b", b * 100.0), ("alpha", a)];
if let Some([h, w, b, a]) = parse_values(values, variables) {
return Ok(Color::from_hwba(h, w / 100.0, b / 100.0, a));
};
}
ParseColorError::InvalidHsl => {
let [h, s, l, a] = color.to_hsla();
let variables = [("h", h), ("s", s * 100.0), ("l", l * 100.0), ("alpha", a)];
if let Some([h, s, l, a]) = parse_values(values, variables) {
return Ok(Color::from_hsla(
h,
(s / 100.0).clamp(0.0, 1.0),
(l / 100.0).clamp(0.0, 1.0),
a,
));
};
}
ParseColorError::InvalidHsv => {
let [h, s, v, a] = color.to_hsva();
let variables = [("h", h), ("s", s * 100.0), ("v", v * 100.0), ("alpha", a)];
if let Some([h, s, v, a]) = parse_values(values, variables) {
return Ok(Color::from_hsva(h, s / 100.0, v / 100.0, a));
};
}
ParseColorError::InvalidLab => {
let [l, a, b, alpha] = color.to_laba();
let variables = [("l", l), ("a", a), ("b", b), ("alpha", alpha)];
if let Some([l, a, b, alpha]) = parse_values(values, variables) {
return Ok(Color::from_laba(l.max(0.0), a, b, alpha));
};
}
ParseColorError::InvalidLch => {
let [l, c, h, a] = color.to_lcha();
let variables = [("l", l), ("c", c), ("h", h.to_degrees()), ("alpha", a)];
if let Some([l, c, h, a]) = parse_values(values, variables) {
return Ok(Color::from_lcha(l.max(0.0), c.max(0.0), h.to_radians(), a));
};
}
ParseColorError::InvalidOklab => {
let [l, a, b, alpha] = color.to_oklaba();
let variables = [("l", l), ("a", a), ("b", b), ("alpha", alpha)];
if let Some([l, a, b, alpha]) = parse_values(values, variables) {
return Ok(Color::from_oklaba(l.max(0.0), a, b, alpha));
};
}
ParseColorError::InvalidOklch => {
let [l, c, h, a] = color.to_oklcha();
let variables = [("l", l), ("c", c), ("h", h.to_degrees()), ("alpha", a)];
if let Some([l, c, h, a]) = parse_values(values, variables) {
return Ok(Color::from_oklcha(
l.max(0.0),
c.max(0.0),
h.to_radians(),
a,
));
};
}
_ => unreachable!(),
}
return Err(err);
}
unreachable!();
}
fn parse_abs(s: &str) -> Result<Color, ParseColorError> {
if s.eq_ignore_ascii_case("transparent") {
return Ok(Color::new(0.0, 0.0, 0.0, 0.0));
}
if let Some(s) = s.strip_prefix('#') {
return parse_hex(s);
}
if let (Some(idx), Some(s)) = (s.find('('), s.strip_suffix(')')) {
let fname = &s[..idx].trim_end();
let err = match fname {
s if s.eq_ignore_ascii_case("rgb") || s.eq_ignore_ascii_case("rgba") => {
ParseColorError::InvalidRgb
}
s if s.eq_ignore_ascii_case("hsl") || s.eq_ignore_ascii_case("hsla") => {
ParseColorError::InvalidHsl
}
s if s.eq_ignore_ascii_case("hwb") || s.eq_ignore_ascii_case("hwba") => {
ParseColorError::InvalidHwb
}
s if s.eq_ignore_ascii_case("hsv") || s.eq_ignore_ascii_case("hsva") => {
ParseColorError::InvalidHsv
}
s if s.eq_ignore_ascii_case("lab") => ParseColorError::InvalidLab,
s if s.eq_ignore_ascii_case("lch") => ParseColorError::InvalidLch,
s if s.eq_ignore_ascii_case("oklab") => ParseColorError::InvalidOklab,
s if s.eq_ignore_ascii_case("oklch") => ParseColorError::InvalidOklch,
_ => return Err(ParseColorError::InvalidFunction),
};
let s = &s[idx + 1..];
if !s.is_ascii() {
return Err(err);
}
let mut pp = ParamParser::new(s);
pp.space();
let (Some(val0), true, Some(val1), true, Some(val2)) = (
pp.value(),
pp.comma_or_space(),
pp.value(),
pp.comma_or_space(),
pp.value(),
) else {
return Err(err);
};
let is_space = pp.space();
let alpha = if pp.is_end() {
1.0
} else if let (true, Some(a), _, true) = (
pp.comma_or_slash() || is_space,
pp.value(),
pp.space(),
pp.is_end(),
) {
if let Some((v, _)) = parse_percent_or_float(a) {
v.clamp(0.0, 1.0)
} else {
return Err(err);
}
} else {
return Err(err);
};
match err {
ParseColorError::InvalidRgb => {
if let (Some(r), Some(g), Some(b)) = (
parse_percent_or_255(val0),
parse_percent_or_255(val1),
parse_percent_or_255(val2),
) {
return Ok(Color {
r: r.clamp(0.0, 1.0),
g: g.clamp(0.0, 1.0),
b: b.clamp(0.0, 1.0),
a: alpha,
});
}
}
ParseColorError::InvalidHsl => {
if let (Some(h), Some((s, _)), Some((l, _))) = (
parse_angle(val0),
parse_percent_or_float(val1),
parse_percent_or_float(val2),
) {
return Ok(Color::from_hsla(h, s, l, alpha));
}
}
ParseColorError::InvalidHwb => {
if let (Some(h), Some((w, _)), Some((b, _))) = (
parse_angle(val0),
parse_percent_or_float(val1),
parse_percent_or_float(val2),
) {
return Ok(Color::from_hwba(h, w, b, alpha));
}
}
ParseColorError::InvalidHsv => {
if let (Some(h), Some((s, _)), Some((v, _))) = (
parse_angle(val0),
parse_percent_or_float(val1),
parse_percent_or_float(val2),
) {
return Ok(Color::from_hsva(h, s, v, alpha));
}
}
ParseColorError::InvalidLab => {
if let (Some((l, l_pct)), Some((a, a_pct)), Some((b, b_pct))) = (
parse_percent_or_float(val0),
parse_percent_or_float(val1),
parse_percent_or_float(val2),
) {
let l = if l_pct { l * 100.0 } else { l };
let a = if a_pct {
remap(a, -1.0, 1.0, -125.0, 125.0)
} else {
a
};
let b = if b_pct {
remap(b, -1.0, 1.0, -125.0, 125.0)
} else {
b
};
return Ok(Color::from_laba(l.max(0.0), a, b, alpha));
}
}
ParseColorError::InvalidLch => {
if let (Some((l, l_pct)), Some((c, c_pct)), Some(h)) = (
parse_percent_or_float(val0),
parse_percent_or_float(val1),
parse_angle(val2),
) {
let l = if l_pct { l * 100.0 } else { l };
let c = if c_pct { c * 150.0 } else { c };
return Ok(Color::from_lcha(
l.max(0.0),
c.max(0.0),
h.to_radians(),
alpha,
));
}
}
ParseColorError::InvalidOklab => {
if let (Some((l, _)), Some((a, a_pct)), Some((b, b_pct))) = (
parse_percent_or_float(val0),
parse_percent_or_float(val1),
parse_percent_or_float(val2),
) {
let a = if a_pct {
remap(a, -1.0, 1.0, -0.4, 0.4)
} else {
a
};
let b = if b_pct {
remap(b, -1.0, 1.0, -0.4, 0.4)
} else {
b
};
return Ok(Color::from_oklaba(l.max(0.0), a, b, alpha));
}
}
ParseColorError::InvalidOklch => {
if let (Some((l, _)), Some((c, c_pct)), Some(h)) = (
parse_percent_or_float(val0),
parse_percent_or_float(val1),
parse_angle(val2),
) {
let c = if c_pct { c * 0.4 } else { c };
return Ok(Color::from_oklcha(
l.max(0.0),
c.max(0.0),
h.to_radians(),
alpha,
));
}
}
_ => unreachable!(),
}
return Err(err);
}
if let Ok(c) = parse_hex(s) {
return Ok(c);
}
#[cfg(feature = "named-colors")]
if s.len() > 2 && s.len() < 21 {
if let Some([r, g, b]) = NAMED_COLORS.get(s.into()) {
return Ok(Color::from_rgba8(*r, *g, *b, 255));
}
}
Err(ParseColorError::InvalidUnknown)
}
fn parse_hex(s: &str) -> Result<Color, ParseColorError> {
if !s.is_ascii() {
return Err(ParseColorError::InvalidHex);
}
let n = s.len();
fn parse_single_digit(digit: &str) -> Result<u8, ParseColorError> {
u8::from_str_radix(digit, 16)
.map(|n| (n << 4) | n)
.map_err(|_| ParseColorError::InvalidHex)
}
if n == 3 || n == 4 {
let r = parse_single_digit(&s[0..1])?;
let g = parse_single_digit(&s[1..2])?;
let b = parse_single_digit(&s[2..3])?;
let a = if n == 4 {
parse_single_digit(&s[3..4])?
} else {
255
};
Ok(Color::from_rgba8(r, g, b, a))
} else if n == 6 || n == 8 {
let r = u8::from_str_radix(&s[0..2], 16).map_err(|_| ParseColorError::InvalidHex)?;
let g = u8::from_str_radix(&s[2..4], 16).map_err(|_| ParseColorError::InvalidHex)?;
let b = u8::from_str_radix(&s[4..6], 16).map_err(|_| ParseColorError::InvalidHex)?;
let a = if n == 8 {
u8::from_str_radix(&s[6..8], 16).map_err(|_| ParseColorError::InvalidHex)?
} else {
255
};
Ok(Color::from_rgba8(r, g, b, a))
} else {
Err(ParseColorError::InvalidHex)
}
}
fn strip_suffix<'a>(s: &'a str, suffix: &str) -> Option<&'a str> {
if suffix.len() > s.len() {
return None;
}
let s_end = &s[s.len() - suffix.len()..];
if s_end.eq_ignore_ascii_case(suffix) {
Some(&s[..s.len() - suffix.len()])
} else {
None
}
}
fn parse_percent_or_float(s: &str) -> Option<(f32, bool)> {
if s.eq_ignore_ascii_case("none") {
return Some((0.0, false));
}
s.strip_suffix('%')
.and_then(|s| s.parse().ok().map(|t: f32| (t / 100.0, true)))
.or_else(|| s.parse().ok().map(|t| (t, false)))
}
fn parse_percent_or_255(s: &str) -> Option<f32> {
if s.eq_ignore_ascii_case("none") {
return Some(0.0);
}
s.strip_suffix('%')
.and_then(|s| s.parse().ok().map(|t: f32| t / 100.0))
.or_else(|| s.parse().ok().map(|t: f32| t / 255.0))
}
fn parse_angle(s: &str) -> Option<f32> {
if s.eq_ignore_ascii_case("none") {
return Some(0.0);
}
strip_suffix(s, "deg")
.and_then(|s| s.parse().ok())
.or_else(|| {
strip_suffix(s, "grad")
.and_then(|s| s.parse().ok())
.map(|t: f32| t * 360.0 / 400.0)
})
.or_else(|| {
strip_suffix(s, "rad")
.and_then(|s| s.parse().ok())
.map(|t: f32| t.to_degrees())
})
.or_else(|| {
strip_suffix(s, "turn")
.and_then(|s| s.parse().ok())
.map(|t: f32| t * 360.0)
})
.or_else(|| s.parse().ok())
}
#[cfg(test)]
mod t {
use super::*;
#[test]
fn strip_suffix_() {
assert_eq!(strip_suffix("45deg", "deg"), Some("45"));
assert_eq!(strip_suffix("90DEG", "deg"), Some("90"));
assert_eq!(strip_suffix("0.25turn", "turn"), Some("0.25"));
assert_eq!(strip_suffix("1.0Turn", "turn"), Some("1.0"));
assert_eq!(strip_suffix("", "deg"), None);
assert_eq!(strip_suffix("90", "deg"), None);
}
#[test]
fn parse_percent_or_float_() {
let test_data = [
("none", Some((0.0, false))),
("NONE", Some((0.0, false))),
("0%", Some((0.0, true))),
("100%", Some((1.0, true))),
("50%", Some((0.5, true))),
("0", Some((0.0, false))),
("1", Some((1.0, false))),
("0.5", Some((0.5, false))),
("100.0", Some((100.0, false))),
("-23.7", Some((-23.7, false))),
("%", None),
("1x", None),
];
for (s, expected) in test_data {
assert_eq!(parse_percent_or_float(s), expected);
}
}
#[test]
fn parse_percent_or_255_() {
let test_data = [
("none", Some(0.0)),
("NONE", Some(0.0)),
("0%", Some(0.0)),
("100%", Some(1.0)),
("50%", Some(0.5)),
("-100%", Some(-1.0)),
("0", Some(0.0)),
("255", Some(1.0)),
("127.5", Some(0.5)),
("%", None),
("255x", None),
];
for (s, expected) in test_data {
assert_eq!(parse_percent_or_255(s), expected);
}
}
#[test]
fn parse_angle_() {
let test_data = [
("none", Some(0.0)),
("NONE", Some(0.0)),
("360", Some(360.0)),
("127.356", Some(127.356)),
("+120deg", Some(120.0)),
("90deg", Some(90.0)),
("-127deg", Some(-127.0)),
("100grad", Some(90.0)),
("1.5707963267948966rad", Some(90.0)),
("0.25turn", Some(90.0)),
("-0.25turn", Some(-90.0)),
("O", None),
("Odeg", None),
("rad", None),
];
for (s, expected) in test_data {
assert_eq!(parse_angle(s), expected);
}
}
#[test]
fn parse_hex_() {
macro_rules! cmp {
($a:expr, $b:expr) => {
assert_eq!(
parse_hex($a).unwrap().to_rgba8(),
parse_hex($b).unwrap().to_rgba8()
);
};
}
cmp!("abc", "ABC");
cmp!("DeF", "dEf");
cmp!("f0eB", "F0Eb");
cmp!("abcdef", "ABCDEF");
cmp!("Ff03E0cB", "fF03e0Cb");
}
}