///|
/// HTML-AAM Role Mapping
/// Based on HTML Accessibility API Mappings 1.0
/// https://www.w3.org/TR/html-aam-1.0/
// =============================================================================
// Role Mapping Context
// =============================================================================
///|
/// Context for determining contextual roles
pub(all) struct RoleMappingContext {
// Parent element tag (for contextual roles like header/footer)
parent_tag : String?
// Whether element has accessible name
has_accessible_name : Bool
// Whether element is in a sectioning content
in_sectioning_content : Bool
}
///|
pub fn RoleMappingContext::default() -> RoleMappingContext {
{
parent_tag: Option::None,
has_accessible_name: false,
in_sectioning_content: false,
}
}
// =============================================================================
// Main Role Mapping Function
// =============================================================================
///|
/// Get the computed role for an HTML element
/// Follows HTML-AAM specification for role mapping
pub fn compute_role(
element : @html.Element,
context : RoleMappingContext,
) -> Role {
// 1. Check for explicit role attribute
match element.attributes.get("role") {
Some(role_str) =>
match Role::from_string(role_str.to_lower()) {
Some(explicit_role) => return explicit_role
Option::None => () // Invalid role, fall through to implicit
}
Option::None => ()
}
// 2. Compute implicit role based on element tag and context
compute_implicit_role(element, context)
}
///|
/// Compute the implicit role based on HTML element semantics
fn compute_implicit_role(
element : @html.Element,
context : RoleMappingContext,
) -> Role {
let tag = element.tag.to_lower()
match tag {
// Sectioning content
"article" => Article
"aside" => Complementary
"nav" => Navigation
"section" => if context.has_accessible_name { Region } else { Generic }
// Heading content
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => Heading
// Grouping content
"p" => Paragraph
"hr" => Separator
"pre" => Generic
"blockquote" => Blockquote
"ol" | "ul" => List
"menu" => List
"li" => ListItem
"dl" => List
"dt" => Term
"dd" => Definition
"figure" => Figure
"figcaption" => Caption
"main" => Main
"div" => Generic
"address" => Group
// Text-level semantics
"a" => if element.attributes.contains("href") { Link } else { Generic }
"em" => Emphasis
"strong" => Strong
"small" => Generic
"s" => Deletion
"cite" => Generic
"q" => Generic
"dfn" => Term
"abbr" => Generic
"ruby" => Generic
"rt" => Generic
"rp" => Generic
"data" => Generic
"time" => Time
"code" => Code
"var" => Generic
"samp" => Generic
"kbd" => Generic
"sub" => Subscript
"sup" => Superscript
"i" => Generic
"b" => Generic
"u" => Generic
"mark" => Mark
"bdi" => Generic
"bdo" => Generic
"span" => Generic
"br" => Generic
"wbr" => Generic
// Edits
"ins" => Insertion
"del" => Deletion
// Embedded content
"img" => compute_img_role(element)
"iframe" => Generic
"embed" => Generic
"object" => Generic
"video" => HtmlVideo
"audio" => HtmlAudio
"canvas" => HtmlCanvas
"map" => Generic
"area" => if element.attributes.contains("href") { Link } else { Generic }
"svg" => Generic
"math" => Math
// Tabular data
"table" => Table
"caption" => Caption
"colgroup" => Generic
"col" => Generic
"tbody" | "thead" | "tfoot" => RowGroup
"tr" => Row
"td" => Cell
"th" => compute_th_role(element)
// Forms
"form" => if context.has_accessible_name { Form } else { Generic }
"label" => HtmlLabel
"input" => compute_input_role(element)
"button" => Button
"select" => compute_select_role(element)
"datalist" => Listbox
"optgroup" => Group
"option" => Option
"textarea" => Textbox
"output" => Status
"progress" => Progressbar
"meter" => Meter
"fieldset" => Group
"legend" => Generic
// Interactive elements
"details" => Group
"summary" => HtmlSummary
"dialog" => Dialog
"hgroup" => Group
// Scripting
"script" | "noscript" | "template" => Generic
// Document structure
"html" => Document
"head" => Generic
"title" => Generic
"body" => Generic
"header" => compute_header_role(context)
"footer" => compute_footer_role(context)
"search" => Search
// Default
_ => Generic
}
}
// =============================================================================
// Contextual Role Helpers
// =============================================================================
///|
/// Compute role for
element
fn compute_img_role(element : @html.Element) -> Role {
// img with empty alt is presentational, unless it has aria-label/aria-labelledby
match element.attributes.get("alt") {
Some(alt) if alt.is_empty() =>
// Check if element has an accessible name source that overrides presentation
if element.attributes.contains("aria-label") ||
element.attributes.contains("aria-labelledby") {
Img
} else {
Presentation
}
_ => Img
}
}
///|
/// Compute role for element based on scope
fn compute_th_role(element : @html.Element) -> Role {
match element.attributes.get("scope") {
Some("row") => RowHeader
Some("rowgroup") => RowHeader
_ => ColumnHeader
}
}
///|
/// Compute role for element based on type
fn compute_input_role(element : @html.Element) -> Role {
let input_type = match element.attributes.get("type") {
Some(t) => t.to_lower()
Option::None => "text" // default type
}
match input_type {
"button" | "submit" | "reset" | "image" => Button
"checkbox" => Checkbox
"radio" => Radio
"range" => Slider
"number" => SpinButton
"search" => SearchBox
"email" | "tel" | "url" | "text" | "password" => Textbox
"hidden" => Generic
"color" => Generic
"date" | "datetime-local" | "month" | "time" | "week" => Generic
"file" => Generic
_ => Textbox
}
}
///|
/// Compute role for |