1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
6pub struct ElementNode {
7 pub tag_name: String,
9
10 #[serde(default)]
12 pub attributes: HashMap<String, String>,
13
14 #[serde(skip_serializing_if = "Option::is_none")]
16 pub text_content: Option<String>,
17
18 #[serde(default, skip_serializing_if = "Vec::is_empty")]
20 pub children: Vec<ElementNode>,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub index: Option<usize>,
25
26 #[serde(default)]
28 pub is_visible: bool,
29
30 #[serde(default)]
32 pub is_interactive: bool,
33
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub bounding_box: Option<BoundingBox>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
41pub struct BoundingBox {
42 pub x: f64,
43 pub y: f64,
44 pub width: f64,
45 pub height: f64,
46}
47
48impl ElementNode {
49 pub fn new(tag_name: impl Into<String>) -> Self {
51 Self {
52 tag_name: tag_name.into(),
53 attributes: HashMap::new(),
54 text_content: None,
55 children: Vec::new(),
56 index: None,
57 is_visible: false,
58 is_interactive: false,
59 bounding_box: None,
60 }
61 }
62
63 pub fn with_attributes(mut self, attributes: HashMap<String, String>) -> Self {
65 self.attributes = attributes;
66 self
67 }
68
69 pub fn with_text(mut self, text: impl Into<String>) -> Self {
71 self.text_content = Some(text.into());
72 self
73 }
74
75 pub fn with_children(mut self, children: Vec<ElementNode>) -> Self {
77 self.children = children;
78 self
79 }
80
81 pub fn with_index(mut self, index: usize) -> Self {
83 self.index = Some(index);
84 self
85 }
86
87 pub fn with_visibility(mut self, visible: bool) -> Self {
89 self.is_visible = visible;
90 self
91 }
92
93 pub fn with_interactivity(mut self, interactive: bool) -> Self {
95 self.is_interactive = interactive;
96 self
97 }
98
99 pub fn with_bounding_box(mut self, x: f64, y: f64, width: f64, height: f64) -> Self {
101 self.bounding_box = Some(BoundingBox {
102 x,
103 y,
104 width,
105 height,
106 });
107 self
108 }
109
110 pub fn add_attribute(&mut self, key: impl Into<String>, value: impl Into<String>) {
112 self.attributes.insert(key.into(), value.into());
113 }
114
115 pub fn add_child(&mut self, child: ElementNode) {
117 self.children.push(child);
118 }
119
120 pub fn get_attribute(&self, key: &str) -> Option<&String> {
122 self.attributes.get(key)
123 }
124
125 pub fn has_class(&self, class_name: &str) -> bool {
127 if let Some(classes) = self.attributes.get("class") {
128 classes.split_whitespace().any(|c| c == class_name)
129 } else {
130 false
131 }
132 }
133
134 pub fn id(&self) -> Option<&String> {
136 self.attributes.get("id")
137 }
138
139 pub fn is_tag(&self, tag: &str) -> bool {
141 self.tag_name.eq_ignore_ascii_case(tag)
142 }
143
144 pub fn compute_interactivity(&mut self) {
146 let interactive_tags = ["button", "a", "input", "select", "textarea", "label"];
148
149 let tag_is_interactive = interactive_tags.iter().any(|&tag| self.is_tag(tag));
151
152 let has_event_handler = self.attributes.keys().any(|k| {
154 k.starts_with("on")
155 || k == "role" && self.attributes.get("role").map_or(false, |r| r == "button")
156 });
157
158 let has_clickable_role = self.get_attribute("role").map_or(false, |r| {
160 ["button", "link", "tab", "menuitem"].contains(&r.as_str())
161 });
162
163 self.is_interactive = tag_is_interactive || has_event_handler || has_clickable_role;
164 }
165
166 pub fn simplify(&mut self) {
168 self.children
170 .retain(|child| !matches!(child.tag_name.as_str(), "script" | "style" | "noscript"));
171
172 for child in &mut self.children {
174 child.simplify();
175 }
176 }
177
178 pub fn to_simple_string(&self) -> String {
180 let mut parts = vec![format!("<{}", self.tag_name)];
181
182 if let Some(id) = self.id() {
183 parts.push(format!(" id=\"{}\"", id));
184 }
185
186 if let Some(class) = self.attributes.get("class") {
187 parts.push(format!(" class=\"{}\"", class));
188 }
189
190 if let Some(index) = self.index {
191 parts.push(format!(" data-index=\"{}\"", index));
192 }
193
194 parts.push(">".to_string());
195
196 if let Some(text) = &self.text_content {
197 if !text.trim().is_empty() {
198 parts.push(text.trim().to_string());
199 }
200 }
201
202 parts.join("")
203 }
204}
205
206impl BoundingBox {
207 pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
209 Self {
210 x,
211 y,
212 width,
213 height,
214 }
215 }
216
217 pub fn is_visible(&self) -> bool {
219 self.width > 0.0 && self.height > 0.0
220 }
221
222 pub fn area(&self) -> f64 {
224 self.width * self.height
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn test_element_node_creation() {
234 let mut attrs = HashMap::new();
235 attrs.insert("id".to_string(), "test-id".to_string());
236 attrs.insert("class".to_string(), "btn primary".to_string());
237
238 let element = ElementNode::new("button")
239 .with_attributes(attrs)
240 .with_text("Click me")
241 .with_index(1)
242 .with_visibility(true)
243 .with_interactivity(true);
244
245 assert_eq!(element.tag_name, "button");
246 assert_eq!(element.id(), Some(&"test-id".to_string()));
247 assert_eq!(element.text_content, Some("Click me".to_string()));
248 assert_eq!(element.index, Some(1));
249 assert!(element.is_visible);
250 assert!(element.is_interactive);
251 }
252
253 #[test]
254 fn test_has_class() {
255 let mut element = ElementNode::new("div");
256 element.add_attribute("class", "container main active");
257
258 assert!(element.has_class("container"));
259 assert!(element.has_class("main"));
260 assert!(element.has_class("active"));
261 assert!(!element.has_class("hidden"));
262 }
263
264 #[test]
265 fn test_compute_interactivity() {
266 let mut button = ElementNode::new("button");
267 button.compute_interactivity();
268 assert!(button.is_interactive);
269
270 let mut div = ElementNode::new("div");
271 div.compute_interactivity();
272 assert!(!div.is_interactive);
273
274 let mut clickable_div = ElementNode::new("div");
275 clickable_div.add_attribute("onclick", "alert('hi')");
276 clickable_div.compute_interactivity();
277 assert!(clickable_div.is_interactive);
278
279 let mut role_button = ElementNode::new("div");
280 role_button.add_attribute("role", "button");
281 role_button.compute_interactivity();
282 assert!(role_button.is_interactive);
283 }
284
285 #[test]
286 fn test_simplify() {
287 let mut parent = ElementNode::new("div");
288 parent.add_child(ElementNode::new("p").with_text("Content"));
289 parent.add_child(ElementNode::new("script").with_text("alert('test')"));
290 parent.add_child(ElementNode::new("style").with_text(".test { color: red; }"));
291 parent.add_child(ElementNode::new("span").with_text("More content"));
292
293 parent.simplify();
294
295 assert_eq!(parent.children.len(), 2);
296 assert!(parent.children[0].is_tag("p"));
297 assert!(parent.children[1].is_tag("span"));
298 }
299
300 #[test]
301 fn test_serialization() {
302 let element = ElementNode::new("button")
303 .with_text("Click")
304 .with_index(5)
305 .with_visibility(true);
306
307 let json = serde_json::to_string(&element).unwrap();
308 let deserialized: ElementNode = serde_json::from_str(&json).unwrap();
309
310 assert_eq!(element, deserialized);
311 }
312
313 #[test]
314 fn test_bounding_box() {
315 let bbox = BoundingBox::new(10.0, 20.0, 100.0, 50.0);
316
317 assert!(bbox.is_visible());
318 assert_eq!(bbox.area(), 5000.0);
319
320 let invisible_bbox = BoundingBox::new(0.0, 0.0, 0.0, 0.0);
321 assert!(!invisible_bbox.is_visible());
322 }
323
324 #[test]
325 fn test_to_simple_string() {
326 let mut attrs = HashMap::new();
327 attrs.insert("id".to_string(), "my-btn".to_string());
328 attrs.insert("class".to_string(), "btn primary".to_string());
329
330 let element = ElementNode::new("button")
331 .with_attributes(attrs)
332 .with_text("Submit")
333 .with_index(10);
334
335 let simple = element.to_simple_string();
336 assert!(simple.contains("<button"));
337 assert!(simple.contains("id=\"my-btn\""));
338 assert!(simple.contains("class=\"btn primary\""));
339 assert!(simple.contains("data-index=\"10\""));
340 assert!(simple.contains("Submit"));
341 }
342}