1use ahash::AHashMap;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum SemanticRole {
13 Navigation,
15 Main,
17 Header,
19 Footer,
21 Aside,
23 Article,
25 Section,
27 Search,
29 Form,
31 Button,
33 Link,
35 TextInput,
37 Checkbox,
39 Radio,
41 Select,
43 Heading,
45 List,
47 ListItem,
49 Table,
51 Image,
53 Video,
55 Audio,
57 Dialog,
59 Alert,
61 Menu,
63 Tab,
65 TabPanel,
67 Interactive,
69 Container,
71 Unknown,
73}
74
75impl SemanticRole {
76 pub fn is_landmark(&self) -> bool {
78 matches!(
79 self,
80 SemanticRole::Navigation
81 | SemanticRole::Main
82 | SemanticRole::Header
83 | SemanticRole::Footer
84 | SemanticRole::Aside
85 | SemanticRole::Search
86 )
87 }
88
89 pub fn is_interactable(&self) -> bool {
91 matches!(
92 self,
93 SemanticRole::Button
94 | SemanticRole::Link
95 | SemanticRole::TextInput
96 | SemanticRole::Checkbox
97 | SemanticRole::Radio
98 | SemanticRole::Select
99 | SemanticRole::Interactive
100 )
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(rename_all = "lowercase")]
107pub enum SemanticIntent {
108 Navigate,
110 Submit,
112 Action,
114 Toggle,
116 Select,
118 Input,
120 Search,
122 Play,
124 Pause,
126 Open,
128 Close,
130 Expand,
132 Collapse,
134 Download,
136 Delete,
138 Edit,
140 Create,
142 Unknown,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct SemanticNode {
149 pub id: String,
151 pub label: String,
153 pub role: SemanticRole,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub intent: Option<SemanticIntent>,
158 pub selector: String,
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub accessible_name: Option<String>,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub href: Option<String>,
166 #[serde(skip_serializing_if = "Vec::is_empty", default)]
168 pub children: Vec<String>,
169 #[serde(skip_serializing_if = "Option::is_none")]
171 pub parent: Option<String>,
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub metadata: Option<AHashMap<String, String>>,
175 pub depth: usize,
177}
178
179impl SemanticNode {
180 pub fn new(id: String, label: String, role: SemanticRole, selector: String) -> Self {
182 Self {
183 id,
184 label,
185 role,
186 intent: None,
187 selector,
188 accessible_name: None,
189 href: None,
190 children: Vec::new(),
191 parent: None,
192 metadata: None,
193 depth: 0,
194 }
195 }
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct State {
201 pub id: String,
203 pub name: String,
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub description: Option<String>,
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub url_pattern: Option<String>,
211 #[serde(default)]
213 pub is_initial: bool,
214 #[serde(default)]
216 pub is_terminal: bool,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct Transition {
222 pub from: String,
224 pub to: String,
226 pub trigger: String,
228 #[serde(skip_serializing_if = "Option::is_none")]
230 pub action: Option<String>,
231 #[serde(skip_serializing_if = "Option::is_none")]
233 pub guard: Option<String>,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct StateGraph {
239 pub states: Vec<State>,
241 pub transitions: Vec<Transition>,
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub initial_state: Option<String>,
246}
247
248impl Default for StateGraph {
249 fn default() -> Self {
250 Self {
251 states: Vec::new(),
252 transitions: Vec::new(),
253 initial_state: None,
254 }
255 }
256}
257
258impl StateGraph {
259 pub fn new() -> Self {
261 Self::default()
262 }
263
264 pub fn is_deterministic(&self) -> bool {
266 let mut seen: AHashMap<(&str, &str), bool> = AHashMap::new();
267 for t in &self.transitions {
268 let key = (t.from.as_str(), t.trigger.as_str());
269 if seen.contains_key(&key) {
270 return false;
271 }
272 seen.insert(key, true);
273 }
274 true
275 }
276
277 pub fn reachable_states(&self) -> Vec<&State> {
279 let initial = match &self.initial_state {
280 Some(id) => id,
281 None => return Vec::new(),
282 };
283
284 let mut visited: AHashMap<&str, bool> = AHashMap::new();
285 let mut queue = vec![initial.as_str()];
286
287 while let Some(state_id) = queue.pop() {
288 if visited.contains_key(state_id) {
289 continue;
290 }
291 visited.insert(state_id, true);
292
293 for t in &self.transitions {
294 if t.from == state_id && !visited.contains_key(t.to.as_str()) {
295 queue.push(&t.to);
296 }
297 }
298 }
299
300 self.states
301 .iter()
302 .filter(|s| visited.contains_key(s.id.as_str()))
303 .collect()
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_role_is_landmark() {
313 assert!(SemanticRole::Navigation.is_landmark());
314 assert!(SemanticRole::Main.is_landmark());
315 assert!(!SemanticRole::Button.is_landmark());
316 }
317
318 #[test]
319 fn test_role_is_interactable() {
320 assert!(SemanticRole::Button.is_interactable());
321 assert!(SemanticRole::Link.is_interactable());
322 assert!(!SemanticRole::Navigation.is_interactable());
323 }
324
325 #[test]
326 fn test_state_graph_deterministic() {
327 let mut graph = StateGraph::new();
328 graph.states.push(State {
329 id: "home".to_string(),
330 name: "Home".to_string(),
331 description: None,
332 url_pattern: Some("/".to_string()),
333 is_initial: true,
334 is_terminal: false,
335 });
336 graph.transitions.push(Transition {
337 from: "home".to_string(),
338 to: "about".to_string(),
339 trigger: "about-link".to_string(),
340 action: None,
341 guard: None,
342 });
343 graph.initial_state = Some("home".to_string());
344
345 assert!(graph.is_deterministic());
346
347 graph.transitions.push(Transition {
349 from: "home".to_string(),
350 to: "contact".to_string(),
351 trigger: "about-link".to_string(), action: None,
353 guard: None,
354 });
355
356 assert!(!graph.is_deterministic());
357 }
358}