use a11y_dom::{ancestors, Node};
const VALID: &[&str] = &[
"alert",
"alertdialog",
"application",
"article",
"banner",
"blockquote",
"button",
"caption",
"cell",
"checkbox",
"code",
"columnheader",
"combobox",
"complementary",
"contentinfo",
"definition",
"deletion",
"dialog",
"directory",
"document",
"emphasis",
"feed",
"figure",
"form",
"generic",
"grid",
"gridcell",
"group",
"heading",
"img",
"insertion",
"link",
"list",
"listbox",
"listitem",
"log",
"main",
"mark",
"marquee",
"math",
"menu",
"menubar",
"menuitem",
"menuitemcheckbox",
"menuitemradio",
"meter",
"navigation",
"none",
"note",
"option",
"paragraph",
"presentation",
"progressbar",
"radio",
"radiogroup",
"region",
"row",
"rowgroup",
"rowheader",
"scrollbar",
"search",
"searchbox",
"separator",
"slider",
"spinbutton",
"status",
"strong",
"subscript",
"superscript",
"switch",
"tab",
"table",
"tablist",
"tabpanel",
"term",
"textbox",
"time",
"timer",
"toolbar",
"tooltip",
"tree",
"treegrid",
"treeitem",
];
const NAME_FROM_CONTENT: &[&str] = &[
"button",
"cell",
"checkbox",
"columnheader",
"gridcell",
"heading",
"link",
"menuitem",
"menuitemcheckbox",
"menuitemradio",
"option",
"radio",
"row",
"rowheader",
"sectionhead",
"switch",
"tab",
"tooltip",
"treeitem",
];
const NAME_PROHIBITED: &[&str] = &[
"caption",
"code",
"deletion",
"emphasis",
"generic",
"insertion",
"paragraph",
"presentation",
"none",
"strong",
"subscript",
"superscript",
"term",
"time",
];
pub fn allows_name_from_content(role: &str) -> bool {
NAME_FROM_CONTENT.contains(&role)
}
pub fn name_is_prohibited(role: &str) -> bool {
NAME_PROHIBITED.contains(&role)
}
pub fn role<'a, N: Node<'a>>(node: N) -> Option<&'static str> {
if let Some(explicit) = node.attr("role") {
for token in explicit.split_whitespace() {
if let Some(known) = VALID.iter().find(|v| **v == token) {
return Some(known);
}
}
}
implicit(node)
}
pub fn implicit<'a, N: Node<'a>>(node: N) -> Option<&'static str> {
let tag = node.local_name();
Some(match tag {
"a" | "area" => {
if node.has_attr("href") {
"link"
} else {
"generic"
}
}
"article" => "article",
"aside" => "complementary",
"blockquote" => "blockquote",
"button" => "button",
"caption" => "caption",
"code" => "code",
"datalist" => "listbox",
"dd" => "definition",
"del" => "deletion",
"details" => "group",
"dfn" => "term",
"dialog" => "dialog",
"dt" => "term",
"em" => "emphasis",
"fieldset" => "group",
"figure" => "figure",
"form" => "form",
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => "heading",
"hr" => "separator",
"html" => "document",
"img" => match node.attr("alt") {
Some("") => "presentation",
_ => "img",
},
"input" => return input_role(node),
"ins" => "insertion",
"li" => "listitem",
"main" => "main",
"mark" => "mark",
"menu" | "ul" | "ol" => "list",
"meter" => "meter",
"nav" => "navigation",
"optgroup" => "group",
"option" => "option",
"output" => "status",
"p" => "paragraph",
"progress" => "progressbar",
"search" => "search",
"select" => {
if node.has_attr("multiple")
|| node
.attr("size")
.and_then(|s| s.trim().parse::<u32>().ok())
.is_some_and(|s| s > 1)
{
"listbox"
} else {
"combobox"
}
}
"strong" => "strong",
"sub" => "subscript",
"summary" => "button",
"sup" => "superscript",
"table" => "table",
"tbody" | "tfoot" | "thead" => "rowgroup",
"td" => "cell",
"textarea" => "textbox",
"th" => {
match node.attr("scope") {
Some("row") | Some("rowgroup") => "rowheader",
_ => "columnheader",
}
}
"time" => "time",
"tr" => "row",
"header" => {
if in_sectioning_content(node) {
"generic"
} else {
"banner"
}
}
"footer" => {
if in_sectioning_content(node) {
"generic"
} else {
"contentinfo"
}
}
"section" => {
if node.has_attr("aria-label") || node.has_attr("aria-labelledby") {
"region"
} else {
"generic"
}
}
"div" | "span" => "generic",
_ => return None,
})
}
fn in_sectioning_content<'a, N: Node<'a>>(node: N) -> bool {
ancestors(node).any(|a| matches!(a.local_name(), "article" | "aside" | "nav" | "section"))
}
fn input_role<'a, N: Node<'a>>(node: N) -> Option<&'static str> {
let ty = node
.attr("type")
.map(|t| t.trim().to_ascii_lowercase())
.unwrap_or_else(|| "text".into());
Some(match ty.as_str() {
"button" | "image" | "reset" | "submit" => "button",
"checkbox" => "checkbox",
"email" | "tel" | "text" | "url" => {
if node.has_attr("list") {
"combobox"
} else {
"textbox"
}
}
"number" => "spinbutton",
"radio" => "radio",
"range" => "slider",
"search" => {
if node.has_attr("list") {
"combobox"
} else {
"searchbox"
}
}
_ => return None,
})
}