use serde::{Deserialize, Serialize};
use std::collections::HashMap;
fn is_false(value: &bool) -> bool {
!*value
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AriaNode {
pub role: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub classes: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub index: Option<usize>,
#[serde(default, skip_serializing_if = "is_false")]
pub public_handle: bool,
#[serde(default)]
pub children: Vec<AriaChild>,
#[serde(default)]
pub props: HashMap<String, String>,
#[serde(default)]
pub box_info: BoxInfo,
#[serde(skip_serializing_if = "Option::is_none")]
pub checked: Option<AriaChecked>,
#[serde(skip_serializing_if = "Option::is_none")]
pub disabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expanded: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub level: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pressed: Option<AriaPressed>,
#[serde(skip_serializing_if = "Option::is_none")]
pub selected: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum AriaChild {
Text(String),
Node(Box<AriaNode>),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum AriaChecked {
Bool(bool),
Mixed(String), }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum AriaPressed {
Bool(bool),
Mixed(String), }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct BoxInfo {
#[serde(default)]
pub visible: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub in_viewport: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub persistent_chrome: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub persistent_position: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub persistent_edge: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
impl AriaNode {
pub fn new(role: impl Into<String>, name: impl Into<String>) -> Self {
Self {
role: role.into(),
name: name.into(),
tag: None,
id: None,
classes: Vec::new(),
index: None,
public_handle: false,
children: Vec::new(),
props: HashMap::new(),
box_info: BoxInfo::default(),
checked: None,
disabled: None,
expanded: None,
level: None,
pressed: None,
selected: None,
active: None,
}
}
pub fn fragment() -> Self {
Self::new("fragment", "")
}
pub fn with_index(mut self, index: usize) -> Self {
self.index = Some(index);
self
}
pub fn with_dom_identity(
mut self,
tag: impl Into<String>,
id: Option<String>,
classes: Vec<String>,
) -> Self {
self.tag = Some(tag.into());
self.id = id;
self.classes = classes;
self
}
pub fn with_public_handle(mut self, public_handle: bool) -> Self {
self.public_handle = public_handle;
self
}
pub fn with_child(mut self, child: AriaChild) -> Self {
self.children.push(child);
self
}
pub fn with_children(mut self, children: Vec<AriaChild>) -> Self {
self.children = children;
self
}
pub fn with_prop(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.props.insert(key.into(), value.into());
self
}
pub fn with_box(mut self, visible: bool, cursor: Option<String>) -> Self {
self.box_info = BoxInfo {
visible,
in_viewport: visible,
persistent_chrome: false,
persistent_position: None,
persistent_edge: None,
cursor,
};
self
}
pub fn with_checked(mut self, checked: bool) -> Self {
self.checked = Some(AriaChecked::Bool(checked));
self
}
pub fn with_disabled(mut self, disabled: bool) -> Self {
self.disabled = Some(disabled);
self
}
pub fn with_expanded(mut self, expanded: bool) -> Self {
self.expanded = Some(expanded);
self
}
pub fn with_selected(mut self, selected: bool) -> Self {
self.selected = Some(selected);
self
}
pub fn with_active(mut self, active: bool) -> Self {
self.active = Some(active);
self
}
pub fn with_level(mut self, level: u32) -> Self {
self.level = Some(level);
self
}
pub fn is_interactive(&self) -> bool {
self.index.is_some() && self.box_info.visible
}
pub fn has_public_handle(&self) -> bool {
self.index.is_some() && self.public_handle
}
pub fn has_pointer_cursor(&self) -> bool {
self.box_info
.cursor
.as_ref()
.is_some_and(|c| c == "pointer")
}
pub fn is_persistent_chrome(&self) -> bool {
self.box_info.persistent_chrome
}
pub fn carries_snapshot_state(&self) -> bool {
self.active == Some(true)
|| self.expanded == Some(true)
|| self.selected == Some(true)
|| self.checked.is_some()
|| self.pressed.is_some()
|| self.disabled == Some(true)
}
pub fn is_container(&self) -> bool {
self.role == "fragment" || self.role == "iframe"
}
pub fn get_text_content(&self) -> String {
let mut result = String::new();
self.collect_text(&mut result);
result.trim().to_string()
}
fn collect_text(&self, buffer: &mut String) {
for child in &self.children {
match child {
AriaChild::Text(text) => {
buffer.push_str(text);
buffer.push(' ');
}
AriaChild::Node(node) => {
node.collect_text(buffer);
}
}
}
}
pub fn count_nodes(&self) -> usize {
1 + self
.children
.iter()
.map(|c| match c {
AriaChild::Text(_) => 0,
AriaChild::Node(n) => n.count_nodes(),
})
.sum::<usize>()
}
pub fn find_by_index(&self, index: usize) -> Option<&AriaNode> {
if self.index == Some(index) {
return Some(self);
}
for child in &self.children {
if let AriaChild::Node(node) = child
&& let Some(found) = node.find_by_index(index)
{
return Some(found);
}
}
None
}
pub fn find_by_index_mut(&mut self, index: usize) -> Option<&mut AriaNode> {
if self.index == Some(index) {
return Some(self);
}
for child in &mut self.children {
if let AriaChild::Node(node) = child
&& let Some(found) = node.find_by_index_mut(index)
{
return Some(found);
}
}
None
}
pub fn count_interactive(&self) -> usize {
let mut count = 0;
self.count_interactive_recursive(&mut count);
count
}
fn count_interactive_recursive(&self, count: &mut usize) {
if self.index.is_some() {
*count += 1;
}
for child in &self.children {
if let AriaChild::Node(node) = child {
node.count_interactive_recursive(count);
}
}
}
pub fn aria_equals(&self, other: &AriaNode) -> bool {
if self.role != other.role || self.name != other.name {
return false;
}
if self.checked != other.checked
|| self.disabled != other.disabled
|| self.expanded != other.expanded
|| self.level != other.level
|| self.pressed != other.pressed
|| self.selected != other.selected
{
return false;
}
if self.has_pointer_cursor() != other.has_pointer_cursor() {
return false;
}
if self.props.len() != other.props.len() {
return false;
}
for (k, v) in &self.props {
if other.props.get(k) != Some(v) {
return false;
}
}
true
}
}
pub type ElementNode = AriaNode;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BoundingBox {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
impl BoundingBox {
pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
Self {
x,
y,
width,
height,
}
}
pub fn is_visible(&self) -> bool {
self.width > 0.0 && self.height > 0.0
}
pub fn area(&self) -> f64 {
self.width * self.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_interactive() {
let interactive = AriaNode::new("button", "Click")
.with_index(0)
.with_box(true, None);
assert!(interactive.is_interactive());
let not_interactive = AriaNode::new("button", "Click").with_box(false, None);
assert!(!not_interactive.is_interactive());
let no_index = AriaNode::new("button", "Click").with_box(true, None);
assert!(!no_index.is_interactive());
}
#[test]
fn test_has_pointer_cursor() {
let with_pointer = AriaNode::new("button", "").with_box(true, Some("pointer".to_string()));
assert!(with_pointer.has_pointer_cursor());
let without_pointer =
AriaNode::new("button", "").with_box(true, Some("default".to_string()));
assert!(!without_pointer.has_pointer_cursor());
}
#[test]
fn test_is_persistent_chrome() {
let mut sticky = AriaNode::new("button", "Nav").with_box(true, Some("pointer".to_string()));
sticky.box_info.persistent_chrome = true;
sticky.box_info.persistent_position = Some("sticky".to_string());
sticky.box_info.persistent_edge = Some("top".to_string());
assert!(sticky.is_persistent_chrome());
assert_eq!(
sticky.box_info.persistent_position.as_deref(),
Some("sticky")
);
assert_eq!(sticky.box_info.persistent_edge.as_deref(), Some("top"));
}
#[test]
fn test_get_text_content() {
let mut node = AriaNode::new("div", "");
node.children.push(AriaChild::Text("Hello ".to_string()));
node.children.push(AriaChild::Node(Box::new(
AriaNode::new("span", "").with_child(AriaChild::Text("World".to_string())),
)));
assert_eq!(node.get_text_content(), "Hello World");
}
#[test]
fn test_find_by_index() {
let mut root = AriaNode::new("fragment", "");
root.children.push(AriaChild::Node(Box::new(
AriaNode::new("button", "First").with_index(0),
)));
root.children.push(AriaChild::Node(Box::new(
AriaNode::new("button", "Second").with_index(1),
)));
let found = root.find_by_index(1);
assert!(found.is_some());
assert_eq!(found.unwrap().name, "Second");
let not_found = root.find_by_index(999);
assert!(not_found.is_none());
}
#[test]
fn test_count_interactive() {
let mut root = AriaNode::fragment().with_index(0);
root.children.push(AriaChild::Node(Box::new(
AriaNode::new("button", "").with_index(1),
)));
root.children.push(AriaChild::Node(Box::new(
AriaNode::new("link", "").with_index(2),
)));
let count = root.count_interactive();
assert_eq!(count, 3); }
#[test]
fn test_aria_equals() {
let node1 = AriaNode::new("button", "Click")
.with_disabled(false)
.with_box(true, Some("pointer".to_string()));
let node2 = AriaNode::new("button", "Click")
.with_disabled(false)
.with_box(true, Some("pointer".to_string()));
assert!(node1.aria_equals(&node2));
let node3 = AriaNode::new("button", "Click")
.with_disabled(true)
.with_box(true, Some("pointer".to_string()));
assert!(!node1.aria_equals(&node3));
}
#[test]
fn test_count_nodes() {
let mut root = AriaNode::fragment();
root.children.push(AriaChild::Text("text".to_string()));
root.children
.push(AriaChild::Node(Box::new(AriaNode::new("button", ""))));
root.children.push(AriaChild::Node(Box::new(
AriaNode::new("div", "")
.with_child(AriaChild::Node(Box::new(AriaNode::new("span", "")))),
)));
assert_eq!(root.count_nodes(), 4);
}
}