pub(crate) const NEUTRAL: &str = "#667085";
pub(crate) fn sanitize(value: &str) -> Option<&str> {
let trimmed = value.trim();
if trimmed.is_empty() {
return None;
}
match trimmed.as_bytes()[0] {
b'#' => hex(trimmed).then_some(trimmed),
_ => variable(trimmed).then_some(trimmed),
}
}
pub(crate) fn emit(value: &str) -> String {
let trimmed = value.trim();
if !variable(trimmed) {
return trimmed.to_owned();
}
let name = trimmed
.strip_prefix("var(")
.and_then(|rest| rest.strip_suffix(')'))
.expect("a variable color was checked by the contract");
format!("var({name},currentColor)")
}
fn hex(value: &str) -> bool {
let digits = &value[1..];
matches!(digits.len(), 3 | 6 | 8) && digits.bytes().all(|byte| byte.is_ascii_hexdigit())
}
fn variable(value: &str) -> bool {
let Some(name) = value
.strip_prefix("var(")
.and_then(|rest| rest.strip_suffix(')'))
else {
return false;
};
let Some(name) = name.strip_prefix("--") else {
return false;
};
!name.is_empty()
&& name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
}
#[cfg(test)]
mod tests {
use super::{NEUTRAL, emit, sanitize};
#[test]
fn accepts_the_chartkit_whitelist() {
for color in [
"#abc",
"#ABC",
"#2563eb",
"#2563eb80",
"var(--chart-basis)",
"var(--x-1)",
] {
assert_eq!(sanitize(color), Some(color), "{color}");
}
}
#[test]
fn rejects_everything_else() {
for color in [
"",
"red",
"rgb(1, 2, 3)",
"#12",
"#12345",
"#gggggg",
"var(--name, #fff)",
"var(--name, red)",
"var(name)",
"var(--)",
"url(#gradient)",
"#2563eb;fill:red",
] {
assert_eq!(sanitize(color), None, "{color}");
}
}
#[test]
fn trims_surrounding_whitespace() {
assert_eq!(sanitize(" #fff "), Some("#fff"));
}
#[test]
fn neutral_fallback_is_a_defined_gray() {
assert_eq!(NEUTRAL, "#667085");
}
#[test]
fn emitted_colors_keep_a_way_back_when_the_page_defines_no_variable() {
assert_eq!(emit("#2563eb"), "#2563eb");
assert_eq!(emit("#2563eb80"), "#2563eb80");
assert_eq!(
emit("var(--chart-basis)"),
"var(--chart-basis,currentColor)"
);
}
}