lumivor 0.1.7__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,428 @@
1
+ (
2
+ doHighlightElements = true
3
+ ) => {
4
+ let highlightIndex = 0; // Reset highlight index
5
+
6
+ function highlightElement(element, index, parentIframe = null) {
7
+ // Create or get highlight container
8
+ let container = document.getElementById('playwright-highlight-container');
9
+ if (!container) {
10
+ container = document.createElement('div');
11
+ container.id = 'playwright-highlight-container';
12
+ container.style.position = 'fixed';
13
+ container.style.pointerEvents = 'none';
14
+ container.style.top = '0';
15
+ container.style.left = '0';
16
+ container.style.width = '100%';
17
+ container.style.height = '100%';
18
+ container.style.zIndex = '2147483647'; // Maximum z-index value
19
+ document.documentElement.appendChild(container);
20
+ }
21
+
22
+ // Generate a color based on the index
23
+ const colors = [
24
+ '#FF0000', '#00FF00', '#0000FF', '#FFA500',
25
+ '#800080', '#008080', '#FF69B4', '#4B0082',
26
+ '#FF4500', '#2E8B57', '#DC143C', '#4682B4'
27
+ ];
28
+ const colorIndex = index % colors.length;
29
+ const baseColor = colors[colorIndex];
30
+ const backgroundColor = `${baseColor}1A`; // 10% opacity version of the color
31
+
32
+ // Create highlight overlay
33
+ const overlay = document.createElement('div');
34
+ overlay.style.position = 'absolute';
35
+ overlay.style.border = `2px solid ${baseColor}`;
36
+ overlay.style.backgroundColor = backgroundColor;
37
+ overlay.style.pointerEvents = 'none';
38
+ overlay.style.boxSizing = 'border-box';
39
+
40
+ // Position overlay based on element
41
+ const rect = element.getBoundingClientRect();
42
+ let top = rect.top;
43
+ let left = rect.left;
44
+
45
+ // Adjust position if element is inside an iframe
46
+ if (parentIframe) {
47
+ const iframeRect = parentIframe.getBoundingClientRect();
48
+ top += iframeRect.top;
49
+ left += iframeRect.left;
50
+ }
51
+
52
+ overlay.style.top = `${top}px`;
53
+ overlay.style.left = `${left}px`;
54
+ overlay.style.width = `${rect.width}px`;
55
+ overlay.style.height = `${rect.height}px`;
56
+
57
+ // Create label
58
+ const label = document.createElement('div');
59
+ label.className = 'playwright-highlight-label';
60
+ label.style.position = 'absolute';
61
+ label.style.background = baseColor;
62
+ label.style.color = 'white';
63
+ label.style.padding = '1px 4px';
64
+ label.style.borderRadius = '4px';
65
+ label.style.fontSize = `${Math.min(12, Math.max(8, rect.height / 2))}px`; // Responsive font size
66
+ label.textContent = index;
67
+
68
+ // Calculate label position
69
+ const labelWidth = 20; // Approximate width
70
+ const labelHeight = 16; // Approximate height
71
+
72
+ // Default position (top-right corner inside the box)
73
+ let labelTop = top + 2;
74
+ let labelLeft = left + rect.width - labelWidth - 2;
75
+
76
+ // Adjust if box is too small
77
+ if (rect.width < labelWidth + 4 || rect.height < labelHeight + 4) {
78
+ // Position outside the box if it's too small
79
+ labelTop = top - labelHeight - 2;
80
+ labelLeft = left + rect.width - labelWidth;
81
+ }
82
+
83
+ // Ensure label stays within viewport
84
+ if (labelTop < 0) labelTop = top + 2;
85
+ if (labelLeft < 0) labelLeft = left + 2;
86
+ if (labelLeft + labelWidth > window.innerWidth) {
87
+ labelLeft = left + rect.width - labelWidth - 2;
88
+ }
89
+
90
+ label.style.top = `${labelTop}px`;
91
+ label.style.left = `${labelLeft}px`;
92
+
93
+ // Add to container
94
+ container.appendChild(overlay);
95
+ container.appendChild(label);
96
+
97
+ // Store reference for cleanup
98
+ element.setAttribute('browser-user-highlight-id', `playwright-highlight-${index}`);
99
+
100
+ return index + 1;
101
+ }
102
+
103
+
104
+ // Helper function to generate XPath as a tree
105
+ function getXPathTree(element, stopAtBoundary = true) {
106
+ const segments = [];
107
+ let currentElement = element;
108
+
109
+ while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {
110
+ // Stop if we hit a shadow root or iframe
111
+ if (stopAtBoundary && (currentElement.parentNode instanceof ShadowRoot || currentElement.parentNode instanceof HTMLIFrameElement)) {
112
+ break;
113
+ }
114
+
115
+ let index = 0;
116
+ let sibling = currentElement.previousSibling;
117
+ while (sibling) {
118
+ if (sibling.nodeType === Node.ELEMENT_NODE &&
119
+ sibling.nodeName === currentElement.nodeName) {
120
+ index++;
121
+ }
122
+ sibling = sibling.previousSibling;
123
+ }
124
+
125
+ const tagName = currentElement.nodeName.toLowerCase();
126
+ const xpathIndex = index > 0 ? `[${index + 1}]` : '';
127
+ segments.unshift(`${tagName}${xpathIndex}`);
128
+
129
+ currentElement = currentElement.parentNode;
130
+ }
131
+
132
+ return segments.join('/');
133
+ }
134
+
135
+ // Helper function to check if element is accepted
136
+ function isElementAccepted(element) {
137
+ const leafElementDenyList = new Set(['svg', 'script', 'style', 'link', 'meta']);
138
+ return !leafElementDenyList.has(element.tagName.toLowerCase());
139
+ }
140
+
141
+ // Helper function to check if element is interactive
142
+ function isInteractiveElement(element) {
143
+ // Base interactive elements and roles
144
+ const interactiveElements = new Set([
145
+ 'a', 'button', 'details', 'embed', 'input', 'label',
146
+ 'menu', 'menuitem', 'object', 'select', 'textarea', 'summary'
147
+ ]);
148
+
149
+ const interactiveRoles = new Set([
150
+ 'button', 'menu', 'menuitem', 'link', 'checkbox', 'radio',
151
+ 'slider', 'tab', 'tabpanel', 'textbox', 'combobox', 'grid',
152
+ 'listbox', 'option', 'progressbar', 'scrollbar', 'searchbox',
153
+ 'switch', 'tree', 'treeitem', 'spinbutton', 'tooltip', 'a-button-inner', 'a-dropdown-button', 'click',
154
+ 'menuitemcheckbox', 'menuitemradio', 'a-button-text', 'button-text', 'button-icon', 'button-icon-only', 'button-text-icon-only', 'dropdown', 'combobox'
155
+ ]);
156
+
157
+ const tagName = element.tagName.toLowerCase();
158
+ const role = element.getAttribute('role');
159
+ const ariaRole = element.getAttribute('aria-role');
160
+ const tabIndex = element.getAttribute('tabindex');
161
+
162
+ // Basic role/attribute checks
163
+ const hasInteractiveRole = interactiveElements.has(tagName) ||
164
+ interactiveRoles.has(role) ||
165
+ interactiveRoles.has(ariaRole) ||
166
+ (tabIndex !== null && tabIndex !== '-1') ||
167
+ element.getAttribute('data-action') === 'a-dropdown-select' ||
168
+ element.getAttribute('data-action') === 'a-dropdown-button';
169
+
170
+ if (hasInteractiveRole) return true;
171
+
172
+ // Get computed style
173
+ const style = window.getComputedStyle(element);
174
+
175
+ // Check if element has click-like styling
176
+ // const hasClickStyling = style.cursor === 'pointer' ||
177
+ // element.style.cursor === 'pointer' ||
178
+ // style.pointerEvents !== 'none';
179
+
180
+ // Check for event listeners
181
+ const hasClickHandler = element.onclick !== null ||
182
+ element.getAttribute('onclick') !== null ||
183
+ element.hasAttribute('ng-click') ||
184
+ element.hasAttribute('@click') ||
185
+ element.hasAttribute('v-on:click');
186
+
187
+ // Helper function to safely get event listeners
188
+ function getEventListeners(el) {
189
+ try {
190
+ // Try to get listeners using Chrome DevTools API
191
+ return window.getEventListeners?.(el) || {};
192
+ } catch (e) {
193
+ // Fallback: check for common event properties
194
+ const listeners = {};
195
+
196
+ // List of common event types to check
197
+ const eventTypes = [
198
+ 'click', 'mousedown', 'mouseup',
199
+ 'touchstart', 'touchend',
200
+ 'keydown', 'keyup', 'focus', 'blur'
201
+ ];
202
+
203
+ for (const type of eventTypes) {
204
+ const handler = el[`on${type}`];
205
+ if (handler) {
206
+ listeners[type] = [{
207
+ listener: handler,
208
+ useCapture: false
209
+ }];
210
+ }
211
+ }
212
+
213
+ return listeners;
214
+ }
215
+ }
216
+
217
+ // Check for click-related events on the element itself
218
+ const listeners = getEventListeners(element);
219
+ const hasClickListeners = listeners && (
220
+ listeners.click?.length > 0 ||
221
+ listeners.mousedown?.length > 0 ||
222
+ listeners.mouseup?.length > 0 ||
223
+ listeners.touchstart?.length > 0 ||
224
+ listeners.touchend?.length > 0
225
+ );
226
+
227
+ // Check for ARIA properties that suggest interactivity
228
+ const hasAriaProps = element.hasAttribute('aria-expanded') ||
229
+ element.hasAttribute('aria-pressed') ||
230
+ element.hasAttribute('aria-selected') ||
231
+ element.hasAttribute('aria-checked');
232
+
233
+ // Check for form-related functionality
234
+ const isFormRelated = element.form !== undefined ||
235
+ element.hasAttribute('contenteditable') ||
236
+ style.userSelect !== 'none';
237
+
238
+ // Check if element is draggable
239
+ const isDraggable = element.draggable ||
240
+ element.getAttribute('draggable') === 'true';
241
+
242
+ return hasAriaProps ||
243
+ // hasClickStyling ||
244
+ hasClickHandler ||
245
+ hasClickListeners ||
246
+ // isFormRelated ||
247
+ isDraggable;
248
+
249
+ }
250
+
251
+ // Helper function to check if element is visible
252
+ function isElementVisible(element) {
253
+ const style = window.getComputedStyle(element);
254
+ return element.offsetWidth > 0 &&
255
+ element.offsetHeight > 0 &&
256
+ style.visibility !== 'hidden' &&
257
+ style.display !== 'none';
258
+ }
259
+
260
+ // Helper function to check if element is the top element at its position
261
+ function isTopElement(element) {
262
+ // Find the correct document context and root element
263
+ let doc = element.ownerDocument;
264
+
265
+ // If we're in an iframe, elements are considered top by default
266
+ if (doc !== window.document) {
267
+ return true;
268
+ }
269
+
270
+ // For shadow DOM, we need to check within its own root context
271
+ const shadowRoot = element.getRootNode();
272
+ if (shadowRoot instanceof ShadowRoot) {
273
+ const rect = element.getBoundingClientRect();
274
+ const point = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
275
+
276
+ try {
277
+ // Use shadow root's elementFromPoint to check within shadow DOM context
278
+ const topEl = shadowRoot.elementFromPoint(point.x, point.y);
279
+ if (!topEl) return false;
280
+
281
+ // Check if the element or any of its parents match our target element
282
+ let current = topEl;
283
+ while (current && current !== shadowRoot) {
284
+ if (current === element) return true;
285
+ current = current.parentElement;
286
+ }
287
+ return false;
288
+ } catch (e) {
289
+ return true; // If we can't determine, consider it visible
290
+ }
291
+ }
292
+
293
+ // Regular DOM elements
294
+ const rect = element.getBoundingClientRect();
295
+ const point = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
296
+
297
+ try {
298
+ const topEl = document.elementFromPoint(point.x, point.y);
299
+ if (!topEl) return false;
300
+
301
+ let current = topEl;
302
+ while (current && current !== document.documentElement) {
303
+ if (current === element) return true;
304
+ current = current.parentElement;
305
+ }
306
+ return false;
307
+ } catch (e) {
308
+ return true;
309
+ }
310
+ }
311
+
312
+ // Helper function to check if text node is visible
313
+ function isTextNodeVisible(textNode) {
314
+ const range = document.createRange();
315
+ range.selectNodeContents(textNode);
316
+ const rect = range.getBoundingClientRect();
317
+
318
+ return rect.width !== 0 &&
319
+ rect.height !== 0 &&
320
+ rect.top >= 0 &&
321
+ rect.top <= window.innerHeight &&
322
+ textNode.parentElement?.checkVisibility({
323
+ checkOpacity: true,
324
+ checkVisibilityCSS: true
325
+ });
326
+ }
327
+
328
+
329
+ // Function to traverse the DOM and create nested JSON
330
+ function buildDomTree(node, parentIframe = null) {
331
+ if (!node) return null;
332
+
333
+ // Special case for text nodes
334
+ if (node.nodeType === Node.TEXT_NODE) {
335
+ const textContent = node.textContent.trim();
336
+ if (textContent && isTextNodeVisible(node)) {
337
+ return {
338
+ type: "TEXT_NODE",
339
+ text: textContent,
340
+ isVisible: true,
341
+ };
342
+ }
343
+ return null;
344
+ }
345
+
346
+ // Check if element is accepted
347
+ if (node.nodeType === Node.ELEMENT_NODE && !isElementAccepted(node)) {
348
+ return null;
349
+ }
350
+
351
+ const nodeData = {
352
+ tagName: node.tagName ? node.tagName.toLowerCase() : null,
353
+ attributes: {},
354
+ xpath: node.nodeType === Node.ELEMENT_NODE ? getXPathTree(node, true) : null,
355
+ children: [],
356
+ };
357
+
358
+ // Copy all attributes if the node is an element
359
+ if (node.nodeType === Node.ELEMENT_NODE && node.attributes) {
360
+ // Use getAttributeNames() instead of directly iterating attributes
361
+ const attributeNames = node.getAttributeNames?.() || [];
362
+ for (const name of attributeNames) {
363
+ nodeData.attributes[name] = node.getAttribute(name);
364
+ }
365
+ }
366
+
367
+ if (node.nodeType === Node.ELEMENT_NODE) {
368
+ const isInteractive = isInteractiveElement(node);
369
+ const isVisible = isElementVisible(node);
370
+ const isTop = isTopElement(node);
371
+
372
+ nodeData.isInteractive = isInteractive;
373
+ nodeData.isVisible = isVisible;
374
+ nodeData.isTopElement = isTop;
375
+
376
+ // Highlight if element meets all criteria and highlighting is enabled
377
+ if (isInteractive && isVisible && isTop) {
378
+ nodeData.highlightIndex = highlightIndex++;
379
+ if (doHighlightElements) {
380
+ highlightElement(node, nodeData.highlightIndex, parentIframe);
381
+ }
382
+ }
383
+ }
384
+
385
+ // Only add iframeContext if we're inside an iframe
386
+ // if (parentIframe) {
387
+ // nodeData.iframeContext = `iframe[src="${parentIframe.src || ''}"]`;
388
+ // }
389
+
390
+ // Only add shadowRoot field if it exists
391
+ if (node.shadowRoot) {
392
+ nodeData.shadowRoot = true;
393
+ }
394
+
395
+ // Handle shadow DOM
396
+ if (node.shadowRoot) {
397
+ const shadowChildren = Array.from(node.shadowRoot.childNodes).map(child =>
398
+ buildDomTree(child, parentIframe)
399
+ );
400
+ nodeData.children.push(...shadowChildren);
401
+ }
402
+
403
+ // Handle iframes
404
+ if (node.tagName === 'IFRAME') {
405
+ try {
406
+ const iframeDoc = node.contentDocument || node.contentWindow.document;
407
+ if (iframeDoc) {
408
+ const iframeChildren = Array.from(iframeDoc.body.childNodes).map(child =>
409
+ buildDomTree(child, node)
410
+ );
411
+ nodeData.children.push(...iframeChildren);
412
+ }
413
+ } catch (e) {
414
+ console.warn('Unable to access iframe:', node);
415
+ }
416
+ } else {
417
+ const children = Array.from(node.childNodes).map(child =>
418
+ buildDomTree(child, parentIframe)
419
+ );
420
+ nodeData.children.push(...children);
421
+ }
422
+
423
+ return nodeData;
424
+ }
425
+
426
+
427
+ return buildDomTree(document.body);
428
+ }
@@ -0,0 +1,112 @@
1
+ import hashlib
2
+ from dataclasses import dataclass
3
+ from typing import Optional
4
+
5
+ from lumivor.dom.history_tree_processor.view import DOMHistoryElement, HashedDomElement
6
+ from lumivor.dom.views import DOMElementNode
7
+
8
+
9
+ class HistoryTreeProcessor:
10
+ """ "
11
+ Operations on the DOM elements
12
+
13
+ @dev be careful - text nodes can change even if elements stay the same
14
+ """
15
+
16
+ @staticmethod
17
+ def convert_dom_element_to_history_element(dom_element: DOMElementNode) -> DOMHistoryElement:
18
+ parent_branch_path = HistoryTreeProcessor._get_parent_branch_path(
19
+ dom_element)
20
+ return DOMHistoryElement(
21
+ dom_element.tag_name,
22
+ dom_element.xpath,
23
+ dom_element.highlight_index,
24
+ parent_branch_path,
25
+ dom_element.attributes,
26
+ dom_element.shadow_root,
27
+ )
28
+
29
+ @staticmethod
30
+ def find_history_element_in_tree(
31
+ dom_history_element: DOMHistoryElement, tree: DOMElementNode
32
+ ) -> Optional[DOMElementNode]:
33
+ hashed_dom_history_element = HistoryTreeProcessor._hash_dom_history_element(
34
+ dom_history_element
35
+ )
36
+
37
+ def process_node(node: DOMElementNode):
38
+ if node.highlight_index is not None:
39
+ hashed_node = HistoryTreeProcessor._hash_dom_element(node)
40
+ if hashed_node == hashed_dom_history_element:
41
+ return node
42
+ for child in node.children:
43
+ if isinstance(child, DOMElementNode):
44
+ result = process_node(child)
45
+ if result is not None:
46
+ return result
47
+ return None
48
+
49
+ return process_node(tree)
50
+
51
+ @staticmethod
52
+ def compare_history_element_and_dom_element(
53
+ dom_history_element: DOMHistoryElement, dom_element: DOMElementNode
54
+ ) -> bool:
55
+ hashed_dom_history_element = HistoryTreeProcessor._hash_dom_history_element(
56
+ dom_history_element
57
+ )
58
+ hashed_dom_element = HistoryTreeProcessor._hash_dom_element(
59
+ dom_element)
60
+
61
+ return hashed_dom_history_element == hashed_dom_element
62
+
63
+ @staticmethod
64
+ def _hash_dom_history_element(dom_history_element: DOMHistoryElement) -> HashedDomElement:
65
+ branch_path_hash = HistoryTreeProcessor._parent_branch_path_hash(
66
+ dom_history_element.entire_parent_branch_path
67
+ )
68
+ attributes_hash = HistoryTreeProcessor._attributes_hash(
69
+ dom_history_element.attributes)
70
+
71
+ return HashedDomElement(branch_path_hash, attributes_hash)
72
+
73
+ @staticmethod
74
+ def _hash_dom_element(dom_element: DOMElementNode) -> HashedDomElement:
75
+ parent_branch_path = HistoryTreeProcessor._get_parent_branch_path(
76
+ dom_element)
77
+ branch_path_hash = HistoryTreeProcessor._parent_branch_path_hash(
78
+ parent_branch_path)
79
+ attributes_hash = HistoryTreeProcessor._attributes_hash(
80
+ dom_element.attributes)
81
+ # text_hash = DomTreeProcessor._text_hash(dom_element)
82
+
83
+ return HashedDomElement(branch_path_hash, attributes_hash)
84
+
85
+ @staticmethod
86
+ def _get_parent_branch_path(dom_element: DOMElementNode) -> list[str]:
87
+ parents: list[DOMElementNode] = []
88
+ current_element: DOMElementNode = dom_element
89
+ while current_element.parent is not None:
90
+ parents.append(current_element)
91
+ current_element = current_element.parent
92
+
93
+ parents.reverse()
94
+
95
+ return [parent.tag_name for parent in parents]
96
+
97
+ @staticmethod
98
+ def _parent_branch_path_hash(parent_branch_path: list[str]) -> str:
99
+ parent_branch_path_string = '/'.join(parent_branch_path)
100
+ return hashlib.sha256(parent_branch_path_string.encode()).hexdigest()
101
+
102
+ @staticmethod
103
+ def _attributes_hash(attributes: dict[str, str]) -> str:
104
+ attributes_string = ''.join(
105
+ f'{key}={value}' for key, value in attributes.items())
106
+ return hashlib.sha256(attributes_string.encode()).hexdigest()
107
+
108
+ @staticmethod
109
+ def _text_hash(dom_element: DOMElementNode) -> str:
110
+ """ """
111
+ text_string = dom_element.get_all_text_till_next_clickable_element()
112
+ return hashlib.sha256(text_string.encode()).hexdigest()
@@ -0,0 +1,33 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+
5
+ @dataclass
6
+ class HashedDomElement:
7
+ """
8
+ Hash of the dom element to be used as a unique identifier
9
+ """
10
+
11
+ branch_path_hash: str
12
+ attributes_hash: str
13
+ # text_hash: str
14
+
15
+
16
+ @dataclass
17
+ class DOMHistoryElement:
18
+ tag_name: str
19
+ xpath: str
20
+ highlight_index: Optional[int]
21
+ entire_parent_branch_path: list[str]
22
+ attributes: dict[str, str]
23
+ shadow_root: bool = False
24
+
25
+ def to_dict(self) -> dict:
26
+ return {
27
+ 'tag_name': self.tag_name,
28
+ 'xpath': self.xpath,
29
+ 'highlight_index': self.highlight_index,
30
+ 'entire_parent_branch_path': self.entire_parent_branch_path,
31
+ 'attributes': self.attributes,
32
+ 'shadow_root': self.shadow_root,
33
+ }
lumivor/dom/service.py ADDED
@@ -0,0 +1,100 @@
1
+ import logging
2
+ from importlib import resources
3
+ from typing import Optional
4
+
5
+ from playwright.async_api import Page
6
+
7
+ from lumivor.dom.views import (
8
+ DOMBaseNode,
9
+ DOMElementNode,
10
+ DOMState,
11
+ DOMTextNode,
12
+ SelectorMap,
13
+ )
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class DomService:
19
+ def __init__(self, page: Page):
20
+ self.page = page
21
+ self.xpath_cache = {}
22
+
23
+ # region - Clickable elements
24
+ async def get_clickable_elements(self, highlight_elements: bool = True) -> DOMState:
25
+ element_tree = await self._build_dom_tree(highlight_elements)
26
+ selector_map = self._create_selector_map(element_tree)
27
+
28
+ return DOMState(element_tree=element_tree, selector_map=selector_map)
29
+
30
+ async def _build_dom_tree(self, highlight_elements: bool) -> DOMElementNode:
31
+ js_code = resources.read_text('lumivor.dom', 'buildDomTree.js')
32
+
33
+ eval_page = await self.page.evaluate(
34
+ js_code, [highlight_elements]
35
+ ) # This is quite big, so be careful
36
+ html_to_dict = self._parse_node(eval_page)
37
+
38
+ if html_to_dict is None or not isinstance(html_to_dict, DOMElementNode):
39
+ raise ValueError('Failed to parse HTML to dictionary')
40
+
41
+ return html_to_dict
42
+
43
+ def _create_selector_map(self, element_tree: DOMElementNode) -> SelectorMap:
44
+ selector_map = {}
45
+
46
+ def process_node(node: DOMBaseNode):
47
+ if isinstance(node, DOMElementNode):
48
+ if node.highlight_index is not None:
49
+ selector_map[node.highlight_index] = node
50
+
51
+ for child in node.children:
52
+ process_node(child)
53
+
54
+ process_node(element_tree)
55
+ return selector_map
56
+
57
+ def _parse_node(
58
+ self,
59
+ node_data: dict,
60
+ parent: Optional[DOMElementNode] = None,
61
+ ) -> Optional[DOMBaseNode]:
62
+ if not node_data:
63
+ return None
64
+
65
+ if node_data.get('type') == 'TEXT_NODE':
66
+ text_node = DOMTextNode(
67
+ text=node_data['text'],
68
+ is_visible=node_data['isVisible'],
69
+ parent=parent,
70
+ )
71
+
72
+ return text_node
73
+
74
+ tag_name = node_data['tagName']
75
+
76
+ element_node = DOMElementNode(
77
+ tag_name=tag_name,
78
+ xpath=node_data['xpath'],
79
+ attributes=node_data.get('attributes', {}),
80
+ children=[], # Initialize empty, will fill later
81
+ is_visible=node_data.get('isVisible', False),
82
+ is_interactive=node_data.get('isInteractive', False),
83
+ is_top_element=node_data.get('isTopElement', False),
84
+ highlight_index=node_data.get('highlightIndex'),
85
+ shadow_root=node_data.get('shadowRoot', False),
86
+ parent=parent,
87
+ )
88
+
89
+ children: list[DOMBaseNode] = []
90
+ for child in node_data.get('children', []):
91
+ if child is not None:
92
+ child_node = self._parse_node(child, parent=element_node)
93
+ if child_node is not None:
94
+ children.append(child_node)
95
+
96
+ element_node.children = children
97
+
98
+ return element_node
99
+
100
+ # endregion