///|
/// Accessibility Object Model (AOM) Types
/// Based on WAI-ARIA 1.2 and HTML-AAM 1.0 specifications
// =============================================================================
// ARIA Roles (WAI-ARIA 1.2)
// =============================================================================
///|
/// ARIA Role categories based on WAI-ARIA specification
pub(all) enum Role {
// Document structure roles
Article
Blockquote
Caption
Cell
Code
ColumnHeader
Definition
Deletion
Directory
Document
Emphasis
Feed
Figure
Generic
Group
Heading
Img
Insertion
List
ListItem
Mark
Math
Meter
None
Note
Paragraph
Presentation
Row
RowGroup
RowHeader
Separator
Strong
Subscript
Superscript
Table
Term
Time
Toolbar
Tooltip
// Widget roles
Button
Checkbox
Combobox
GridCell
Link
Listbox
Menu
MenuBar
MenuItem
MenuItemCheckbox
MenuItemRadio
Option
Progressbar
Radio
RadioGroup
Scrollbar
SearchBox
Slider
SpinButton
Switch
Tab
TabList
TabPanel
Textbox
Tree
TreeGrid
TreeItem
// Landmark roles
Application
Banner
Complementary
ContentInfo
Form
Main
Navigation
Region
Search
// Live region roles
Alert
AlertDialog
Dialog
Log
Marquee
Status
Timer
// Window roles
Window
// HTML-specific mappings (not in ARIA but needed for HTML-AAM)
HtmlAudio
HtmlVideo
HtmlCanvas
HtmlLabel
HtmlSummary
} derive(Eq, Debug)
///|
pub fn Role::to_string(self : Role) -> String {
match self {
Article => "article"
Blockquote => "blockquote"
Caption => "caption"
Cell => "cell"
Code => "code"
ColumnHeader => "columnheader"
Definition => "definition"
Deletion => "deletion"
Directory => "directory"
Document => "document"
Emphasis => "emphasis"
Feed => "feed"
Figure => "figure"
Generic => "generic"
Group => "group"
Heading => "heading"
Img => "img"
Insertion => "insertion"
List => "list"
ListItem => "listitem"
Mark => "mark"
Math => "math"
Meter => "meter"
None => "none"
Note => "note"
Paragraph => "paragraph"
Presentation => "presentation"
Row => "row"
RowGroup => "rowgroup"
RowHeader => "rowheader"
Separator => "separator"
Strong => "strong"
Subscript => "subscript"
Superscript => "superscript"
Table => "table"
Term => "term"
Time => "time"
Toolbar => "toolbar"
Tooltip => "tooltip"
Button => "button"
Checkbox => "checkbox"
Combobox => "combobox"
GridCell => "gridcell"
Link => "link"
Listbox => "listbox"
Menu => "menu"
MenuBar => "menubar"
MenuItem => "menuitem"
MenuItemCheckbox => "menuitemcheckbox"
MenuItemRadio => "menuitemradio"
Option => "option"
Progressbar => "progressbar"
Radio => "radio"
RadioGroup => "radiogroup"
Scrollbar => "scrollbar"
SearchBox => "searchbox"
Slider => "slider"
SpinButton => "spinbutton"
Switch => "switch"
Tab => "tab"
TabList => "tablist"
TabPanel => "tabpanel"
Textbox => "textbox"
Tree => "tree"
TreeGrid => "treegrid"
TreeItem => "treeitem"
Application => "application"
Banner => "banner"
Complementary => "complementary"
ContentInfo => "contentinfo"
Form => "form"
Main => "main"
Navigation => "navigation"
Region => "region"
Search => "search"
Alert => "alert"
AlertDialog => "alertdialog"
Dialog => "dialog"
Log => "log"
Marquee => "marquee"
Status => "status"
Timer => "timer"
Window => "window"
HtmlAudio => "html-audio"
HtmlVideo => "html-video"
HtmlCanvas => "html-canvas"
HtmlLabel => "html-label"
HtmlSummary => "html-summary"
}
}
///|
pub impl Show for Role with fn output(self, logger) {
match self {
Article => logger.write_string("Article")
Blockquote => logger.write_string("Blockquote")
Caption => logger.write_string("Caption")
Cell => logger.write_string("Cell")
Code => logger.write_string("Code")
ColumnHeader => logger.write_string("ColumnHeader")
Definition => logger.write_string("Definition")
Deletion => logger.write_string("Deletion")
Directory => logger.write_string("Directory")
Document => logger.write_string("Document")
Emphasis => logger.write_string("Emphasis")
Feed => logger.write_string("Feed")
Figure => logger.write_string("Figure")
Generic => logger.write_string("Generic")
Group => logger.write_string("Group")
Heading => logger.write_string("Heading")
Img => logger.write_string("Img")
Insertion => logger.write_string("Insertion")
List => logger.write_string("List")
ListItem => logger.write_string("ListItem")
Mark => logger.write_string("Mark")
Math => logger.write_string("Math")
Meter => logger.write_string("Meter")
None => logger.write_string("None")
Note => logger.write_string("Note")
Paragraph => logger.write_string("Paragraph")
Presentation => logger.write_string("Presentation")
Row => logger.write_string("Row")
RowGroup => logger.write_string("RowGroup")
RowHeader => logger.write_string("RowHeader")
Separator => logger.write_string("Separator")
Strong => logger.write_string("Strong")
Subscript => logger.write_string("Subscript")
Superscript => logger.write_string("Superscript")
Table => logger.write_string("Table")
Term => logger.write_string("Term")
Time => logger.write_string("Time")
Toolbar => logger.write_string("Toolbar")
Tooltip => logger.write_string("Tooltip")
Button => logger.write_string("Button")
Checkbox => logger.write_string("Checkbox")
Combobox => logger.write_string("Combobox")
GridCell => logger.write_string("GridCell")
Link => logger.write_string("Link")
Listbox => logger.write_string("Listbox")
Menu => logger.write_string("Menu")
MenuBar => logger.write_string("MenuBar")
MenuItem => logger.write_string("MenuItem")
MenuItemCheckbox => logger.write_string("MenuItemCheckbox")
MenuItemRadio => logger.write_string("MenuItemRadio")
Option => logger.write_string("Option")
Progressbar => logger.write_string("Progressbar")
Radio => logger.write_string("Radio")
RadioGroup => logger.write_string("RadioGroup")
Scrollbar => logger.write_string("Scrollbar")
SearchBox => logger.write_string("SearchBox")
Slider => logger.write_string("Slider")
SpinButton => logger.write_string("SpinButton")
Switch => logger.write_string("Switch")
Tab => logger.write_string("Tab")
TabList => logger.write_string("TabList")
TabPanel => logger.write_string("TabPanel")
Textbox => logger.write_string("Textbox")
Tree => logger.write_string("Tree")
TreeGrid => logger.write_string("TreeGrid")
TreeItem => logger.write_string("TreeItem")
Application => logger.write_string("Application")
Banner => logger.write_string("Banner")
Complementary => logger.write_string("Complementary")
ContentInfo => logger.write_string("ContentInfo")
Form => logger.write_string("Form")
Main => logger.write_string("Main")
Navigation => logger.write_string("Navigation")
Region => logger.write_string("Region")
Search => logger.write_string("Search")
Alert => logger.write_string("Alert")
AlertDialog => logger.write_string("AlertDialog")
Dialog => logger.write_string("Dialog")
Log => logger.write_string("Log")
Marquee => logger.write_string("Marquee")
Status => logger.write_string("Status")
Timer => logger.write_string("Timer")
Window => logger.write_string("Window")
HtmlAudio => logger.write_string("HtmlAudio")
HtmlVideo => logger.write_string("HtmlVideo")
HtmlCanvas => logger.write_string("HtmlCanvas")
HtmlLabel => logger.write_string("HtmlLabel")
HtmlSummary => logger.write_string("HtmlSummary")
}
}
///|
pub fn Role::from_string(s : String) -> Role? {
match s {
"article" => Some(Article)
"blockquote" => Some(Blockquote)
"caption" => Some(Caption)
"cell" => Some(Cell)
"code" => Some(Code)
"columnheader" => Some(ColumnHeader)
"definition" => Some(Definition)
"deletion" => Some(Deletion)
"directory" => Some(Directory)
"document" => Some(Document)
"emphasis" => Some(Emphasis)
"feed" => Some(Feed)
"figure" => Some(Figure)
"generic" => Some(Generic)
"group" => Some(Group)
"heading" => Some(Heading)
"img" | "image" => Some(Img)
"insertion" => Some(Insertion)
"list" => Some(List)
"listitem" => Some(ListItem)
"mark" => Some(Mark)
"math" => Some(Math)
"meter" => Some(Meter)
"none" => Some(None)
"note" => Some(Note)
"paragraph" => Some(Paragraph)
"presentation" => Some(Presentation)
"row" => Some(Row)
"rowgroup" => Some(RowGroup)
"rowheader" => Some(RowHeader)
"separator" => Some(Separator)
"strong" => Some(Strong)
"subscript" => Some(Subscript)
"superscript" => Some(Superscript)
"table" => Some(Table)
"term" => Some(Term)
"time" => Some(Time)
"toolbar" => Some(Toolbar)
"tooltip" => Some(Tooltip)
"button" => Some(Button)
"checkbox" => Some(Checkbox)
"combobox" => Some(Combobox)
"gridcell" => Some(GridCell)
"link" => Some(Link)
"listbox" => Some(Listbox)
"menu" => Some(Menu)
"menubar" => Some(MenuBar)
"menuitem" => Some(MenuItem)
"menuitemcheckbox" => Some(MenuItemCheckbox)
"menuitemradio" => Some(MenuItemRadio)
"option" => Some(Option)
"progressbar" => Some(Progressbar)
"radio" => Some(Radio)
"radiogroup" => Some(RadioGroup)
"scrollbar" => Some(Scrollbar)
"searchbox" => Some(SearchBox)
"slider" => Some(Slider)
"spinbutton" => Some(SpinButton)
"switch" => Some(Switch)
"tab" => Some(Tab)
"tablist" => Some(TabList)
"tabpanel" => Some(TabPanel)
"textbox" => Some(Textbox)
"tree" => Some(Tree)
"treegrid" => Some(TreeGrid)
"treeitem" => Some(TreeItem)
"application" => Some(Application)
"banner" => Some(Banner)
"complementary" => Some(Complementary)
"contentinfo" => Some(ContentInfo)
"form" => Some(Form)
"main" => Some(Main)
"navigation" => Some(Navigation)
"region" => Some(Region)
"search" => Some(Search)
"alert" => Some(Alert)
"alertdialog" => Some(AlertDialog)
"dialog" => Some(Dialog)
"log" => Some(Log)
"marquee" => Some(Marquee)
"status" => Some(Status)
"timer" => Some(Timer)
"window" => Some(Window)
_ => Option::None
}
}
// =============================================================================
// ARIA States and Properties
// =============================================================================
///|
/// ARIA state values
pub(all) enum State {
// Boolean states
Busy
Checked
Disabled
Expanded
Grabbed
Hidden
Invalid
Pressed
Selected
// Tristate
CheckedMixed
PressedMixed
} derive(Eq, Debug)
///|
pub fn State::to_string(self : State) -> String {
match self {
Busy => "busy"
Checked => "checked"
Disabled => "disabled"
Expanded => "expanded"
Grabbed => "grabbed"
Hidden => "hidden"
Invalid => "invalid"
Pressed => "pressed"
Selected => "selected"
CheckedMixed => "mixed"
PressedMixed => "mixed"
}
}
///|
pub impl Show for State with fn output(self, logger) {
match self {
Busy => logger.write_string("Busy")
Checked => logger.write_string("Checked")
Disabled => logger.write_string("Disabled")
Expanded => logger.write_string("Expanded")
Grabbed => logger.write_string("Grabbed")
Hidden => logger.write_string("Hidden")
Invalid => logger.write_string("Invalid")
Pressed => logger.write_string("Pressed")
Selected => logger.write_string("Selected")
CheckedMixed => logger.write_string("CheckedMixed")
PressedMixed => logger.write_string("PressedMixed")
}
}
// =============================================================================
// Accessibility Node
// =============================================================================
///|
/// Bounding rectangle for spatial information
pub(all) struct Bounds {
x : Double
y : Double
width : Double
height : Double
}
///|
pub fn Bounds::new(
x : Double,
y : Double,
width : Double,
height : Double,
) -> Bounds {
{ x, y, width, height }
}
///|
pub fn Bounds::default() -> Bounds {
{ x: 0.0, y: 0.0, width: 0.0, height: 0.0 }
}
///|
/// Core accessibility node in the AOM tree
pub(all) struct AccessibilityNode {
// Identity
id : String
// HTML-AAM computed role
role : Role
// Accessible name (accname spec)
name : String?
// Accessible description
description : String?
// Heading level (1-6, only for Heading role)
level : Int?
// Current states
states : Array[State]
// ARIA properties
properties : Map[String, String]
// Spatial information (from layout)
bounds : Bounds?
// Tree structure
children : Array[AccessibilityNode]
// Focusable / interactive
focusable : Bool
// Tabindex value for tab navigation order
// None = no explicit tabindex (inherent focusability only)
// Some(-1) = programmatically focusable but not via Tab
// Some(0) = focusable in document order
// Some(n) where n > 0 = focusable in priority order
tabindex : Int?
// Reference to source element id (for debugging/tooling)
source_id : String?
// === AI scraping enhancements ===
// URL for links (href attribute)
href : String?
// Original HTML tag name (for disambiguation when role is generic)
tag_name : String?
// CSS selector for element targeting (for AI interaction)
selector : String?
// Whether this is visible content (false for style, script, head content)
visible : Bool
// Sequential reference ID for AI interaction (e.g., "ref_1", "ref_2")
// Simple, unique identifier for element targeting
ref_id : String?
// === Form input enhancements ===
// Input type for form elements (text, email, password, checkbox, radio, etc.)
input_type : String?
// Placeholder text for form inputs
placeholder : String?
// Current value of form inputs
input_value : String?
// Whether the form field is required
required : Bool
// === AI text aggregation ===
// Aggregated text content from all children (for paragraphs, etc.)
text : String?
// === Select element options ===
// Options for select/listbox elements
options : Array[SelectOption]
// === Label associations ===
// Ref ID of the element that labels this element
labelled_by : String?
}
///|
/// Option for select elements
pub(all) struct SelectOption {
value : String
label : String
selected : Bool
}
///|
pub fn AccessibilityNode::new(id : String, role : Role) -> AccessibilityNode {
{
id,
role,
name: Option::None,
description: Option::None,
level: Option::None,
states: [],
properties: {},
bounds: Option::None,
children: [],
focusable: false,
tabindex: Option::None,
source_id: Option::None,
href: Option::None,
tag_name: Option::None,
selector: Option::None,
visible: true,
ref_id: Option::None,
input_type: Option::None,
placeholder: Option::None,
input_value: Option::None,
required: false,
text: Option::None,
options: [],
labelled_by: Option::None,
}
}
///|
pub fn AccessibilityNode::with_name(
self : AccessibilityNode,
name : String,
) -> AccessibilityNode {
{ ..self, name: Some(name) }
}
///|
pub fn AccessibilityNode::with_bounds(
self : AccessibilityNode,
bounds : Bounds,
) -> AccessibilityNode {
{ ..self, bounds: Some(bounds) }
}
///|
pub fn AccessibilityNode::with_level(
self : AccessibilityNode,
level : Int,
) -> AccessibilityNode {
{ ..self, level: Some(level) }
}
///|
pub fn AccessibilityNode::add_state(
self : AccessibilityNode,
state : State,
) -> AccessibilityNode {
let new_states = self.states.copy()
new_states.push(state)
{ ..self, states: new_states }
}
///|
pub fn AccessibilityNode::add_child(
self : AccessibilityNode,
child : AccessibilityNode,
) -> AccessibilityNode {
let new_children = self.children.copy()
new_children.push(child)
{ ..self, children: new_children }
}
///|
pub fn AccessibilityNode::set_focusable(
self : AccessibilityNode,
focusable : Bool,
) -> AccessibilityNode {
{ ..self, focusable, }
}
///|
pub fn AccessibilityNode::with_tabindex(
self : AccessibilityNode,
tabindex : Int,
) -> AccessibilityNode {
{ ..self, tabindex: Some(tabindex) }
}
// =============================================================================
// Accessibility Tree
// =============================================================================
///|
/// The complete accessibility tree
pub(all) struct AccessibilityTree {
root : AccessibilityNode
// Index for fast lookup by id
node_map : Map[String, AccessibilityNode]
}
///|
pub fn AccessibilityTree::new(root : AccessibilityNode) -> AccessibilityTree {
let node_map : Map[String, AccessibilityNode] = {}
build_node_map(root, node_map)
{ root, node_map }
}
///|
fn build_node_map(
node : AccessibilityNode,
map : Map[String, AccessibilityNode],
) -> Unit {
map[node.id] = node
for child in node.children {
build_node_map(child, map)
}
}
///|
pub fn AccessibilityTree::find_by_id(
self : AccessibilityTree,
id : String,
) -> AccessibilityNode? {
self.node_map.get(id)
}
///|
pub fn AccessibilityTree::find_by_role(
self : AccessibilityTree,
role : Role,
) -> Array[AccessibilityNode] {
let result : Array[AccessibilityNode] = []
find_by_role_recursive(self.root, role, result)
result
}
///|
fn find_by_role_recursive(
node : AccessibilityNode,
role : Role,
result : Array[AccessibilityNode],
) -> Unit {
if node.role == role {
result.push(node)
}
for child in node.children {
find_by_role_recursive(child, role, result)
}
}
///|
pub fn AccessibilityTree::find_focusable(
self : AccessibilityTree,
) -> Array[AccessibilityNode] {
let result : Array[AccessibilityNode] = []
find_focusable_recursive(self.root, result)
result
}
///|
fn find_focusable_recursive(
node : AccessibilityNode,
result : Array[AccessibilityNode],
) -> Unit {
if node.focusable {
result.push(node)
}
for child in node.children {
find_focusable_recursive(child, result)
}
}
///|
/// Find node by its source element id
pub fn AccessibilityTree::find_by_source_id(
self : AccessibilityTree,
source_id : String,
) -> AccessibilityNode? {
find_by_source_id_recursive(self.root, source_id)
}
///|
fn find_by_source_id_recursive(
node : AccessibilityNode,
source_id : String,
) -> AccessibilityNode? {
match node.source_id {
Some(id) if id == source_id => return Some(node)
_ => ()
}
for child in node.children {
match find_by_source_id_recursive(child, source_id) {
Some(found) => return Some(found)
Option::None => ()
}
}
Option::None
}