use std::collections::HashSet;
use a11y_dom::{ancestors, descendants, Node, NodeId, NodeKind};
use crate::index::IdIndex;
use crate::role::{allows_name_from_content, name_is_prohibited, role};
#[derive(Clone, Copy, PartialEq, Eq)]
enum Ziel {
Name,
Beschreibung,
}
struct Ctx<'i, 'a, N: Node<'a>> {
ids: &'i IdIndex<'a, N>,
besucht: HashSet<NodeId>,
ziel: Ziel,
}
pub fn name<'a, N: Node<'a>>(node: N, ids: &IdIndex<'a, N>) -> Option<String> {
let mut ctx = Ctx {
ids,
besucht: HashSet::new(),
ziel: Ziel::Name,
};
let s = flatten(&berechne(&mut ctx, node, false, false));
(!s.is_empty()).then_some(s)
}
pub fn description<'a, N: Node<'a>>(node: N, ids: &IdIndex<'a, N>) -> Option<String> {
if let Some(v) = node.attr("aria-describedby") {
let mut ctx = Ctx {
ids,
besucht: HashSet::new(),
ziel: Ziel::Beschreibung,
};
ctx.besucht.insert(node.id());
let mut teile = Vec::new();
for id in v.split_whitespace() {
if let Some(ziel) = ids.get(id) {
teile.push(berechne(&mut ctx, ziel, true, true));
}
}
let s = flatten(&teile.join(" "));
if !s.is_empty() {
return Some(s);
}
}
let title = node.attr("title")?.trim();
if title.is_empty() {
return None;
}
let name_kommt_woanders_her = node.has_attr("aria-label")
|| node.has_attr("aria-labelledby")
|| natives_label(node, ids).is_some();
name_kommt_woanders_her.then(|| title.to_string())
}
fn berechne<'a, N: Node<'a>>(
ctx: &mut Ctx<'_, 'a, N>,
node: N,
rekursion: bool,
via_verweis: bool,
) -> String {
if !ctx.besucht.insert(node.id()) {
return String::new();
}
if node.kind() == NodeKind::Text {
return node.text().to_string();
}
let rolle = role(node);
if !via_verweis && ist_versteckt(node) {
return String::new();
}
if ctx.ziel == Ziel::Name && !via_verweis {
if let Some(v) = node.attr("aria-labelledby") {
let mut teile = Vec::new();
for id in v.split_whitespace() {
if let Some(ziel) = ctx.ids.get(id) {
teile.push(berechne(ctx, ziel, true, true));
}
}
let s = teile.join(" ");
if !s.trim().is_empty() {
return s;
}
}
}
let verboten = rolle.is_some_and(name_is_prohibited);
if !verboten && ctx.ziel == Ziel::Name {
if let Some(label) = node.attr("aria-label") {
let label = label.trim();
if !label.is_empty() && !(rekursion && ist_eingebettetes_steuerelement(rolle)) {
return label.to_string();
}
}
}
if !verboten {
if let Some(s) = natives_label(node, ctx.ids) {
if !s.trim().is_empty() {
return s;
}
}
}
if rekursion {
if let Some(wert) = eingebetteter_wert(node, rolle) {
return wert;
}
}
let aus_inhalt = rolle.is_some_and(allows_name_from_content) || via_verweis || rekursion;
if aus_inhalt {
let mut teile: Vec<String> = Vec::new();
for kind in node.children() {
let s = berechne(ctx, kind, true, false);
if !s.is_empty() {
teile.push(s);
}
}
let s = teile.join(" ");
if !s.trim().is_empty() {
return s;
}
}
if let Some(t) = node.attr("title") {
if !t.trim().is_empty() {
return t.to_string();
}
}
String::new()
}
fn ist_versteckt<'a, N: Node<'a>>(node: N) -> bool {
if node.attr("aria-hidden") == Some("true") || node.has_attr("hidden") {
return true;
}
ancestors(node).any(|a| a.attr("aria-hidden") == Some("true") || a.has_attr("hidden"))
}
fn ist_eingebettetes_steuerelement(rolle: Option<&str>) -> bool {
matches!(
rolle,
Some(
"textbox"
| "searchbox"
| "combobox"
| "listbox"
| "slider"
| "spinbutton"
| "progressbar"
| "scrollbar"
| "checkbox"
| "radio"
| "switch"
)
)
}
fn eingebetteter_wert<'a, N: Node<'a>>(node: N, rolle: Option<&str>) -> Option<String> {
match rolle? {
"textbox" | "searchbox" => node
.attr("value")
.map(str::to_string)
.or_else(|| Some(subtree_plain(node))),
"combobox" | "listbox" => {
let gewaehlt = descendants(node)
.filter(|d| d.is_element("option"))
.find(|d| d.has_attr("selected"))
.or_else(|| descendants(node).find(|d| d.is_element("option")))?;
Some(subtree_plain(gewaehlt))
}
"slider" | "spinbutton" | "progressbar" | "scrollbar" => node
.attr("aria-valuetext")
.or_else(|| node.attr("aria-valuenow"))
.or_else(|| node.attr("value"))
.map(str::to_string),
_ => None,
}
}
fn natives_label<'a, N: Node<'a>>(node: N, ids: &IdIndex<'a, N>) -> Option<String> {
let tag = node.local_name();
match tag {
"img" | "area" => node.attr("alt").map(str::to_string),
"input" => {
let ty = node
.attr("type")
.map(|t| t.trim().to_ascii_lowercase())
.unwrap_or_else(|| "text".into());
match ty.as_str() {
"button" => node.attr("value").map(str::to_string),
"submit" => Some(node.attr("value").unwrap_or("Submit").to_string()),
"reset" => Some(node.attr("value").unwrap_or("Reset").to_string()),
"image" => node
.attr("alt")
.map(str::to_string)
.or_else(|| label_elemente(node, ids))
.or_else(|| Some("Submit Query".to_string())),
_ => label_elemente(node, ids).or_else(|| {
node.attr("placeholder").map(str::to_string)
}),
}
}
"select" | "textarea" | "meter" | "progress" | "output" => label_elemente(node, ids),
"fieldset" => erstes_kind_mit_tag(node, "legend").map(subtree_plain),
"figure" => descendants(node)
.find(|d| d.is_element("figcaption"))
.map(subtree_plain),
"table" => erstes_kind_mit_tag(node, "caption").map(subtree_plain),
"svg" => erstes_kind_mit_tag(node, "title").map(subtree_plain),
"iframe" => node.attr("title").map(str::to_string),
_ => None,
}
}
fn erstes_kind_mit_tag<'a, N: Node<'a>>(node: N, tag: &str) -> Option<N> {
node.children().find(|c| c.is_element(tag))
}
fn label_elemente<'a, N: Node<'a>>(node: N, ids: &IdIndex<'a, N>) -> Option<String> {
let mut teile: Vec<String> = Vec::new();
if let Some(id) = node.attr("id") {
for &l in ids.labels_for(id) {
let t = subtree_ohne(l, node);
if !t.trim().is_empty() {
teile.push(t);
}
}
}
if teile.is_empty() {
if let Some(l) = a11y_dom::closest(node, "label") {
let t = subtree_ohne(l, node);
if !t.trim().is_empty() {
teile.push(t);
}
}
}
(!teile.is_empty()).then(|| teile.join(" "))
}
fn subtree_ohne<'a, N: Node<'a>>(wurzel: N, aussparen: N) -> String {
let mut out = String::new();
for n in a11y_dom::self_and_descendants(wurzel) {
if n == aussparen {
continue;
}
if ancestors(n).any(|a| a == aussparen) {
continue;
}
if n.kind() == NodeKind::Text {
out.push(' ');
out.push_str(n.text());
}
}
flatten(&out)
}
fn subtree_plain<'a, N: Node<'a>>(node: N) -> String {
flatten(&a11y_dom::subtree_text(node))
}
pub(crate) fn flatten(s: &str) -> String {
s.split_whitespace().collect::<Vec<_>>().join(" ")
}