1use crate::error::AuditError;
6use crate::node::{is_interactive, A11yNode, NodeId};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct TreeAudit {
11 pub nodes: usize,
13 pub roots: usize,
15 pub interactive: usize,
17 pub relations: usize,
19}
20
21#[derive(Debug, Clone, Default)]
23pub struct A11yTree {
24 nodes: Vec<A11yNode>,
25}
26
27impl A11yTree {
28 #[must_use]
30 pub fn new() -> Self {
31 Self::default()
32 }
33
34 pub fn insert(&mut self, node: A11yNode) -> NodeId {
37 let id = node.id;
38 self.nodes.push(node);
39 id
40 }
41
42 pub fn push(&mut self, mut node: A11yNode) -> NodeId {
44 node.id = NodeId(self.nodes.len() as u32);
45 self.insert(node)
46 }
47
48 #[must_use]
50 pub fn get(&self, id: NodeId) -> Option<&A11yNode> {
51 self.nodes.iter().find(|n| n.id == id)
52 }
53
54 pub fn nodes(&self) -> &[A11yNode] {
56 &self.nodes
57 }
58
59 #[must_use]
61 pub fn roots(&self) -> Vec<NodeId> {
62 self.nodes
63 .iter()
64 .filter(|n| n.parent.is_none())
65 .map(|n| n.id)
66 .collect()
67 }
68
69 #[must_use]
71 pub fn focused(&self) -> Vec<NodeId> {
72 self.nodes
73 .iter()
74 .filter(|n| n.states.focused)
75 .map(|n| n.id)
76 .collect()
77 }
78
79 pub fn audit(&self) -> Result<TreeAudit, AuditError> {
87 for (i, node) in self.nodes.iter().enumerate() {
89 if self.nodes.iter().skip(i + 1).any(|n| n.id == node.id) {
90 return Err(AuditError::DuplicateId(node.id));
91 }
92 }
93
94 for node in &self.nodes {
96 if let Some(parent) = node.parent {
97 if self.get(parent).is_none() {
98 return Err(AuditError::DanglingReference {
99 node: node.id,
100 target: parent,
101 });
102 }
103 }
104 for relation in &node.relations {
105 let target = match relation {
106 crate::node::Relation::Controls(id)
107 | crate::node::Relation::DescribedBy(id)
108 | crate::node::Relation::LabelledBy(id)
109 | crate::node::Relation::ActiveDescendant(id) => *id,
110 };
111 if self.get(target).is_none() {
112 return Err(AuditError::DanglingReference {
113 node: node.id,
114 target,
115 });
116 }
117 }
118 }
119
120 for node in &self.nodes {
122 let mut seen = vec![node.id];
123 let mut current = node.parent;
124 while let Some(id) = current {
125 if seen.contains(&id) {
126 return Err(AuditError::Cycle { node: node.id });
127 }
128 seen.push(id);
129 current = self.get(id).and_then(|n| n.parent);
130 }
131 }
132
133 let mut interactive = 0usize;
135 for node in &self.nodes {
136 if is_interactive(node.role) {
137 interactive += 1;
138 if node.name.trim().is_empty() {
139 return Err(AuditError::MissingAccessibleName(node.id));
140 }
141 }
142 }
143
144 let focused = self.focused();
149 if focused.len() > 1 {
150 return Err(AuditError::MultipleFocus(focused));
151 }
152 if let Some(id) = focused.first() {
153 let in_window = self
154 .nodes
155 .iter()
156 .filter(|n| n.role == crate::node::A11yRole::Window)
157 .flat_map(|w| self.descendants_of(w.id))
158 .any(|n| n == *id);
159 if !in_window {
160 return Err(AuditError::UnreachableFocus(*id));
161 }
162 }
163
164 let relations = self.nodes.iter().map(|n| n.relations.len()).sum();
165 Ok(TreeAudit {
166 nodes: self.nodes.len(),
167 roots: self.roots().len(),
168 interactive,
169 relations,
170 })
171 }
172
173 #[must_use]
175 pub fn reachable_from_roots(&self) -> Vec<NodeId> {
176 let mut out = Vec::new();
177 let mut stack = self.roots();
178 while let Some(id) = stack.pop() {
179 out.push(id);
180 for child in self.children_of(id) {
181 stack.push(child);
182 }
183 }
184 out
185 }
186
187 #[must_use]
189 pub fn children_of(&self, id: NodeId) -> Vec<NodeId> {
190 self.nodes
191 .iter()
192 .filter(|n| n.parent == Some(id))
193 .map(|n| n.id)
194 .collect()
195 }
196
197 #[must_use]
199 pub fn descendants_of(&self, id: NodeId) -> Vec<NodeId> {
200 let mut out = vec![id];
201 let mut stack = vec![id];
202 while let Some(current) = stack.pop() {
203 for child in self.children_of(current) {
204 out.push(child);
205 stack.push(child);
206 }
207 }
208 out
209 }
210}