| 756fe44a Andy McCall ↶ |
1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
use std::collections::HashMap; |
| 6 |
|
| 7 |
fn css() -> String { |
| 8 |
std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/../../templates/style.css")).unwrap() |
| 9 |
} |
| 10 |
|
| 11 |
|
| 12 |
fn theme(css: &str, selector: &str) -> HashMap<String, String> { |
| 13 |
let start = css.find(selector).unwrap_or_else(|| panic!("{selector} missing")); |
| 14 |
let body = &css[start..start + css[start..].find('}').unwrap()]; |
| 15 |
let mut out = HashMap::new(); |
| 16 |
for part in body.split("--").skip(1) { |
| 17 |
let Some((name, rest)) = part.split_once(':') else { continue }; |
| 18 |
let value = rest.trim_start(); |
| 19 |
let valid = !name.is_empty() && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-'); |
| 20 |
if valid && value.len() >= 7 && value.starts_with('#') && value[1..7].chars().all(|c| c.is_ascii_hexdigit()) { |
| 21 |
out.insert(name.to_string(), value[..7].to_lowercase()); |
| 22 |
} |
| 23 |
} |
| 24 |
out |
| 25 |
} |
| 26 |
|
| 27 |
fn luminance(hex: &str) -> f64 { |
| 28 |
let c = |i: usize| { |
| 29 |
let x = u8::from_str_radix(&hex[i..i + 2], 16).unwrap() as f64 / 255.0; |
| 30 |
if x <= 0.03928 { |
| 31 |
x / 12.92 |
| 32 |
} else { |
| 33 |
((x + 0.055) / 1.055).powf(2.4) |
| 34 |
} |
| 35 |
}; |
| 36 |
0.2126 * c(1) + 0.7152 * c(3) + 0.0722 * c(5) |
| 37 |
} |
| 38 |
|
| 39 |
fn ratio(a: &str, b: &str) -> f64 { |
| 40 |
let (x, y) = (luminance(a), luminance(b)); |
| 41 |
(x.max(y) + 0.05) / (x.min(y) + 0.05) |
| 42 |
} |
| 43 |
|
| 44 |
#[test] |
| 45 |
fn text_meets_aa_in_both_themes() { |
| 46 |
let css = css(); |
| 47 |
let mut failures = Vec::new(); |
| 48 |
for (name, sel) in [("dark", ":root {"), ("light", ":root[data-theme=\"light\"] {")] { |
| 49 |
let t = theme(&css, sel); |
| 50 |
let mut text: Vec<&str> = vec!["ink", "mute", "acc", "ok", "bad", "warn"]; |
| 51 |
text.extend(t.keys().filter(|k| k.starts_with("sy-")).map(String::as_str)); |
| 52 |
for fg in text { |
| 53 |
for bg in ["bg", "panel", "soft"] { |
| 54 |
let r = ratio(&t[fg], &t[bg]); |
| 55 |
if r < 4.5 { |
| 56 |
failures.push(format!("{name}: --{fg} on --{bg} is {r:.2}:1")); |
| 57 |
} |
| 58 |
} |
| 59 |
} |
| 60 |
for (fg, bg) in [("band-ink", "band"), ("add-ink", "add"), ("del-ink", "del"), ("now-ink", "now")] { |
| 61 |
let r = ratio(&t[fg], &t[bg]); |
| 62 |
if r < 4.5 { |
| 63 |
failures.push(format!("{name}: --{fg} on --{bg} is {r:.2}:1")); |
| 64 |
} |
| 65 |
} |
| 66 |
} |
| 67 |
assert!(failures.is_empty(), "below WCAG AA:\n{}", failures.join("\n")); |
| 68 |
} |
| 69 |
|
| 70 |
#[test] |
| 71 |
fn text_does_not_use_dim() { |
| 72 |
let css = css(); |
| 73 |
let offenders: Vec<&str> = css.lines().filter(|l| l.contains("color: var(--dim)") && !l.contains("border")).collect(); |
| 74 |
assert!(offenders.is_empty(), "--dim is too faint for text:\n{}", offenders.join("\n")); |
| 75 |
} |