-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcdp_parser.py
More file actions
211 lines (170 loc) · 8.13 KB
/
Copy pathcdp_parser.py
File metadata and controls
211 lines (170 loc) · 8.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
import os
import json
def parse_cdp_snapshot(json_path):
if not os.path.exists(json_path):
print(f"❌ File not found at: {json_path}")
return
print(f"📂 Reading snapshot from {json_path}...")
with open(json_path, "r", encoding="utf-8") as f:
data = json.load(f)
strings = data.get("strings", [])
documents = data.get("documents", [])
print(f"📊 Total Documents (Frames): {len(documents)}")
print(f"📊 String Pool Size: {len(strings)}")
def get_str(idx):
if idx == -1 or idx is None:
return ""
if 0 <= idx < len(strings):
return strings[idx]
return ""
parsed_elements = []
# We will query these computed styles from the snapshot:
# computedStyles = ['display', 'visibility', 'opacity', 'pointer-events']
computed_style_keys = ['display', 'visibility', 'opacity', 'pointer-events']
for doc_idx, doc in enumerate(documents):
print(f"\n📄 Processing Document {doc_idx + 1}...")
nodes = doc.get("nodes", {})
layout = doc.get("layout", {})
node_names = nodes.get("nodeName", [])
node_types = nodes.get("nodeType", [])
node_values = nodes.get("nodeValue", [])
parent_indices = nodes.get("parentIndex", [])
attributes_list = nodes.get("attributes", [])
node_count = len(node_names)
print(f" - Total DOM Nodes: {node_count}")
# Build child mapping for traversing the tree
child_map = {}
for child_idx, parent_idx in enumerate(parent_indices):
if parent_idx != -1:
child_map.setdefault(parent_idx, []).append(child_idx)
# Map nodeIndex to layout properties (bounds & styles)
node_indices = layout.get("nodeIndex", [])
bounds = layout.get("bounds", [])
styles = layout.get("styles", [])
node_layout_map = {}
for L, node_idx in enumerate(node_indices):
node_layout_map[node_idx] = {
"bounds": bounds[L] if L < len(bounds) else [0, 0, 0, 0],
"styles": styles[L] if L < len(styles) else []
}
# Recursive text extractor for an element
def extract_text(idx):
n_type = node_types[idx]
if n_type == 3: # Text node
return get_str(node_values[idx]).strip()
# For element nodes, concatenate all child text nodes
parts = []
for child_idx in child_map.get(idx, []):
child_txt = extract_text(child_idx)
if child_txt:
parts.append(child_txt)
return " ".join(parts)
# Helper to get all attributes for a node
def get_attributes(idx):
attrs = {}
if idx < len(attributes_list):
pairs = attributes_list[idx]
for i in range(0, len(pairs), 2):
if i + 1 < len(pairs):
name = get_str(pairs[i])
val = get_str(pairs[i+1])
attrs[name] = val
return attrs
interactive_count = 0
for idx in range(node_count):
tag_name = get_str(node_names[idx]).upper()
node_type = node_types[idx]
if node_type != 1: # Only parse Element nodes (type = 1)
continue
attrs = get_attributes(idx)
role = attrs.get("role", "").lower()
tabindex = attrs.get("tabindex", "")
# Determine if this element is interactive
is_interactive = False
# 1. Standard Interactive Tag Names
if tag_name in ["BUTTON", "A", "INPUT", "TEXTAREA", "SELECT"]:
is_interactive = True
# 2. ARIA role="button" or role="link"
elif role in ["button", "link", "checkbox", "radio", "textbox"]:
is_interactive = True
# 3. Has tabindex (excluding negative ones like -1)
elif tabindex and tabindex != "-1":
is_interactive = True
if not is_interactive:
continue
# Now let's extract visibility and geometry
layout_data = node_layout_map.get(idx)
if not layout_data:
# Element has no layout node (it is hidden or display:none)
continue
x, y, w, h = layout_data["bounds"]
# 1. Filter out elements with zero size
if w <= 0 or h <= 0:
continue
# 2. Filter out off-screen hidden elements (e.g. left: -9999px or top: -9999px)
# We allow minor overflow but filter out anything significantly negative.
# We also filter out elements positioned too far to the right of the document content width.
content_width = doc.get("contentWidth", 1920)
if x < -20 or y < -20 or x > (content_width + 50):
continue
# Extract computed styles
style_indices = layout_data["styles"]
resolved_styles = {}
for s_idx, key in enumerate(computed_style_keys):
if s_idx < len(style_indices):
resolved_styles[key] = get_str(style_indices[s_idx])
else:
resolved_styles[key] = ""
# Check visibility computed styles
display = resolved_styles.get("display", "")
visibility = resolved_styles.get("visibility", "")
opacity_str = resolved_styles.get("opacity", "1")
pointer_events = resolved_styles.get("pointer-events", "")
try:
opacity = float(opacity_str)
except ValueError:
opacity = 1.0
# 3. Filter out hidden display/visibility or low opacity (e.g. opacity < 0.1 trap buttons)
if display == "none" or visibility == "hidden" or opacity < 0.1:
continue
# 4. Filter out elements that cannot receive clicks / pointer events
if pointer_events == "none":
continue
# Gather text description
text = extract_text(idx)
# Gather other hints
placeholder = attrs.get("placeholder", "")
aria_label = attrs.get("aria-label", "")
el_id = attrs.get("id", "")
el_class = attrs.get("class", "")
element_info = {
"nodeIndex": idx,
"tag": tag_name,
"text": text,
"aria_label": aria_label,
"placeholder": placeholder,
"id": el_id,
"class": el_class,
"role": role,
"bounds": {"x": x, "y": y, "width": w, "height": h},
"styles": resolved_styles
}
parsed_elements.append(element_info)
interactive_count += 1
print(f" - Found {interactive_count} visible, interactive elements.")
# Save to clean JSON file
output_file = os.path.join(os.path.dirname(json_path), "parsed_elements.json")
print(f"\n💾 Saving parsed elements to: {output_file}...")
with open(output_file, "w", encoding="utf-8") as f:
json.dump(parsed_elements, f, indent=2)
# Print summary list of parsed elements
print(f"\n🎯 --- VISIBLE BUTTONS & INTERACTIVE ELEMENTS ({len(parsed_elements)}) ---")
for i, el in enumerate(parsed_elements):
text_summary = el["text"] or el["aria_label"] or el["placeholder"] or "[NO TEXT]"
if len(text_summary) > 60:
text_summary = text_summary[:60] + "..."
print(f"[{i+1}] <{el['tag']}> id={el['id']} text='{text_summary}' bounds={el['bounds']}")
if __name__ == "__main__":
current_dir = os.path.dirname(os.path.abspath(__file__))
json_path = os.path.join(current_dir, "cdp_snapshot.json")
parse_cdp_snapshot(json_path)