use super::registry::{NamedPredicate, PredicateRegistry};
use serde_json::Value;
use std::sync::Arc;
pub fn register_color_predicates(registry: &mut PredicateRegistry) {
registry.register(Arc::new(IsHexColorPredicate));
registry.register(Arc::new(IsRgbColorPredicate));
registry.register(Arc::new(IsHslColorPredicate));
}
struct IsHexColorPredicate;
impl NamedPredicate for IsHexColorPredicate {
fn name(&self) -> &str {
"is_hex_color"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
let allow_alpha = args
.get("allow_alpha")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let require_hash = args
.get("require_hash")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let hex = if let Some(rest) = s.strip_prefix('#') {
rest
} else if require_hash {
return false;
} else {
s
};
let valid_len = if allow_alpha {
matches!(hex.len(), 3 | 4 | 6 | 8)
} else {
matches!(hex.len(), 3 | 6)
};
if !valid_len {
return false;
}
hex.chars().all(|c| c.is_ascii_hexdigit())
}
}
struct IsRgbColorPredicate;
impl NamedPredicate for IsRgbColorPredicate {
fn name(&self) -> &str {
"is_rgb_color"
}
fn evaluate(&self, value: &Value, _args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
let (inner, has_alpha_prefix) = if let Some(rest) = s.strip_prefix("rgba(") {
(rest, true)
} else if let Some(rest) = s.strip_prefix("rgb(") {
(rest, false)
} else {
return false;
};
let inner = match inner.strip_suffix(')') {
Some(i) => i.trim(),
None => return false,
};
if let Some((rgb_part, alpha_part)) = inner.split_once('/') {
let parts: Vec<&str> = rgb_part.split_whitespace().collect();
if parts.len() != 3 {
return false;
}
let all_percent = parts.iter().all(|p| p.ends_with('%'));
let all_number = parts.iter().all(|p| !p.ends_with('%'));
if !all_percent && !all_number {
return false;
}
for p in &parts {
if !validate_rgb_component(p) {
return false;
}
}
return validate_alpha_component(alpha_part.trim());
}
if inner.contains(',') {
let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
match parts.len() {
3 => {
let all_percent = parts.iter().all(|p| p.ends_with('%'));
let all_number = parts.iter().all(|p| !p.ends_with('%'));
if !all_percent && !all_number {
return false;
}
parts.iter().all(|p| validate_rgb_component(p))
}
4 => {
let rgb = &parts[..3];
let all_percent = rgb.iter().all(|p| p.ends_with('%'));
let all_number = rgb.iter().all(|p| !p.ends_with('%'));
if !all_percent && !all_number {
return false;
}
rgb.iter().all(|p| validate_rgb_component(p))
&& validate_alpha_component(parts[3])
}
_ => false,
}
} else {
let parts: Vec<&str> = inner.split_whitespace().collect();
if parts.len() != 3 && !(has_alpha_prefix && parts.len() == 4) {
return false;
}
let rgb = &parts[..3];
let all_percent = rgb.iter().all(|p| p.ends_with('%'));
let all_number = rgb.iter().all(|p| !p.ends_with('%'));
if !all_percent && !all_number {
return false;
}
let rgb_valid = rgb.iter().all(|p| validate_rgb_component(p));
if parts.len() == 4 {
rgb_valid && validate_alpha_component(parts[3])
} else {
rgb_valid
}
}
}
}
fn validate_rgb_component(s: &str) -> bool {
if let Some(pct) = s.strip_suffix('%') {
match pct.trim().parse::<f64>() {
Ok(v) => (0.0..=100.0).contains(&v),
Err(_) => false,
}
} else {
match s.trim().parse::<f64>() {
Ok(v) => (0.0..=255.0).contains(&v) && v.fract() == 0.0,
Err(_) => false,
}
}
}
fn validate_alpha_component(s: &str) -> bool {
if let Some(pct) = s.strip_suffix('%') {
match pct.trim().parse::<f64>() {
Ok(v) => (0.0..=100.0).contains(&v),
Err(_) => false,
}
} else {
match s.trim().parse::<f64>() {
Ok(v) => (0.0..=1.0).contains(&v),
Err(_) => false,
}
}
}
struct IsHslColorPredicate;
impl NamedPredicate for IsHslColorPredicate {
fn name(&self) -> &str {
"is_hsl_color"
}
fn evaluate(&self, value: &Value, _args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
let (inner, has_alpha_prefix) = if let Some(rest) = s.strip_prefix("hsla(") {
(rest, true)
} else if let Some(rest) = s.strip_prefix("hsl(") {
(rest, false)
} else {
return false;
};
let inner = match inner.strip_suffix(')') {
Some(i) => i.trim(),
None => return false,
};
if let Some((hsl_part, alpha_part)) = inner.split_once('/') {
let parts: Vec<&str> = hsl_part.split_whitespace().collect();
if parts.len() != 3 {
return false;
}
return validate_hue(parts[0])
&& validate_percentage_component(parts[1])
&& validate_percentage_component(parts[2])
&& validate_alpha_component(alpha_part.trim());
}
if inner.contains(',') {
let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
match parts.len() {
3 => {
validate_hue(parts[0])
&& validate_percentage_component(parts[1])
&& validate_percentage_component(parts[2])
}
4 => {
validate_hue(parts[0])
&& validate_percentage_component(parts[1])
&& validate_percentage_component(parts[2])
&& validate_alpha_component(parts[3])
}
_ => false,
}
} else {
let parts: Vec<&str> = inner.split_whitespace().collect();
if parts.len() != 3 && !(has_alpha_prefix && parts.len() == 4) {
return false;
}
let hsl_valid = validate_hue(parts[0])
&& validate_percentage_component(parts[1])
&& validate_percentage_component(parts[2]);
if parts.len() == 4 {
hsl_valid && validate_alpha_component(parts[3])
} else {
hsl_valid
}
}
}
}
fn validate_hue(s: &str) -> bool {
let s = s.strip_suffix("deg").unwrap_or(s);
match s.trim().parse::<f64>() {
Ok(v) => (0.0..=360.0).contains(&v),
Err(_) => false,
}
}
fn validate_percentage_component(s: &str) -> bool {
if let Some(pct) = s.strip_suffix('%') {
match pct.trim().parse::<f64>() {
Ok(v) => (0.0..=100.0).contains(&v),
Err(_) => false,
}
} else {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn eval(pred: &dyn NamedPredicate, value: &str, args: Value) -> bool {
pred.evaluate(&json!(value), &args)
}
#[test]
fn test_hex_color_basic() {
let p = IsHexColorPredicate;
assert!(eval(&p, "#fff", json!({})));
assert!(eval(&p, "#F0A", json!({})));
assert!(eval(&p, "#FF00AA", json!({})));
assert!(eval(&p, "#ff00aa", json!({})));
assert!(eval(&p, "#000000", json!({})));
assert!(eval(&p, "#F0AF", json!({})));
assert!(eval(&p, "#FF00AAFF", json!({})));
}
#[test]
fn test_hex_color_no_alpha() {
let p = IsHexColorPredicate;
assert!(eval(&p, "#FFF", json!({"allow_alpha": false})));
assert!(eval(&p, "#FF00AA", json!({"allow_alpha": false})));
assert!(!eval(&p, "#FFFF", json!({"allow_alpha": false})));
assert!(!eval(&p, "#FF00AAFF", json!({"allow_alpha": false})));
}
#[test]
fn test_hex_color_no_hash() {
let p = IsHexColorPredicate;
assert!(!eval(&p, "FF00AA", json!({})));
assert!(eval(&p, "FF00AA", json!({"require_hash": false})));
assert!(eval(&p, "#FF00AA", json!({"require_hash": false})));
}
#[test]
fn test_hex_color_invalid() {
let p = IsHexColorPredicate;
assert!(!eval(&p, "#GGG", json!({})));
assert!(!eval(&p, "#12345", json!({})));
assert!(!eval(&p, "red", json!({})));
assert!(!eval(&p, "", json!({})));
assert!(!eval(&p, "#", json!({})));
}
#[test]
fn test_rgb_comma_separated() {
let p = IsRgbColorPredicate;
assert!(eval(&p, "rgb(255, 0, 170)", json!({})));
assert!(eval(&p, "rgb(0, 0, 0)", json!({})));
assert!(eval(&p, "rgb(255, 255, 255)", json!({})));
}
#[test]
fn test_rgb_space_separated() {
let p = IsRgbColorPredicate;
assert!(eval(&p, "rgb(255 0 170)", json!({})));
}
#[test]
fn test_rgb_percentages() {
let p = IsRgbColorPredicate;
assert!(eval(&p, "rgb(100%, 0%, 50%)", json!({})));
assert!(eval(&p, "rgb(100% 0% 50%)", json!({})));
}
#[test]
fn test_rgba_comma() {
let p = IsRgbColorPredicate;
assert!(eval(&p, "rgba(255, 0, 170, 0.5)", json!({})));
assert!(eval(&p, "rgba(255, 0, 170, 1)", json!({})));
assert!(eval(&p, "rgba(255, 0, 170, 50%)", json!({})));
}
#[test]
fn test_rgb_slash_alpha() {
let p = IsRgbColorPredicate;
assert!(eval(&p, "rgb(255 0 170 / 0.5)", json!({})));
assert!(eval(&p, "rgb(255 0 170 / 50%)", json!({})));
}
#[test]
fn test_rgb_invalid() {
let p = IsRgbColorPredicate;
assert!(!eval(&p, "rgb(256, 0, 0)", json!({})));
assert!(!eval(&p, "rgb(-1, 0, 0)", json!({})));
assert!(!eval(&p, "rgb(0, 0, 101%)", json!({})));
assert!(!eval(&p, "rgb(255, 0)", json!({})));
assert!(!eval(&p, "rgb()", json!({})));
assert!(!eval(&p, "hsl(0, 0%, 0%)", json!({})));
assert!(!eval(&p, "rgb(255, 0%, 0)", json!({})));
assert!(!eval(&p, "rgba(255, 0, 0, 1.5)", json!({})));
}
#[test]
fn test_hsl_comma_separated() {
let p = IsHslColorPredicate;
assert!(eval(&p, "hsl(360, 100%, 50%)", json!({})));
assert!(eval(&p, "hsl(0, 0%, 0%)", json!({})));
assert!(eval(&p, "hsl(180, 50%, 75%)", json!({})));
}
#[test]
fn test_hsl_space_separated() {
let p = IsHslColorPredicate;
assert!(eval(&p, "hsl(360 100% 50%)", json!({})));
assert!(eval(&p, "hsl(0 0% 0%)", json!({})));
}
#[test]
fn test_hsla_comma() {
let p = IsHslColorPredicate;
assert!(eval(&p, "hsla(360, 100%, 50%, 0.5)", json!({})));
assert!(eval(&p, "hsla(360, 100%, 50%, 50%)", json!({})));
}
#[test]
fn test_hsl_slash_alpha() {
let p = IsHslColorPredicate;
assert!(eval(&p, "hsl(360 100% 50% / 0.5)", json!({})));
assert!(eval(&p, "hsl(360 100% 50% / 50%)", json!({})));
}
#[test]
fn test_hsl_deg_suffix() {
let p = IsHslColorPredicate;
assert!(eval(&p, "hsl(360deg, 100%, 50%)", json!({})));
assert!(eval(&p, "hsl(180deg 50% 75%)", json!({})));
}
#[test]
fn test_hsl_invalid() {
let p = IsHslColorPredicate;
assert!(!eval(&p, "hsl(361, 100%, 50%)", json!({})));
assert!(!eval(&p, "hsl(-1, 100%, 50%)", json!({})));
assert!(!eval(&p, "hsl(360, 100, 50)", json!({})));
assert!(!eval(&p, "hsl(360, 101%, 50%)", json!({})));
assert!(!eval(&p, "hsl()", json!({})));
assert!(!eval(&p, "rgb(255, 0, 0)", json!({})));
assert!(!eval(&p, "hsla(360, 100%, 50%, 1.5)", json!({})));
}
}