use std::sync::OnceLock;
use crate::taxonomy::rules::RULES;
pub const WCAG_AA_TOTAL: usize = 50;
const WCAG_22_ONLY_CRITERIA: &[&str] = &[
"2.4.11", "2.4.12", "2.4.13", "2.5.7", "2.5.8", "3.2.6", "3.3.7", "3.3.8", "3.3.9",
];
fn wcag_id_order(id: &str) -> Vec<u32> {
id.split('.').filter_map(|p| p.parse().ok()).collect()
}
pub fn automated_criteria() -> &'static [(&'static str, &'static str)] {
static CACHE: OnceLock<Vec<(&'static str, &'static str)>> = OnceLock::new();
CACHE.get_or_init(|| {
let mut out: Vec<(&'static str, &'static str)> = Vec::new();
for rule in RULES {
let (Some(ext_ref), Some(level)) = (rule.external_ref, rule.external_level) else {
continue;
};
let Some(id) = ext_ref.strip_prefix("WCAG ") else {
continue;
};
if !out.iter().any(|(existing, _)| *existing == id) {
out.push((id, level));
}
}
out.sort_by_key(|a| wcag_id_order(a.0));
out
})
}
const MANUAL_REVIEW_CRITERIA_RAW: &[(&str, &str, &str)] = &[
("1.2.1", "A", "Audio-only and Video-only (Prerecorded)"),
("1.2.2", "A", "Captions (Prerecorded)"),
("1.2.3", "A", "Audio Description or Media Alternative"),
("1.2.5", "AA", "Audio Description (Prerecorded)"),
("1.4.2", "A", "Audio Control"),
("1.4.5", "AA", "Images of Text"),
("2.1.4", "A", "Character Key Shortcuts"),
("2.2.2", "A", "Pause, Stop, Hide"),
("2.3.1", "A", "Three Flashes or Below Threshold"),
("2.5.1", "A", "Pointer Gestures"),
("2.5.2", "A", "Pointer Cancellation"),
("2.5.4", "A", "Motion Actuation"),
("3.2.3", "AA", "Consistent Navigation"),
("3.2.4", "AA", "Consistent Identification"),
("3.3.3", "AA", "Error Suggestion"),
("3.3.4", "AA", "Error Prevention (Legal, Financial, Data)"),
];
pub fn manual_review_criteria() -> &'static [(&'static str, &'static str, &'static str)] {
static CACHE: OnceLock<Vec<(&'static str, &'static str, &'static str)>> = OnceLock::new();
CACHE.get_or_init(|| {
let automated = automated_criteria();
MANUAL_REVIEW_CRITERIA_RAW
.iter()
.filter(|(id, _, _)| !automated.iter().any(|(aid, _)| aid == id))
.copied()
.collect()
})
}
pub fn coverage_stats() -> (usize, usize) {
let aa_count = automated_criteria()
.iter()
.filter(|(id, l)| (*l == "A" || *l == "AA") && !WCAG_22_ONLY_CRITERIA.contains(id))
.count();
(aa_count, WCAG_AA_TOTAL)
}
pub fn is_wcag22_only(id: &str) -> bool {
WCAG_22_ONLY_CRITERIA.contains(&id)
}
pub fn manual_review_criterion_name_de(id: &str, fallback_en: &'static str) -> &'static str {
crate::wcag::en301549::EN301549_WEB_CLAUSES
.iter()
.find(|c| c.wcag == id)
.map(|c| c.title_de)
.unwrap_or(fallback_en)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_three_appendix_groups_partition_the_automated_criteria() {
let (scoped, _) = coverage_stats();
let aaa = automated_criteria()
.iter()
.filter(|(_, level)| *level == "AAA")
.count();
let wcag22_ab = automated_criteria()
.iter()
.filter(|(id, level)| *level != "AAA" && is_wcag22_only(id))
.count();
assert_eq!(
scoped + wcag22_ab + aaa,
automated_criteria().len(),
"scoped {scoped} + WCAG 2.2 A/AA {wcag22_ab} + AAA {aaa} must equal the {} listed",
automated_criteria().len(),
);
}
#[test]
fn manual_review_criterion_name_de_has_a_real_translation_for_every_current_entry() {
for (id, _level, name_en) in manual_review_criteria() {
let name_de = manual_review_criterion_name_de(id, name_en);
assert_ne!(
name_de, *name_en,
"expected a German translation for WCAG {id}, got the English fallback"
);
}
}
#[test]
fn manual_review_criterion_name_de_matches_known_translation() {
assert_eq!(
manual_review_criterion_name_de("1.2.1", "Audio-only and Video-only (Prerecorded)"),
"Nur Audio oder nur Video (aufgezeichnet)"
);
}
#[test]
fn manual_review_criterion_name_de_falls_back_for_unknown_id() {
assert_eq!(
manual_review_criterion_name_de("9.9.9", "Unknown Criterion"),
"Unknown Criterion"
);
}
}