use crate::ast::{AngleUnit, Color, ColorChannel};
use crate::color::is_hex_color_body;
use crate::escapes::trim_css;
use crate::keyword_set::ascii_keyword_set;
pub(crate) fn parse_color(s: &str) -> Option<Color> {
if s.strip_prefix('#').is_some_and(is_hex_color_body) {
return Some(Color::Hex);
}
if is_named_color(s) {
return Some(Color::Named);
}
None
}
pub(crate) fn parse_color_function(name: &str, args_str: &str) -> Option<Color> {
if name.eq_ignore_ascii_case("rgb") || name.eq_ignore_ascii_case("rgba") {
parse_rgb(args_str)
} else if name.eq_ignore_ascii_case("hsl") || name.eq_ignore_ascii_case("hsla") {
parse_hsl(args_str)
} else {
None
}
}
fn parse_color_channel(s: &str) -> Option<ColorChannel> {
let s = trim_css(s);
if s.eq_ignore_ascii_case("none") {
return Some(ColorChannel::None);
}
if let Some(percent_str) = s.strip_suffix('%')
&& let Ok(value) = percent_str.parse::<f64>()
{
return Some(ColorChannel::Percentage(value));
}
if let Ok(value) = s.parse::<f64>() {
return Some(ColorChannel::Number(value));
}
None
}
fn parse_hue(s: &str) -> Option<(ColorChannel, Option<AngleUnit>)> {
let s = trim_css(s);
if s.eq_ignore_ascii_case("none") {
return Some((ColorChannel::None, None));
}
let (value_str, unit) = if s.ends_with("deg") {
(s.trim_end_matches("deg"), Some(AngleUnit::Deg))
} else if s.ends_with("rad") {
(s.trim_end_matches("rad"), Some(AngleUnit::Rad))
} else if s.ends_with("turn") {
(s.trim_end_matches("turn"), Some(AngleUnit::Turn))
} else if s.ends_with("grad") {
(s.trim_end_matches("grad"), Some(AngleUnit::Grad))
} else {
(s, None)
};
if let Ok(value) = trim_css(value_str).parse::<f64>() {
return Some((ColorChannel::Number(value), unit));
}
None
}
type ColorArgs<'a> = (&'a str, &'a str, &'a str, Option<&'a str>);
fn split_color_args(args_str: &str) -> Option<ColorArgs<'_>> {
let args_str = trim_css(args_str);
let bytes = args_str.as_bytes();
let mut channels = [(0, 0); 3];
let mut channel_count = 0;
let mut commas = [0; 3];
let mut comma_count = 0;
let mut channel_start = None;
let mut slash = None;
for (i, &b) in bytes.iter().enumerate() {
match b {
b'/' => {
slash = Some(i);
break;
}
b',' | b' ' | b'\t' | b'\n' | b'\x0C' | b'\r' => {
if let Some(start) = channel_start.take() {
if channel_count < 3 {
channels[channel_count] = (start, i);
}
channel_count = (channel_count + 1).min(4);
}
if b == b',' {
if comma_count < 3 {
commas[comma_count] = i;
}
comma_count = (comma_count + 1).min(4);
}
}
_ => {
if channel_start.is_none() {
channel_start = Some(i);
}
}
}
}
if let Some(start) = channel_start {
if channel_count < 3 {
channels[channel_count] = (start, slash.unwrap_or(bytes.len()));
}
channel_count = (channel_count + 1).min(4);
}
let channel = |(start, end): (usize, usize)| &args_str[start..end];
if let Some(slash) = slash {
if channel_count != 3 {
return None; }
let [c1, c2, c3] = channels.map(channel);
Some((c1, c2, c3, Some(trim_css(&args_str[slash + 1..]))))
} else if comma_count > 0 {
let part = |start: usize, end: usize| trim_css(&args_str[start..end]);
match comma_count {
2 => Some((
part(0, commas[0]),
part(commas[0] + 1, commas[1]),
part(commas[1] + 1, bytes.len()),
None,
)),
3 => Some((
part(0, commas[0]),
part(commas[0] + 1, commas[1]),
part(commas[1] + 1, commas[2]),
Some(part(commas[2] + 1, bytes.len())),
)),
_ => None,
}
} else {
if channel_count != 3 {
return None; }
let [c1, c2, c3] = channels.map(channel);
Some((c1, c2, c3, None))
}
}
fn parse_rgb(args_str: &str) -> Option<Color> {
let (red, green, blue, alpha_part) = split_color_args(args_str)?;
let r = parse_color_channel(red)?;
let g = parse_color_channel(green)?;
let b = parse_color_channel(blue)?;
let alpha = match alpha_part {
Some(alpha) => Some(parse_color_channel(alpha)?),
None => None,
};
Some(Color::Rgb { r, g, b, alpha })
}
fn parse_hsl(args_str: &str) -> Option<Color> {
let (hue_part, saturation_part, lightness_part, alpha_part) = split_color_args(args_str)?;
let (hue, hue_unit) = parse_hue(hue_part)?;
let saturation = parse_color_channel(saturation_part)?;
let lightness = parse_color_channel(lightness_part)?;
let alpha = match alpha_part {
Some(alpha) => Some(parse_color_channel(alpha)?),
None => None,
};
Some(Color::Hsl {
hue,
hue_unit,
saturation,
lightness,
alpha,
})
}
ascii_keyword_set! {
static NAMED_COLORS;
fn is_named_color;
"aliceblue",
"antiquewhite",
"aqua",
"aquamarine",
"azure",
"beige",
"bisque",
"black",
"blanchedalmond",
"blue",
"blueviolet",
"brown",
"burlywood",
"cadetblue",
"chartreuse",
"chocolate",
"coral",
"cornflowerblue",
"cornsilk",
"crimson",
"cyan",
"darkblue",
"darkcyan",
"darkgoldenrod",
"darkgray",
"darkgrey",
"darkgreen",
"darkkhaki",
"darkmagenta",
"darkolivegreen",
"darkorange",
"darkorchid",
"darkred",
"darksalmon",
"darkseagreen",
"darkslateblue",
"darkslategray",
"darkslategrey",
"darkturquoise",
"darkviolet",
"deeppink",
"deepskyblue",
"dimgray",
"dimgrey",
"dodgerblue",
"firebrick",
"floralwhite",
"forestgreen",
"fuchsia",
"gainsboro",
"ghostwhite",
"gold",
"goldenrod",
"gray",
"grey",
"green",
"greenyellow",
"honeydew",
"hotpink",
"indianred",
"indigo",
"ivory",
"khaki",
"lavender",
"lavenderblush",
"lawngreen",
"lemonchiffon",
"lightblue",
"lightcoral",
"lightcyan",
"lightgoldenrodyellow",
"lightgray",
"lightgrey",
"lightgreen",
"lightpink",
"lightsalmon",
"lightseagreen",
"lightskyblue",
"lightslategray",
"lightslategrey",
"lightsteelblue",
"lightyellow",
"lime",
"limegreen",
"linen",
"magenta",
"maroon",
"mediumaquamarine",
"mediumblue",
"mediumorchid",
"mediumpurple",
"mediumseagreen",
"mediumslateblue",
"mediumspringgreen",
"mediumturquoise",
"mediumvioletred",
"midnightblue",
"mintcream",
"mistyrose",
"moccasin",
"navajowhite",
"navy",
"oldlace",
"olive",
"olivedrab",
"orange",
"orangered",
"orchid",
"palegoldenrod",
"palegreen",
"paleturquoise",
"palevioletred",
"papayawhip",
"peachpuff",
"peru",
"pink",
"plum",
"powderblue",
"purple",
"red",
"rosybrown",
"royalblue",
"saddlebrown",
"salmon",
"sandybrown",
"seagreen",
"seashell",
"sienna",
"silver",
"skyblue",
"slateblue",
"slategray",
"slategrey",
"snow",
"springgreen",
"steelblue",
"tan",
"teal",
"thistle",
"tomato",
"turquoise",
"violet",
"wheat",
"white",
"whitesmoke",
"yellow",
"yellowgreen",
"currentcolor",
"transparent",
"inherit",
"initial",
"unset",
}
#[cfg(test)]
mod tests {
use super::*;
fn split_color_args_model(args_str: &str) -> Option<ColorArgs<'_>> {
let args_str = trim_css(args_str);
if let Some((head, alpha)) = args_str.split_once('/') {
let mut channels = head
.split(|c: char| c == ',' || c.is_ascii_whitespace())
.filter(|s| !s.is_empty());
let c1 = channels.next()?;
let c2 = channels.next()?;
let c3 = channels.next()?;
if channels.next().is_some() {
return None;
}
Some((c1, c2, c3, Some(trim_css(alpha))))
} else if args_str.contains(',') {
let mut parts = args_str.split(',').map(trim_css);
let c1 = parts.next()?;
let c2 = parts.next()?;
let c3 = parts.next()?;
let alpha = parts.next();
if parts.next().is_some() {
return None;
}
Some((c1, c2, c3, alpha))
} else {
let mut channels = args_str
.split(|c: char| c.is_ascii_whitespace())
.filter(|s| !s.is_empty());
let c1 = channels.next()?;
let c2 = channels.next()?;
let c3 = channels.next()?;
if channels.next().is_some() {
return None;
}
Some((c1, c2, c3, None))
}
}
#[test]
fn color_args_split_as_the_model_splits_them() {
let alphabet = [
"", ",", "/", " ", "\t", "\n", "\r", "\u{c}", "\u{b}", " ", "1", "50%", "0.5", "none",
"é", "\u{a0}", "a\\ ", "deg",
];
let mut graded = 0;
let mut index = [0usize; 5];
loop {
let args: String = index.iter().map(|&i| alphabet[i]).collect();
let ours = split_color_args(&args);
let model = split_color_args_model(&args);
let spans = |parts: Option<ColorArgs<'_>>| {
parts.map(|(c1, c2, c3, alpha)| {
let at = |s: &str| (s.as_ptr() as usize - args.as_ptr() as usize, s.len());
(at(c1), at(c2), at(c3), alpha.map(at))
})
};
assert_eq!(spans(ours), spans(model), "{args:?}");
graded += 1;
let mut digit = 0;
loop {
if digit == index.len() {
assert!(graded > 1_000_000, "{graded}");
return;
}
index[digit] += 1;
if index[digit] < alphabet.len() {
break;
}
index[digit] = 0;
digit += 1;
}
}
}
#[test]
fn hash_classification_requires_hex_digits() {
assert!(matches!(parse_color("#ABC"), Some(Color::Hex)));
assert!(matches!(parse_color("#AABBCCDD"), Some(Color::Hex)));
assert!(parse_color("#ZZZ").is_none());
assert!(parse_color("#12G").is_none());
}
}