raggiecode 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. Agent/__init__.py +0 -0
  2. Agent/agent.py +891 -0
  3. Agent/chat_history_db.py +1500 -0
  4. Agent/command.py +49 -0
  5. Agent/config.py +46 -0
  6. Agent/effort_levels.py +33 -0
  7. Agent/git_manager.py +727 -0
  8. Agent/tools.py +35 -0
  9. Commands/__init__.py +18 -0
  10. Commands/effort.py +42 -0
  11. Commands/global_todo.py +23 -0
  12. Commands/help.py +22 -0
  13. Commands/reasoning.py +24 -0
  14. Commands/redo.py +11 -0
  15. Commands/reindex.py +27 -0
  16. Commands/shell.py +28 -0
  17. Commands/stream.py +24 -0
  18. Commands/undo.py +13 -0
  19. Commands/unlimited_effort.py +8 -0
  20. Commands/window_size.py +29 -0
  21. RAG/__init__.py +0 -0
  22. RAG/document.py +119 -0
  23. RAG/find.py +408 -0
  24. RAG/graph.py +231 -0
  25. Tools/GetFileCodeStructure.py +43 -0
  26. Tools/GetSymbolSourceCode.py +27 -0
  27. Tools/__init__.py +39 -0
  28. Tools/ask_user.py +102 -0
  29. Tools/dispatch_subagent.py +215 -0
  30. Tools/document.py +35 -0
  31. Tools/edit_symbol.py +250 -0
  32. Tools/fuzzy_search.py +119 -0
  33. Tools/list_dir.py +51 -0
  34. Tools/read.py +49 -0
  35. Tools/read_image.py +75 -0
  36. Tools/remove.py +75 -0
  37. Tools/replace.py +305 -0
  38. Tools/search.py +41 -0
  39. Tools/shell.py +149 -0
  40. Tools/shell_kill.py +87 -0
  41. Tools/temp_background_service.py +113 -0
  42. Tools/todo_list.py +481 -0
  43. Tools/utils.py +116 -0
  44. Tools/view_changes.py +179 -0
  45. Tools/walk_call_tree.py +30 -0
  46. Tools/web_fetch.py +175 -0
  47. Tools/web_search.py +69 -0
  48. Tools/write.py +48 -0
  49. cli.py +111 -0
  50. config/__init__.py +0 -0
  51. config/coder_system_prompt.md +119 -0
  52. config/roles.json +43 -0
  53. config/tools.json +709 -0
  54. indexing/__init__.py +0 -0
  55. indexing/cli.py +128 -0
  56. indexing/code_index_sdk.py +832 -0
  57. indexing/code_indexer.py +1763 -0
  58. indexing/db_schema.py +396 -0
  59. indexing/export_to_json.py +346 -0
  60. indexing/extractors.py +189 -0
  61. indexing/file_utils.py +97 -0
  62. indexing/frontend/__init__.py +0 -0
  63. indexing/frontend/css_extractor.py +195 -0
  64. indexing/frontend/css_parser.py +387 -0
  65. indexing/frontend/css_selector_utils.py +226 -0
  66. indexing/frontend/edit_safety.py +573 -0
  67. indexing/frontend/graph.py +838 -0
  68. indexing/frontend/html_extractor.py +496 -0
  69. indexing/frontend/html_parser.py +314 -0
  70. indexing/frontend/jsx_extractor.py +1204 -0
  71. indexing/frontend/location_lookup.py +247 -0
  72. indexing/frontend/resolver.py +485 -0
  73. indexing/frontend/runtime_resolver.py +862 -0
  74. indexing/frontend/semantic_output.py +705 -0
  75. indexing/frontend/source_location.py +69 -0
  76. indexing/frontend_config.py +72 -0
  77. indexing/frontend_models.py +347 -0
  78. indexing/language_config.py +360 -0
  79. indexing/models.py +284 -0
  80. indexing/node_utils.py +1112 -0
  81. indexing/parse_worker.py +1082 -0
  82. indexing/queries.py +1542 -0
  83. indexing/sdk_examples.py +426 -0
  84. interactive.py +248 -0
  85. raggie.py +673 -0
  86. raggiecode-0.2.1.dist-info/METADATA +944 -0
  87. raggiecode-0.2.1.dist-info/RECORD +93 -0
  88. raggiecode-0.2.1.dist-info/WHEEL +5 -0
  89. raggiecode-0.2.1.dist-info/entry_points.txt +2 -0
  90. raggiecode-0.2.1.dist-info/top_level.txt +10 -0
  91. skills/__init__.py +3 -0
  92. skills/manager.py +114 -0
  93. skills/tool.py +121 -0
@@ -0,0 +1,496 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ HTML semantic extractor.
4
+ Extracts markup elements, scripts, styles, events, and selector matches from parsed HTML.
5
+ """
6
+
7
+ import json
8
+ import re
9
+ from typing import Dict, List, Optional, Tuple
10
+
11
+ from indexing.frontend.html_parser import (
12
+ HTMLDocument, ParsedElement, parse_html, get_all_elements, VOID_TAGS
13
+ )
14
+ from indexing.frontend.source_location import SourceLocation, node_to_location, extract_range
15
+
16
+
17
+ # Event handler attributes (on* attributes)
18
+ EVENT_ATTR_PATTERN = re.compile(r"^on([a-z]+)$", re.IGNORECASE)
19
+
20
+ # Simple CSS selector parsing for static matching
21
+ # Matches: .classname, #id, tagname
22
+ CLASS_SELECTOR_PATTERN = re.compile(r"\.([a-zA-Z_][\w-]*)")
23
+ ID_SELECTOR_PATTERN = re.compile(r"#([a-zA-Z_][\w-]*)")
24
+ TAG_SELECTOR_PATTERN = re.compile(r"^([a-zA-Z][\w-]*)$")
25
+
26
+
27
+ def extract_html_semantics(source_bytes: bytes, config=None) -> dict:
28
+ """Extract all semantic entities from an HTML file.
29
+
30
+ Args:
31
+ source_bytes: Raw HTML file content as bytes.
32
+ config: Optional FrontendConfig instance for extraction limits.
33
+
34
+ Returns:
35
+ dict with keys:
36
+ - markup_elements: list of element dicts
37
+ - frontend_events: list of event handler dicts
38
+ - style_selectors: list of selector dicts (from <style> blocks)
39
+ - style_custom_properties: list of custom property dicts
40
+ - style_custom_property_usages: list of var() usage dicts
41
+ - style_imports: list of stylesheet link dicts
42
+ - style_selector_matches: list of selector→element match dicts
43
+ - frontend_diagnostics: list of diagnostic dicts
44
+ - inline_scripts: list of script dicts (for JS parsing)
45
+ - inline_styles: list of style block dicts (for CSS parsing)
46
+ """
47
+ parse_scripts = config.parse_inline_scripts if config else True
48
+ parse_styles = config.parse_inline_styles if config else True
49
+ include_text = config.include_text_nodes if config else False
50
+ doc = parse_html(source_bytes)
51
+
52
+ result = {
53
+ "markup_elements": [],
54
+ "frontend_events": [],
55
+ "style_selectors": [],
56
+ "style_custom_properties": [],
57
+ "style_custom_property_usages": [],
58
+ "style_imports": [],
59
+ "style_selector_matches": [],
60
+ "frontend_diagnostics": [],
61
+ "inline_scripts": [],
62
+ "inline_styles": [],
63
+ }
64
+
65
+ # Collect all elements (flat list with depth info)
66
+ all_elements = []
67
+ _collect_all_elements(doc.elements, all_elements, parent_idx=None)
68
+
69
+ # Filter out text nodes if config says so
70
+ if not include_text:
71
+ all_elements = [(e, p) for e, p in all_elements if e.element_type != "text"]
72
+
73
+ # Assign indices and build markup_elements
74
+ elem_index_map = {} # id(ParsedElement) -> index in markup_elements list
75
+ for i, (elem, parent_idx) in enumerate(all_elements):
76
+ elem_index_map[id(elem)] = i
77
+ result["markup_elements"].append(_element_to_dict(elem, i, parent_idx))
78
+
79
+ # Extract events from all elements
80
+ for i, (elem, parent_idx) in enumerate(all_elements):
81
+ _extract_events_from_element(elem, i, result["frontend_events"])
82
+
83
+ # Process <style> blocks — extract selectors and custom properties
84
+ style_selectors_list = []
85
+ for style_elem in doc.styles:
86
+ selectors, custom_props, prop_usages = _parse_style_block(style_elem, source_bytes)
87
+ for sel in selectors:
88
+ sel_dict = {
89
+ "selector_text": sel["text"],
90
+ "normalized_selector": sel["text"],
91
+ "selector_type": sel["type"],
92
+ "source_range": sel["location"].to_dict(),
93
+ }
94
+ result["style_selectors"].append(sel_dict)
95
+ style_selectors_list.append((len(result["style_selectors"]) - 1, sel))
96
+
97
+ for cp in custom_props:
98
+ result["style_custom_properties"].append(cp)
99
+
100
+ for pu in prop_usages:
101
+ result["style_custom_property_usages"].append(pu)
102
+
103
+ # Record inline style block for potential CSS parsing
104
+ if parse_styles:
105
+ result["inline_styles"].append({
106
+ "content": style_elem.raw_text or "",
107
+ "location": style_elem.location.to_dict(),
108
+ })
109
+
110
+ # Process <script> elements
111
+ if parse_scripts:
112
+ for script_elem in doc.scripts:
113
+ script_type = script_elem.attributes_dict.get("type", "")
114
+ src = script_elem.attributes_dict.get("src")
115
+
116
+ if src:
117
+ # External script
118
+ result["inline_scripts"].append({
119
+ "type": "external",
120
+ "src": src,
121
+ "script_type": script_type,
122
+ "location": script_elem.location.to_dict(),
123
+ })
124
+ elif script_type in ("", "text/javascript", "application/javascript", "module"):
125
+ # Inline JS script
126
+ result["inline_scripts"].append({
127
+ "type": "inline",
128
+ "content": script_elem.raw_text or "",
129
+ "script_type": script_type,
130
+ "location": script_elem.location.to_dict(),
131
+ })
132
+ elif script_type == "application/ld+json":
133
+ # JSON-LD — skip for semantic extraction
134
+ result["frontend_diagnostics"].append({
135
+ "diagnostic_type": "unsupported_script_type",
136
+ "severity": "unsupported",
137
+ "message": f"Script type '{script_type}' is not parsed for semantics",
138
+ "source_range": script_elem.location.to_dict(),
139
+ })
140
+ else:
141
+ result["frontend_diagnostics"].append({
142
+ "diagnostic_type": "unsupported_script_type",
143
+ "severity": "unsupported",
144
+ "message": f"Script type '{script_type}' is not parsed for semantics",
145
+ "source_range": script_elem.location.to_dict(),
146
+ })
147
+
148
+ # Process <link rel="stylesheet"> as style imports
149
+ for i, (elem, parent_idx) in enumerate(all_elements):
150
+ if elem.tag_name == "link":
151
+ rel = elem.attributes_dict.get("rel", "")
152
+ if "stylesheet" in rel.lower():
153
+ href = elem.attributes_dict.get("href", "")
154
+ is_external = href.startswith(("http://", "https://", "//"))
155
+ result["style_imports"].append({
156
+ "import_path": href,
157
+ "is_external": is_external,
158
+ "source_range": elem.location.to_dict(),
159
+ })
160
+ # Diagnose unresolved local stylesheet
161
+ if not is_external and not href:
162
+ result["frontend_diagnostics"].append({
163
+ "diagnostic_type": "unresolved_stylesheet",
164
+ "severity": "unresolved",
165
+ "message": "Stylesheet link has empty href",
166
+ "source_range": elem.location.to_dict(),
167
+ })
168
+
169
+ # Process inline styles (style="..." attributes) as custom property usages
170
+ for i, (elem, parent_idx) in enumerate(all_elements):
171
+ inline_style = elem.attributes_dict.get("style")
172
+ if inline_style:
173
+ usages = _extract_var_usages(inline_style, elem.location)
174
+ for u in usages:
175
+ u["element_index"] = i
176
+ result["style_custom_property_usages"].append(u)
177
+
178
+ # Compute selector→element matches for static selectors from <style> blocks
179
+ for sel_idx, sel_info in style_selectors_list:
180
+ matches = _compute_selector_matches(sel_info, all_elements)
181
+ for match in matches:
182
+ result["style_selector_matches"].append({
183
+ "selector_index": sel_idx,
184
+ "element_index": match["element_index"],
185
+ "match_type": "static",
186
+ "confidence": "high",
187
+ "source_range": match["location"].to_dict(),
188
+ })
189
+
190
+ # Add diagnostics for parse errors
191
+ for err in doc.errors:
192
+ result["frontend_diagnostics"].append({
193
+ "diagnostic_type": "malformed_html",
194
+ "severity": "recoverable",
195
+ "message": f"Parse error near: {err.get('text', '')[:50]}",
196
+ "source_range": err.get("location"),
197
+ })
198
+
199
+ # Parser recovery: errors occurred but elements were still extracted
200
+ if doc.errors and len(all_elements) > 0:
201
+ result["frontend_diagnostics"].append({
202
+ "diagnostic_type": "parser_recovery",
203
+ "severity": "recoverable",
204
+ "message": f"HTML parser recovered from {len(doc.errors)} error(s), extracted {len(all_elements)} element(s)",
205
+ })
206
+
207
+ # Add diagnostics for unclosed elements
208
+ for i, (elem, parent_idx) in enumerate(all_elements):
209
+ if not elem.has_end_tag and elem.tag_name not in VOID_TAGS and elem.element_type == "element":
210
+ result["frontend_diagnostics"].append({
211
+ "diagnostic_type": "unclosed_element",
212
+ "severity": "recoverable",
213
+ "message": f"Element <{elem.tag_name}> has no closing tag",
214
+ "source_range": elem.location.to_dict(),
215
+ })
216
+
217
+ return result
218
+
219
+
220
+ def _collect_all_elements(elements: List[ParsedElement], result: list, parent_idx: Optional[int]):
221
+ """Iteratively collect all elements as (element, parent_index) tuples."""
222
+ # Stack entries: (element_list, parent_idx)
223
+ stack = [(elements, parent_idx)]
224
+ while stack:
225
+ elem_list, p_idx = stack.pop()
226
+ for elem in elem_list:
227
+ idx = len(result)
228
+ result.append((elem, p_idx))
229
+ child_elements = [c for c in elem.children if c.element_type == "element"]
230
+ if child_elements:
231
+ stack.append((child_elements, idx))
232
+
233
+
234
+ def _element_to_dict(elem: ParsedElement, index: int, parent_index: Optional[int]) -> dict:
235
+ """Convert a ParsedElement to a serializable dict for markup_elements."""
236
+ attrs_dict = {}
237
+ for attr in elem.attributes:
238
+ attrs_dict[attr.name] = attr.value if attr.value is not None else ""
239
+
240
+ return {
241
+ "tag_name": elem.tag_name,
242
+ "element_type": "native",
243
+ "source_range": elem.location.to_dict(),
244
+ "element_id_attr": elem.element_id_attr,
245
+ "static_classes": elem.static_classes if elem.static_classes else None,
246
+ "attributes": attrs_dict if attrs_dict else None,
247
+ "parent_index": parent_index,
248
+ "is_conditional": False,
249
+ "is_repeated": False,
250
+ }
251
+
252
+
253
+ def _extract_events_from_element(elem: ParsedElement, elem_index: int, events: list):
254
+ """Extract inline event handlers from an element's attributes."""
255
+ for attr in elem.attributes:
256
+ match = EVENT_ATTR_PATTERN.match(attr.name)
257
+ if match:
258
+ event_name = match.group(1).lower()
259
+ events.append({
260
+ "element_index": elem_index,
261
+ "event_name": event_name,
262
+ "handler_type": "inline",
263
+ "handler_expression": attr.value or "",
264
+ "resolution_status": "unresolved",
265
+ "source_range": attr.location.to_dict(),
266
+ })
267
+
268
+
269
+ def _parse_style_block(style_elem: ParsedElement, source_bytes: bytes) -> Tuple[list, list, list]:
270
+ """Parse a <style> block and extract selectors, custom properties, and var() usages.
271
+
272
+ Returns:
273
+ (selectors, custom_properties, custom_property_usages)
274
+ """
275
+ css_text = style_elem.raw_text or ""
276
+ if not css_text.strip():
277
+ return [], [], []
278
+
279
+ selectors = []
280
+ custom_properties = []
281
+ property_usages = []
282
+
283
+ # Simple CSS parsing: extract rule sets and custom property definitions
284
+ # This is a basic parser — Phase 4 will add full CSS extraction
285
+ _parse_css_rules(css_text, style_elem.location, selectors, custom_properties, property_usages)
286
+
287
+ return selectors, custom_properties, property_usages
288
+
289
+
290
+ def _parse_css_rules(css_text: str, block_location: SourceLocation,
291
+ selectors: list, custom_properties: list, property_usages: list):
292
+ """Parse CSS text for selectors, custom properties, and var() usages.
293
+
294
+ This is a basic parser for <style> blocks. Full CSS extraction is Phase 4.
295
+ """
296
+ # Remove comments
297
+ css_clean = re.sub(r"/\*.*?\*/", "", css_text, flags=re.DOTALL)
298
+
299
+ # Find rule sets: selector { declarations }
300
+ # Simple approach: find { and } pairs
301
+ pos = 0
302
+ while pos < len(css_clean):
303
+ brace_start = css_clean.find("{", pos)
304
+ if brace_start == -1:
305
+ break
306
+ brace_end = css_clean.find("}", brace_start)
307
+ if brace_end == -1:
308
+ break
309
+
310
+ selector_text = css_clean[pos:brace_start].strip()
311
+ declarations = css_clean[brace_start + 1:brace_end]
312
+
313
+ if selector_text:
314
+ # Split comma-separated selectors
315
+ for sel in selector_text.split(","):
316
+ sel = sel.strip()
317
+ if not sel:
318
+ continue
319
+
320
+ sel_type = _classify_selector(sel)
321
+ # Compute location within the style block, accounting for newlines
322
+ sel_offset = css_clean.find(sel, pos)
323
+ if sel_offset >= 0:
324
+ # Count newlines before sel_offset to get line/column within CSS text
325
+ text_before = css_clean[:sel_offset]
326
+ css_line = text_before.count("\n")
327
+ # Column = distance from last newline to sel_offset
328
+ last_nl = text_before.rfind("\n")
329
+ css_col = sel_offset - (last_nl + 1) if last_nl >= 0 else sel_offset
330
+
331
+ # For the end, count newlines within the selector text itself
332
+ sel_text_newlines = sel.count("\n")
333
+ if sel_text_newlines > 0:
334
+ last_nl_in_sel = sel.rfind("\n")
335
+ end_col = len(sel) - (last_nl_in_sel + 1)
336
+ else:
337
+ end_col = css_col + len(sel)
338
+
339
+ sel_location = SourceLocation(
340
+ start_line=block_location.start_line + css_line,
341
+ start_column=css_col if css_line > 0 else block_location.start_column + css_col,
342
+ end_line=block_location.start_line + css_line + sel_text_newlines,
343
+ end_column=end_col if sel_text_newlines > 0 or css_line > 0 else block_location.start_column + end_col,
344
+ )
345
+ else:
346
+ sel_location = SourceLocation(
347
+ start_line=block_location.start_line,
348
+ start_column=0,
349
+ end_line=block_location.start_line,
350
+ end_column=0,
351
+ )
352
+
353
+ selectors.append({
354
+ "text": sel,
355
+ "type": sel_type,
356
+ "location": sel_location,
357
+ })
358
+
359
+ # Check for custom property definitions in declarations
360
+ decl_block_start = brace_start + 1 # offset in css_clean
361
+ for decl in declarations.split(";"):
362
+ decl = decl.strip()
363
+ if not decl:
364
+ continue
365
+ # Find this declaration's position within css_clean
366
+ decl_offset = css_clean.find(decl, decl_block_start)
367
+ if decl_offset >= 0:
368
+ text_before_decl = css_clean[:decl_offset]
369
+ decl_line = text_before_decl.count("\n")
370
+ last_nl_d = text_before_decl.rfind("\n")
371
+ decl_col = decl_offset - (last_nl_d + 1) if last_nl_d >= 0 else decl_offset
372
+ decl_location = SourceLocation(
373
+ start_line=block_location.start_line + decl_line,
374
+ start_column=decl_col if decl_line > 0 else block_location.start_column + decl_col,
375
+ end_line=block_location.start_line + decl_line,
376
+ end_column=(decl_col + len(decl)) if decl_line > 0 else block_location.start_column + decl_col + len(decl),
377
+ )
378
+ decl_block_start = decl_offset + len(decl)
379
+ else:
380
+ decl_location = sel_location
381
+
382
+ if decl.startswith("--"):
383
+ parts = decl.split(":", 1)
384
+ if len(parts) == 2:
385
+ name = parts[0].strip()
386
+ value = parts[1].strip()
387
+ custom_properties.append({
388
+ "name": name,
389
+ "value": value,
390
+ "scope_selector": sel,
391
+ "source_range": decl_location.to_dict(),
392
+ })
393
+
394
+ # Check for var() usages
395
+ var_usages = _extract_var_usages(decl, decl_location)
396
+ for vu in var_usages:
397
+ vu["selector_text"] = sel
398
+ property_usages.append(vu)
399
+
400
+ pos = brace_end + 1
401
+
402
+
403
+ def _classify_selector(selector: str) -> str:
404
+ """Classify a CSS selector into a type."""
405
+ selector = selector.strip()
406
+ if selector.startswith("#"):
407
+ return "id"
408
+ elif selector.startswith("."):
409
+ return "class"
410
+ elif selector.startswith(":"):
411
+ return "pseudo"
412
+ elif selector.startswith("["):
413
+ return "attribute"
414
+ elif selector.startswith("@"):
415
+ return "at-rule"
416
+ elif TAG_SELECTOR_PATTERN.match(selector):
417
+ return "tag"
418
+ elif "." in selector or "#" in selector or ":" in selector or "[" in selector:
419
+ return "compound"
420
+ else:
421
+ return "unknown"
422
+
423
+
424
+ def _extract_var_usages(text: str, location: SourceLocation) -> list:
425
+ """Extract var(--name) usages from CSS text."""
426
+ usages = []
427
+ var_pattern = re.compile(r"var\(\s*(--[\w-]+)\s*[^)]*\)")
428
+ for match in var_pattern.finditer(text):
429
+ usages.append({
430
+ "property_name": match.group(1),
431
+ "source_range": location.to_dict(),
432
+ })
433
+ return usages
434
+
435
+
436
+ def _compute_selector_matches(sel_info: dict, all_elements: list) -> list:
437
+ """Compute which elements match a given CSS selector (static matching only).
438
+
439
+ Handles: tag selectors, .class selectors, #id selectors, and compound selectors.
440
+ """
441
+ selector = sel_info["text"]
442
+ sel_type = sel_info["type"]
443
+ matches = []
444
+
445
+ if sel_type == "tag":
446
+ tag_name = selector.strip()
447
+ for i, (elem, parent_idx) in enumerate(all_elements):
448
+ if elem.tag_name.lower() == tag_name.lower():
449
+ matches.append({
450
+ "element_index": i,
451
+ "location": elem.location,
452
+ })
453
+
454
+ elif sel_type == "class":
455
+ class_names = CLASS_SELECTOR_PATTERN.findall(selector)
456
+ for i, (elem, parent_idx) in enumerate(all_elements):
457
+ if all(cn in elem.static_classes for cn in class_names):
458
+ matches.append({
459
+ "element_index": i,
460
+ "location": elem.location,
461
+ })
462
+
463
+ elif sel_type == "id":
464
+ id_name = ID_SELECTOR_PATTERN.findall(selector)
465
+ if id_name:
466
+ for i, (elem, parent_idx) in enumerate(all_elements):
467
+ if elem.element_id_attr == id_name[0]:
468
+ matches.append({
469
+ "element_index": i,
470
+ "location": elem.location,
471
+ })
472
+
473
+ elif sel_type == "compound":
474
+ # Parse compound selector: e.g., "div.container#main"
475
+ tag = None
476
+ classes = CLASS_SELECTOR_PATTERN.findall(selector)
477
+ ids = ID_SELECTOR_PATTERN.findall(selector)
478
+
479
+ # Extract tag name if present
480
+ compound_tag_match = re.match(r"^([a-zA-Z][\w-]*)", selector)
481
+ if compound_tag_match:
482
+ tag = compound_tag_match.group(1)
483
+
484
+ for i, (elem, parent_idx) in enumerate(all_elements):
485
+ if tag and elem.tag_name.lower() != tag.lower():
486
+ continue
487
+ if classes and not all(cn in elem.static_classes for cn in classes):
488
+ continue
489
+ if ids and elem.element_id_attr not in ids:
490
+ continue
491
+ matches.append({
492
+ "element_index": i,
493
+ "location": elem.location,
494
+ })
495
+
496
+ return matches