use a11y_dom::{elements, Node, NodeId, Semantics, Tier};
use a11y_report::{Finding, Location, Severity};
use crate::registry::{Meta, SemanticsRule};
fn at(id: NodeId) -> Location {
Location::node(id.to_string())
}
fn named<'n, D: Semantics>(doc: &'n D, n: D::N<'n>) -> bool {
doc.accessible_name(n).is_some_and(|s| !s.trim().is_empty())
}
fn link_names<D: Semantics>(doc: &D, out: &mut Vec<Finding>) {
for n in elements(doc) {
if !n.is_element("a") || !n.has_attr("href") || doc.is_ignored(n) {
continue;
}
if !named(doc, n) {
out.push(
Finding::fail(
"links/name-missing",
"Der Link hat keinen zugänglichen Namen.",
)
.with_severity(Severity::Critical)
.with_wcag(["2.4.4", "4.1.2"])
.at(at(n.id())),
);
}
}
}
fn button_names<D: Semantics>(doc: &D, out: &mut Vec<Finding>) {
for n in elements(doc) {
let ist_button = n.is_element("button") || doc.role(n).as_deref() == Some("button");
if !ist_button || doc.is_ignored(n) {
continue;
}
if !named(doc, n) {
out.push(
Finding::fail(
"buttons/name-missing",
"Der Button hat keinen zugänglichen Namen.",
)
.with_severity(Severity::Critical)
.with_wcag(["4.1.2"])
.at(at(n.id())),
);
}
}
}
fn svg_names<D: Semantics>(doc: &D, out: &mut Vec<Finding>) {
for n in elements(doc) {
if !n.is_element("svg") || doc.is_ignored(n) {
continue;
}
if matches!(n.attr("role"), Some("presentation") | Some("none"))
|| n.attr("aria-hidden") == Some("true")
{
continue;
}
if !named(doc, n) {
out.push(
Finding::fail(
"svg/name-missing",
"Das SVG hat keinen zugänglichen Namen und ist nicht als dekorativ ausgezeichnet.",
)
.with_severity(Severity::High)
.with_wcag(["1.1.1"])
.at(at(n.id())),
);
}
}
}
fn ambiguous_link_names<D: Semantics>(doc: &D, out: &mut Vec<Finding>) {
use std::collections::HashMap;
let mut nach_name: HashMap<String, Vec<(NodeId, String)>> = HashMap::new();
for n in elements(doc) {
if !n.is_element("a") || doc.is_ignored(n) {
continue;
}
let (Some(name), Some(href)) = (doc.accessible_name(n), n.attr("href")) else {
continue;
};
let key = name.trim().to_lowercase();
if key.is_empty() {
continue;
}
nach_name
.entry(key)
.or_default()
.push((n.id(), href.to_string()));
}
for (name, treffer) in nach_name {
if treffer.len() < 2 {
continue;
}
let ziele: std::collections::HashSet<&str> =
treffer.iter().map(|(_, h)| h.as_str()).collect();
if ziele.len() < 2 {
continue; }
for (id, _) in &treffer {
out.push(
Finding::review(
"links/ambiguous-name",
format!("Mehrere Links heißen \"{name}\", zeigen aber auf verschiedene Ziele."),
)
.with_severity(Severity::Medium)
.with_wcag(["2.4.4"])
.at(at(*id)),
);
}
}
}
pub const METAS: &[Meta] = &[
Meta {
ids: &["links/name-missing"],
tier: Tier::Semantics,
wcag: &["2.4.4", "4.1.2"],
severity: Severity::Critical,
help: "Jeder Link braucht einen Namen, der sein Ziel beschreibt.",
},
Meta {
ids: &["buttons/name-missing"],
tier: Tier::Semantics,
wcag: &["4.1.2"],
severity: Severity::Critical,
help: "Jeder Button braucht einen Namen, der seine Wirkung beschreibt.",
},
Meta {
ids: &["svg/name-missing"],
tier: Tier::Semantics,
wcag: &["1.1.1"],
severity: Severity::High,
help: "Informative SVGs brauchen einen Namen, dekorative role=\"presentation\".",
},
Meta {
ids: &["links/ambiguous-name"],
tier: Tier::Semantics,
wcag: &["2.4.4"],
severity: Severity::Medium,
help: "Gleich benannte Links sollten auf dasselbe Ziel zeigen.",
},
];
fn funktionen<D: Semantics>() -> [fn(&D, &mut Vec<Finding>); 4] {
[link_names, button_names, svg_names, ambiguous_link_names]
}
pub fn rules<D: Semantics>() -> Vec<SemanticsRule<D>> {
METAS
.iter()
.zip(funktionen::<D>())
.map(|(meta, run)| SemanticsRule { meta: *meta, run })
.collect()
}