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.
- Agent/__init__.py +0 -0
- Agent/agent.py +891 -0
- Agent/chat_history_db.py +1500 -0
- Agent/command.py +49 -0
- Agent/config.py +46 -0
- Agent/effort_levels.py +33 -0
- Agent/git_manager.py +727 -0
- Agent/tools.py +35 -0
- Commands/__init__.py +18 -0
- Commands/effort.py +42 -0
- Commands/global_todo.py +23 -0
- Commands/help.py +22 -0
- Commands/reasoning.py +24 -0
- Commands/redo.py +11 -0
- Commands/reindex.py +27 -0
- Commands/shell.py +28 -0
- Commands/stream.py +24 -0
- Commands/undo.py +13 -0
- Commands/unlimited_effort.py +8 -0
- Commands/window_size.py +29 -0
- RAG/__init__.py +0 -0
- RAG/document.py +119 -0
- RAG/find.py +408 -0
- RAG/graph.py +231 -0
- Tools/GetFileCodeStructure.py +43 -0
- Tools/GetSymbolSourceCode.py +27 -0
- Tools/__init__.py +39 -0
- Tools/ask_user.py +102 -0
- Tools/dispatch_subagent.py +215 -0
- Tools/document.py +35 -0
- Tools/edit_symbol.py +250 -0
- Tools/fuzzy_search.py +119 -0
- Tools/list_dir.py +51 -0
- Tools/read.py +49 -0
- Tools/read_image.py +75 -0
- Tools/remove.py +75 -0
- Tools/replace.py +305 -0
- Tools/search.py +41 -0
- Tools/shell.py +149 -0
- Tools/shell_kill.py +87 -0
- Tools/temp_background_service.py +113 -0
- Tools/todo_list.py +481 -0
- Tools/utils.py +116 -0
- Tools/view_changes.py +179 -0
- Tools/walk_call_tree.py +30 -0
- Tools/web_fetch.py +175 -0
- Tools/web_search.py +69 -0
- Tools/write.py +48 -0
- cli.py +111 -0
- config/__init__.py +0 -0
- config/coder_system_prompt.md +119 -0
- config/roles.json +43 -0
- config/tools.json +709 -0
- indexing/__init__.py +0 -0
- indexing/cli.py +128 -0
- indexing/code_index_sdk.py +832 -0
- indexing/code_indexer.py +1763 -0
- indexing/db_schema.py +396 -0
- indexing/export_to_json.py +346 -0
- indexing/extractors.py +189 -0
- indexing/file_utils.py +97 -0
- indexing/frontend/__init__.py +0 -0
- indexing/frontend/css_extractor.py +195 -0
- indexing/frontend/css_parser.py +387 -0
- indexing/frontend/css_selector_utils.py +226 -0
- indexing/frontend/edit_safety.py +573 -0
- indexing/frontend/graph.py +838 -0
- indexing/frontend/html_extractor.py +496 -0
- indexing/frontend/html_parser.py +314 -0
- indexing/frontend/jsx_extractor.py +1204 -0
- indexing/frontend/location_lookup.py +247 -0
- indexing/frontend/resolver.py +485 -0
- indexing/frontend/runtime_resolver.py +862 -0
- indexing/frontend/semantic_output.py +705 -0
- indexing/frontend/source_location.py +69 -0
- indexing/frontend_config.py +72 -0
- indexing/frontend_models.py +347 -0
- indexing/language_config.py +360 -0
- indexing/models.py +284 -0
- indexing/node_utils.py +1112 -0
- indexing/parse_worker.py +1082 -0
- indexing/queries.py +1542 -0
- indexing/sdk_examples.py +426 -0
- interactive.py +248 -0
- raggie.py +673 -0
- raggiecode-0.2.1.dist-info/METADATA +944 -0
- raggiecode-0.2.1.dist-info/RECORD +93 -0
- raggiecode-0.2.1.dist-info/WHEEL +5 -0
- raggiecode-0.2.1.dist-info/entry_points.txt +2 -0
- raggiecode-0.2.1.dist-info/top_level.txt +10 -0
- skills/__init__.py +3 -0
- skills/manager.py +114 -0
- 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
|