1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub struct AriaNode {
8 pub role: String,
10
11 pub name: String,
13
14 #[serde(skip_serializing_if = "Option::is_none")]
16 pub index: Option<usize>,
17
18 #[serde(default)]
20 pub children: Vec<AriaChild>,
21
22 #[serde(default)]
24 pub props: HashMap<String, String>,
25
26 #[serde(default)]
28 pub box_info: BoxInfo,
29
30 #[serde(skip_serializing_if = "Option::is_none")]
33 pub checked: Option<AriaChecked>,
34
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub disabled: Option<bool>,
38
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub expanded: Option<bool>,
42
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub level: Option<u32>,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub pressed: Option<AriaPressed>,
50
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub selected: Option<bool>,
54
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub active: Option<bool>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
62#[serde(untagged)]
63pub enum AriaChild {
64 Text(String),
65 Node(Box<AriaNode>),
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
70#[serde(untagged)]
71pub enum AriaChecked {
72 Bool(bool),
73 Mixed(String), }
75
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
78#[serde(untagged)]
79pub enum AriaPressed {
80 Bool(bool),
81 Mixed(String), }
83
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
86pub struct BoxInfo {
87 #[serde(default)]
89 pub visible: bool,
90
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub cursor: Option<String>,
94}
95
96impl Default for BoxInfo {
97 fn default() -> Self {
98 Self {
99 visible: false,
100 cursor: None,
101 }
102 }
103}
104
105impl AriaNode {
106 pub fn new(role: impl Into<String>, name: impl Into<String>) -> Self {
108 Self {
109 role: role.into(),
110 name: name.into(),
111 index: None,
112 children: Vec::new(),
113 props: HashMap::new(),
114 box_info: BoxInfo::default(),
115 checked: None,
116 disabled: None,
117 expanded: None,
118 level: None,
119 pressed: None,
120 selected: None,
121 active: None,
122 }
123 }
124
125 pub fn fragment() -> Self {
127 Self::new("fragment", "")
128 }
129
130 pub fn with_index(mut self, index: usize) -> Self {
132 self.index = Some(index);
133 self
134 }
135
136 pub fn with_child(mut self, child: AriaChild) -> Self {
138 self.children.push(child);
139 self
140 }
141
142 pub fn with_children(mut self, children: Vec<AriaChild>) -> Self {
144 self.children = children;
145 self
146 }
147
148 pub fn with_prop(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
150 self.props.insert(key.into(), value.into());
151 self
152 }
153
154 pub fn with_box(mut self, visible: bool, cursor: Option<String>) -> Self {
156 self.box_info = BoxInfo { visible, cursor };
157 self
158 }
159
160 pub fn with_checked(mut self, checked: bool) -> Self {
162 self.checked = Some(AriaChecked::Bool(checked));
163 self
164 }
165
166 pub fn with_disabled(mut self, disabled: bool) -> Self {
168 self.disabled = Some(disabled);
169 self
170 }
171
172 pub fn with_expanded(mut self, expanded: bool) -> Self {
174 self.expanded = Some(expanded);
175 self
176 }
177
178 pub fn with_level(mut self, level: u32) -> Self {
180 self.level = Some(level);
181 self
182 }
183
184 pub fn is_interactive(&self) -> bool {
186 self.index.is_some() && self.box_info.visible
187 }
188
189 pub fn has_pointer_cursor(&self) -> bool {
191 self.box_info
192 .cursor
193 .as_ref()
194 .map_or(false, |c| c == "pointer")
195 }
196
197 pub fn is_container(&self) -> bool {
199 self.role == "fragment" || self.role == "iframe"
200 }
201
202 pub fn get_text_content(&self) -> String {
204 let mut result = String::new();
205 self.collect_text(&mut result);
206 result.trim().to_string()
207 }
208
209 fn collect_text(&self, buffer: &mut String) {
210 for child in &self.children {
211 match child {
212 AriaChild::Text(text) => {
213 buffer.push_str(text);
214 buffer.push(' ');
215 }
216 AriaChild::Node(node) => {
217 node.collect_text(buffer);
218 }
219 }
220 }
221 }
222
223 pub fn count_nodes(&self) -> usize {
225 1 + self
226 .children
227 .iter()
228 .map(|c| match c {
229 AriaChild::Text(_) => 0,
230 AriaChild::Node(n) => n.count_nodes(),
231 })
232 .sum::<usize>()
233 }
234
235 pub fn find_by_index(&self, index: usize) -> Option<&AriaNode> {
237 if self.index == Some(index) {
238 return Some(self);
239 }
240
241 for child in &self.children {
242 if let AriaChild::Node(node) = child {
243 if let Some(found) = node.find_by_index(index) {
244 return Some(found);
245 }
246 }
247 }
248
249 None
250 }
251
252 pub fn find_by_index_mut(&mut self, index: usize) -> Option<&mut AriaNode> {
254 if self.index == Some(index) {
255 return Some(self);
256 }
257
258 for child in &mut self.children {
259 if let AriaChild::Node(node) = child {
260 if let Some(found) = node.find_by_index_mut(index) {
261 return Some(found);
262 }
263 }
264 }
265
266 None
267 }
268
269 pub fn count_interactive(&self) -> usize {
271 let mut count = 0;
272 self.count_interactive_recursive(&mut count);
273 count
274 }
275
276 fn count_interactive_recursive(&self, count: &mut usize) {
277 if self.index.is_some() {
278 *count += 1;
279 }
280
281 for child in &self.children {
282 if let AriaChild::Node(node) = child {
283 node.count_interactive_recursive(count);
284 }
285 }
286 }
287
288 pub fn aria_equals(&self, other: &AriaNode) -> bool {
291 if self.role != other.role || self.name != other.name {
292 return false;
293 }
294
295 if self.checked != other.checked
296 || self.disabled != other.disabled
297 || self.expanded != other.expanded
298 || self.level != other.level
299 || self.pressed != other.pressed
300 || self.selected != other.selected
301 {
302 return false;
303 }
304
305 if self.has_pointer_cursor() != other.has_pointer_cursor() {
306 return false;
307 }
308
309 if self.props.len() != other.props.len() {
310 return false;
311 }
312
313 for (k, v) in &self.props {
314 if other.props.get(k) != Some(v) {
315 return false;
316 }
317 }
318
319 true
320 }
321}
322
323pub type ElementNode = AriaNode;
326
327#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
329pub struct BoundingBox {
330 pub x: f64,
331 pub y: f64,
332 pub width: f64,
333 pub height: f64,
334}
335
336impl BoundingBox {
337 pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
338 Self {
339 x,
340 y,
341 width,
342 height,
343 }
344 }
345
346 pub fn is_visible(&self) -> bool {
347 self.width > 0.0 && self.height > 0.0
348 }
349
350 pub fn area(&self) -> f64 {
351 self.width * self.height
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn test_is_interactive() {
361 let interactive = AriaNode::new("button", "Click")
362 .with_index(0)
363 .with_box(true, None);
364 assert!(interactive.is_interactive());
365
366 let not_interactive = AriaNode::new("button", "Click").with_box(false, None);
367 assert!(!not_interactive.is_interactive());
368
369 let no_index = AriaNode::new("button", "Click").with_box(true, None);
370 assert!(!no_index.is_interactive());
371 }
372
373 #[test]
374 fn test_has_pointer_cursor() {
375 let with_pointer = AriaNode::new("button", "").with_box(true, Some("pointer".to_string()));
376 assert!(with_pointer.has_pointer_cursor());
377
378 let without_pointer =
379 AriaNode::new("button", "").with_box(true, Some("default".to_string()));
380 assert!(!without_pointer.has_pointer_cursor());
381 }
382
383 #[test]
384 fn test_get_text_content() {
385 let mut node = AriaNode::new("div", "");
386 node.children.push(AriaChild::Text("Hello ".to_string()));
387 node.children.push(AriaChild::Node(Box::new(
388 AriaNode::new("span", "").with_child(AriaChild::Text("World".to_string())),
389 )));
390
391 assert_eq!(node.get_text_content(), "Hello World");
392 }
393
394 #[test]
395 fn test_find_by_index() {
396 let mut root = AriaNode::new("fragment", "");
397 root.children.push(AriaChild::Node(Box::new(
398 AriaNode::new("button", "First").with_index(0),
399 )));
400 root.children.push(AriaChild::Node(Box::new(
401 AriaNode::new("button", "Second").with_index(1),
402 )));
403
404 let found = root.find_by_index(1);
405 assert!(found.is_some());
406 assert_eq!(found.unwrap().name, "Second");
407
408 let not_found = root.find_by_index(999);
409 assert!(not_found.is_none());
410 }
411
412 #[test]
413 fn test_count_interactive() {
414 let mut root = AriaNode::fragment().with_index(0);
415 root.children.push(AriaChild::Node(Box::new(
416 AriaNode::new("button", "").with_index(1),
417 )));
418 root.children.push(AriaChild::Node(Box::new(
419 AriaNode::new("link", "").with_index(2),
420 )));
421
422 let count = root.count_interactive();
423 assert_eq!(count, 3); }
425
426 #[test]
427 fn test_aria_equals() {
428 let node1 = AriaNode::new("button", "Click")
429 .with_disabled(false)
430 .with_box(true, Some("pointer".to_string()));
431
432 let node2 = AriaNode::new("button", "Click")
433 .with_disabled(false)
434 .with_box(true, Some("pointer".to_string()));
435
436 assert!(node1.aria_equals(&node2));
437
438 let node3 = AriaNode::new("button", "Click")
439 .with_disabled(true)
440 .with_box(true, Some("pointer".to_string()));
441
442 assert!(!node1.aria_equals(&node3));
443 }
444
445 #[test]
446 fn test_count_nodes() {
447 let mut root = AriaNode::fragment();
448 root.children.push(AriaChild::Text("text".to_string()));
449 root.children
450 .push(AriaChild::Node(Box::new(AriaNode::new("button", ""))));
451 root.children.push(AriaChild::Node(Box::new(
452 AriaNode::new("div", "")
453 .with_child(AriaChild::Node(Box::new(AriaNode::new("span", "")))),
454 )));
455
456 assert_eq!(root.count_nodes(), 4);
458 }
459}