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,485 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Cross-file resolution utilities for frontend semantic indexing.
|
|
4
|
+
|
|
5
|
+
Provides path normalization, alias resolution, and extension inference
|
|
6
|
+
for resolving file-path references (@import, <script src>, <link href>,
|
|
7
|
+
CSS module imports) during inline indexing.
|
|
8
|
+
|
|
9
|
+
These functions are called inline during _insert_frontend_data(), not
|
|
10
|
+
as a post-indexing pass.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Dict, Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Extensions to try when a path has no extension
|
|
20
|
+
CANDIDATE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".css", ".html", ".mjs", ".cjs"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def normalize_import_path(import_path: str, source_file_path: str, root_dir: str,
|
|
24
|
+
aliases: Optional[Dict[str, str]] = None) -> str:
|
|
25
|
+
"""Resolve a relative or aliased import path to a normalized project-relative path.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
import_path: The raw import path (e.g. "./Button", "../styles/global.css", "@/components/Card").
|
|
29
|
+
source_file_path: Absolute path of the file making the import.
|
|
30
|
+
root_dir: Absolute path of the project root directory.
|
|
31
|
+
aliases: Optional pre-loaded path aliases from load_path_aliases(). If None, will load lazily.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Normalized project-relative path (e.g. "src/components/Button.tsx").
|
|
35
|
+
For external URLs (http://, https://, //), returns the original path unchanged.
|
|
36
|
+
"""
|
|
37
|
+
# External URLs — return as-is
|
|
38
|
+
if import_path.startswith(("http://", "https://", "//")):
|
|
39
|
+
return import_path
|
|
40
|
+
|
|
41
|
+
root = Path(root_dir).resolve()
|
|
42
|
+
source_dir = Path(source_file_path).resolve().parent
|
|
43
|
+
|
|
44
|
+
# Handle path aliases (@/, ~/, etc.)
|
|
45
|
+
if aliases is None:
|
|
46
|
+
aliases = load_path_aliases(root_dir)
|
|
47
|
+
resolved = resolve_path_alias(import_path, aliases, root_dir)
|
|
48
|
+
if resolved != import_path:
|
|
49
|
+
# Alias was applied — resolve relative to root
|
|
50
|
+
candidate = root / resolved
|
|
51
|
+
elif import_path.startswith("/"):
|
|
52
|
+
# Root-relative path
|
|
53
|
+
candidate = root / import_path.lstrip("/")
|
|
54
|
+
else:
|
|
55
|
+
# Relative path — resolve against source file directory
|
|
56
|
+
candidate = (source_dir / import_path).resolve()
|
|
57
|
+
|
|
58
|
+
# Try to infer extension if the path doesn't exist as-is
|
|
59
|
+
if not candidate.exists():
|
|
60
|
+
candidate = infer_extension_path(candidate)
|
|
61
|
+
|
|
62
|
+
# Return project-relative path
|
|
63
|
+
try:
|
|
64
|
+
return str(candidate.relative_to(root))
|
|
65
|
+
except ValueError:
|
|
66
|
+
# Path is outside root — return best-effort normalized
|
|
67
|
+
return str(candidate)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def infer_extension_path(base_path: Path) -> Path:
|
|
71
|
+
"""Try to find a file by appending common extensions to the path.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
base_path: Path without an extension (or with one that doesn't exist).
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
The first matching path with an extension, or the original path if none match.
|
|
78
|
+
"""
|
|
79
|
+
# If the path already has an extension and exists, return it
|
|
80
|
+
if base_path.suffix and base_path.exists():
|
|
81
|
+
return base_path
|
|
82
|
+
|
|
83
|
+
# If the path has an extension but doesn't exist, try index files in that directory
|
|
84
|
+
if base_path.suffix:
|
|
85
|
+
# e.g. "./components" might be a directory with index.tsx
|
|
86
|
+
if base_path.with_suffix("").is_dir():
|
|
87
|
+
for ext in CANDIDATE_EXTENSIONS:
|
|
88
|
+
index_file = base_path.with_suffix("") / f"index{ext}"
|
|
89
|
+
if index_file.exists():
|
|
90
|
+
return index_file
|
|
91
|
+
return base_path
|
|
92
|
+
|
|
93
|
+
# No extension — try each candidate
|
|
94
|
+
for ext in CANDIDATE_EXTENSIONS:
|
|
95
|
+
candidate = base_path.with_suffix(ext)
|
|
96
|
+
if candidate.exists():
|
|
97
|
+
return candidate
|
|
98
|
+
|
|
99
|
+
# Try as a directory with index file
|
|
100
|
+
if base_path.is_dir():
|
|
101
|
+
for ext in CANDIDATE_EXTENSIONS:
|
|
102
|
+
index_file = base_path / f"index{ext}"
|
|
103
|
+
if index_file.exists():
|
|
104
|
+
return index_file
|
|
105
|
+
|
|
106
|
+
return base_path
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def infer_extension(base_path: str) -> str:
|
|
110
|
+
"""Try to infer the file extension for a base path string.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
base_path: Path string without an extension.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The first extension (including dot) that matches an existing file,
|
|
117
|
+
or the first candidate extension if none match.
|
|
118
|
+
"""
|
|
119
|
+
p = Path(base_path)
|
|
120
|
+
for ext in CANDIDATE_EXTENSIONS:
|
|
121
|
+
candidate = p.with_suffix(ext)
|
|
122
|
+
if candidate.exists():
|
|
123
|
+
return ext
|
|
124
|
+
return CANDIDATE_EXTENSIONS[0]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def load_path_aliases(root_dir: str) -> Dict[str, str]:
|
|
128
|
+
"""Load path aliases from tsconfig.json or jsconfig.json.
|
|
129
|
+
|
|
130
|
+
Reads compilerOptions.paths from the config file and returns a mapping
|
|
131
|
+
of alias patterns to resolved paths.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
root_dir: Project root directory path.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Dict mapping alias patterns (e.g. "@/*") to resolved path prefixes (e.g. "src/*").
|
|
138
|
+
Empty dict if no config file found or no paths configured.
|
|
139
|
+
"""
|
|
140
|
+
root = Path(root_dir)
|
|
141
|
+
|
|
142
|
+
for config_name in ("tsconfig.json", "jsconfig.json"):
|
|
143
|
+
config_path = root / config_name
|
|
144
|
+
if config_path.exists():
|
|
145
|
+
try:
|
|
146
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
147
|
+
config = json.load(f)
|
|
148
|
+
paths = config.get("compilerOptions", {}).get("paths", {})
|
|
149
|
+
if paths:
|
|
150
|
+
# Also get baseUrl for resolution
|
|
151
|
+
base_url = config.get("compilerOptions", {}).get("baseUrl", "")
|
|
152
|
+
result = {}
|
|
153
|
+
for alias, targets in paths.items():
|
|
154
|
+
if targets:
|
|
155
|
+
target = targets[0]
|
|
156
|
+
if base_url:
|
|
157
|
+
target = str(Path(base_url) / target)
|
|
158
|
+
result[alias] = target
|
|
159
|
+
return result
|
|
160
|
+
except (json.JSONDecodeError, OSError):
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
return {}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def resolve_path_alias(import_path: str, aliases: Dict[str, str], root_dir: str) -> str:
|
|
167
|
+
"""Apply alias mapping to an import path.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
import_path: The raw import path (e.g. "@/components/Button").
|
|
171
|
+
aliases: Alias mapping from load_path_aliases().
|
|
172
|
+
root_dir: Project root directory path.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Resolved path if alias matched, or the original import_path if no alias matched.
|
|
176
|
+
"""
|
|
177
|
+
for alias_pattern, target_pattern in aliases.items():
|
|
178
|
+
# Handle wildcard patterns like "@/*"
|
|
179
|
+
if alias_pattern.endswith("/*"):
|
|
180
|
+
prefix = alias_pattern[:-2] # "@/"
|
|
181
|
+
if import_path.startswith(prefix):
|
|
182
|
+
suffix = import_path[len(prefix):]
|
|
183
|
+
resolved = target_pattern.rstrip("*").rstrip("/") + "/" + suffix.lstrip("/")
|
|
184
|
+
return resolved
|
|
185
|
+
elif alias_pattern == import_path:
|
|
186
|
+
return target_pattern
|
|
187
|
+
else:
|
|
188
|
+
# Exact prefix match without wildcard
|
|
189
|
+
if import_path.startswith(alias_pattern + "/"):
|
|
190
|
+
suffix = import_path[len(alias_pattern):]
|
|
191
|
+
resolved = target_pattern + suffix
|
|
192
|
+
return resolved
|
|
193
|
+
|
|
194
|
+
return import_path
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class FrontendResolver:
|
|
198
|
+
"""Post-indexing cross-file resolution pass.
|
|
199
|
+
|
|
200
|
+
Called after all files have been indexed to refresh cross-file relationships
|
|
201
|
+
that couldn't be resolved during inline insertion (e.g. because the target
|
|
202
|
+
file hadn't been indexed yet, or was indexed in the wrong order).
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
def __init__(self, conn, root_dir):
|
|
206
|
+
self.conn = conn
|
|
207
|
+
self.root_dir = root_dir
|
|
208
|
+
self.aliases = load_path_aliases(root_dir)
|
|
209
|
+
|
|
210
|
+
def resolve_all(self):
|
|
211
|
+
"""Run all cross-file resolution passes.
|
|
212
|
+
|
|
213
|
+
Resolves:
|
|
214
|
+
1. render_relationships.child_component_id by name lookup
|
|
215
|
+
2. style_imports.resolved_file_id by path lookup
|
|
216
|
+
3. style_custom_property_usages.resolved_property_id by name lookup
|
|
217
|
+
4. frontend_events.handler_symbol_id by expression lookup
|
|
218
|
+
5. style_selector_matches — recompute from selectors and elements
|
|
219
|
+
"""
|
|
220
|
+
self._resolve_render_relationships()
|
|
221
|
+
self._resolve_style_imports()
|
|
222
|
+
self._resolve_custom_property_usages()
|
|
223
|
+
self._resolve_event_handlers()
|
|
224
|
+
self._resolve_selector_matches()
|
|
225
|
+
self.conn.commit()
|
|
226
|
+
|
|
227
|
+
def _resolve_render_relationships(self):
|
|
228
|
+
"""Resolve render_relationships.child_component_id by looking up child_component_name."""
|
|
229
|
+
cursor = self.conn.cursor()
|
|
230
|
+
cursor.execute(
|
|
231
|
+
"SELECT rr.id, rr.child_component_name FROM render_relationships rr "
|
|
232
|
+
"WHERE rr.child_component_id IS NULL AND rr.child_component_name IS NOT NULL"
|
|
233
|
+
)
|
|
234
|
+
rows = cursor.fetchall()
|
|
235
|
+
if not rows:
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
# Batch-load all component name -> id mappings
|
|
239
|
+
cursor.execute("SELECT name, id FROM frontend_components")
|
|
240
|
+
component_map = {}
|
|
241
|
+
for name, comp_id in cursor.fetchall():
|
|
242
|
+
if name not in component_map:
|
|
243
|
+
component_map[name] = comp_id
|
|
244
|
+
|
|
245
|
+
batch = []
|
|
246
|
+
for row_id, child_name in rows:
|
|
247
|
+
comp_id = component_map.get(child_name)
|
|
248
|
+
if comp_id:
|
|
249
|
+
batch.append((comp_id, row_id))
|
|
250
|
+
if batch:
|
|
251
|
+
cursor.executemany(
|
|
252
|
+
"UPDATE render_relationships SET child_component_id = ? WHERE id = ?",
|
|
253
|
+
batch
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def _resolve_style_imports(self):
|
|
257
|
+
"""Resolve style_imports.resolved_file_id by looking up import paths."""
|
|
258
|
+
cursor = self.conn.cursor()
|
|
259
|
+
cursor.execute(
|
|
260
|
+
"SELECT si.id, si.import_path, f.absolute_path "
|
|
261
|
+
"FROM style_imports si "
|
|
262
|
+
"JOIN files f ON si.file_id = f.id "
|
|
263
|
+
"WHERE si.resolved_file_id IS NULL AND si.is_external = 0"
|
|
264
|
+
)
|
|
265
|
+
rows = cursor.fetchall()
|
|
266
|
+
if not rows:
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
# Batch-load all file path -> id mappings
|
|
270
|
+
cursor.execute("SELECT path, id FROM files")
|
|
271
|
+
file_map = {path: fid for path, fid in cursor.fetchall()}
|
|
272
|
+
|
|
273
|
+
batch = []
|
|
274
|
+
for row_id, import_path, source_file_path in rows:
|
|
275
|
+
resolved_path = normalize_import_path(
|
|
276
|
+
import_path, source_file_path, self.root_dir, self.aliases
|
|
277
|
+
)
|
|
278
|
+
file_id = file_map.get(resolved_path)
|
|
279
|
+
if file_id:
|
|
280
|
+
batch.append((file_id, row_id))
|
|
281
|
+
if batch:
|
|
282
|
+
cursor.executemany(
|
|
283
|
+
"UPDATE style_imports SET resolved_file_id = ? WHERE id = ?",
|
|
284
|
+
batch
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def _resolve_custom_property_usages(self):
|
|
288
|
+
"""Resolve style_custom_property_usages.resolved_property_id by name lookup."""
|
|
289
|
+
cursor = self.conn.cursor()
|
|
290
|
+
cursor.execute(
|
|
291
|
+
"SELECT id, property_name FROM style_custom_property_usages "
|
|
292
|
+
"WHERE resolved_property_id IS NULL"
|
|
293
|
+
)
|
|
294
|
+
rows = cursor.fetchall()
|
|
295
|
+
if not rows:
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
# Batch-load all property name -> id mappings
|
|
299
|
+
cursor.execute("SELECT name, id FROM style_custom_properties")
|
|
300
|
+
prop_map = {}
|
|
301
|
+
for name, prop_id in cursor.fetchall():
|
|
302
|
+
if name not in prop_map:
|
|
303
|
+
prop_map[name] = prop_id
|
|
304
|
+
|
|
305
|
+
batch = []
|
|
306
|
+
for row_id, prop_name in rows:
|
|
307
|
+
prop_id = prop_map.get(prop_name)
|
|
308
|
+
if prop_id:
|
|
309
|
+
batch.append((prop_id, row_id))
|
|
310
|
+
if batch:
|
|
311
|
+
cursor.executemany(
|
|
312
|
+
"UPDATE style_custom_property_usages SET resolved_property_id = ? WHERE id = ?",
|
|
313
|
+
batch
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def _resolve_event_handlers(self):
|
|
317
|
+
"""Resolve frontend_events.handler_symbol_id by looking up handler expressions."""
|
|
318
|
+
cursor = self.conn.cursor()
|
|
319
|
+
cursor.execute(
|
|
320
|
+
"SELECT fe.id, fe.handler_expression, fe.file_id "
|
|
321
|
+
"FROM frontend_events fe "
|
|
322
|
+
"WHERE fe.handler_symbol_id IS NULL "
|
|
323
|
+
"AND fe.handler_type = 'inline' "
|
|
324
|
+
"AND fe.handler_expression IS NOT NULL"
|
|
325
|
+
)
|
|
326
|
+
rows = cursor.fetchall()
|
|
327
|
+
if not rows:
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
# Group unresolved events by file_id for batch function lookups
|
|
331
|
+
events_by_file = {}
|
|
332
|
+
for row_id, handler_expr, file_id in rows:
|
|
333
|
+
expr = handler_expr.strip()
|
|
334
|
+
func_name = expr.rstrip("()").strip()
|
|
335
|
+
if func_name and "(" not in func_name:
|
|
336
|
+
events_by_file.setdefault(file_id, []).append((row_id, func_name))
|
|
337
|
+
|
|
338
|
+
batch = []
|
|
339
|
+
for file_id, events in events_by_file.items():
|
|
340
|
+
func_names = set(fn for _, fn in events)
|
|
341
|
+
placeholders = ','.join('?' * len(func_names))
|
|
342
|
+
cursor.execute(
|
|
343
|
+
f"SELECT name, id FROM functions WHERE file_id = ? AND name IN ({placeholders})",
|
|
344
|
+
[file_id] + list(func_names)
|
|
345
|
+
)
|
|
346
|
+
func_map = {name: fid for name, fid in cursor.fetchall()}
|
|
347
|
+
for row_id, func_name in events:
|
|
348
|
+
func_id = func_map.get(func_name)
|
|
349
|
+
if func_id:
|
|
350
|
+
batch.append((func_id, 'resolved', row_id))
|
|
351
|
+
|
|
352
|
+
if batch:
|
|
353
|
+
cursor.executemany(
|
|
354
|
+
"UPDATE frontend_events SET handler_symbol_id = ?, resolution_status = ? WHERE id = ?",
|
|
355
|
+
batch
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
def _resolve_selector_matches(self):
|
|
359
|
+
"""Recompute style_selector_matches from selectors and elements.
|
|
360
|
+
|
|
361
|
+
Clears all existing matches and recomputes them by matching selectors
|
|
362
|
+
against markup elements across all files.
|
|
363
|
+
|
|
364
|
+
Optimized: pre-decodes element JSON once, groups selectors by type to
|
|
365
|
+
reduce cross-product work, and batch-inserts results.
|
|
366
|
+
"""
|
|
367
|
+
cursor = self.conn.cursor()
|
|
368
|
+
cursor.execute("DELETE FROM style_selector_matches")
|
|
369
|
+
|
|
370
|
+
cursor.execute(
|
|
371
|
+
"SELECT ss.id, ss.selector_text, ss.selector_type "
|
|
372
|
+
"FROM style_selectors ss"
|
|
373
|
+
)
|
|
374
|
+
selectors = cursor.fetchall()
|
|
375
|
+
|
|
376
|
+
cursor.execute(
|
|
377
|
+
"SELECT me.id, me.tag_name, me.element_id_attr, me.static_classes "
|
|
378
|
+
"FROM markup_elements me "
|
|
379
|
+
"WHERE me.element_type IN ('element', 'custom_component', 'native')"
|
|
380
|
+
)
|
|
381
|
+
elements = cursor.fetchall()
|
|
382
|
+
|
|
383
|
+
# Pre-decode static_classes JSON once per element
|
|
384
|
+
decoded_elements = []
|
|
385
|
+
for elem_id, tag_name, elem_id_attr, static_classes in elements:
|
|
386
|
+
classes = json.loads(static_classes) if static_classes else []
|
|
387
|
+
decoded_elements.append((elem_id, tag_name, elem_id_attr, classes))
|
|
388
|
+
|
|
389
|
+
# Group selectors by type for targeted matching
|
|
390
|
+
selectors_by_type = {}
|
|
391
|
+
for sel_id, sel_text, sel_type in selectors:
|
|
392
|
+
selectors_by_type.setdefault(sel_type, []).append((sel_id, sel_text))
|
|
393
|
+
|
|
394
|
+
# Build lookup indexes from elements
|
|
395
|
+
elements_by_tag = {}
|
|
396
|
+
elements_by_id_attr = {}
|
|
397
|
+
elements_by_class = {}
|
|
398
|
+
for elem_id, tag_name, elem_id_attr, classes in decoded_elements:
|
|
399
|
+
tag_lower = tag_name.lower()
|
|
400
|
+
elements_by_tag.setdefault(tag_lower, []).append(elem_id)
|
|
401
|
+
if elem_id_attr:
|
|
402
|
+
elements_by_id_attr.setdefault(elem_id_attr, []).append(elem_id)
|
|
403
|
+
for cls in classes:
|
|
404
|
+
elements_by_class.setdefault(cls, []).append(elem_id)
|
|
405
|
+
|
|
406
|
+
batch = []
|
|
407
|
+
# Match class selectors using class index
|
|
408
|
+
for sel_id, sel_text in selectors_by_type.get("class", []):
|
|
409
|
+
target_class = sel_text.lstrip(".")
|
|
410
|
+
matched_ids = elements_by_class.get(target_class, [])
|
|
411
|
+
for elem_id in matched_ids:
|
|
412
|
+
batch.append((sel_id, elem_id, 'static', 'high'))
|
|
413
|
+
|
|
414
|
+
# Match id selectors using id index
|
|
415
|
+
for sel_id, sel_text in selectors_by_type.get("id", []):
|
|
416
|
+
target_id = sel_text.lstrip("#")
|
|
417
|
+
matched_ids = elements_by_id_attr.get(target_id, [])
|
|
418
|
+
for elem_id in matched_ids:
|
|
419
|
+
batch.append((sel_id, elem_id, 'static', 'high'))
|
|
420
|
+
|
|
421
|
+
# Match tag selectors using tag index
|
|
422
|
+
for sel_id, sel_text in selectors_by_type.get("tag", []):
|
|
423
|
+
tag_lower = sel_text.lower()
|
|
424
|
+
matched_ids = elements_by_tag.get(tag_lower, [])
|
|
425
|
+
for elem_id in matched_ids:
|
|
426
|
+
batch.append((sel_id, elem_id, 'static', 'high'))
|
|
427
|
+
|
|
428
|
+
# Match compound selectors (still requires cross-product but only for compound selectors)
|
|
429
|
+
compound_selectors = selectors_by_type.get("compound", [])
|
|
430
|
+
if compound_selectors:
|
|
431
|
+
for sel_id, sel_text in compound_selectors:
|
|
432
|
+
for elem_id, tag_name, elem_id_attr, classes in decoded_elements:
|
|
433
|
+
if self._selector_matches_element(sel_text, "compound", tag_name, elem_id_attr, classes):
|
|
434
|
+
batch.append((sel_id, elem_id, 'static', 'high'))
|
|
435
|
+
|
|
436
|
+
# Match any remaining selector types with full cross-product
|
|
437
|
+
remaining_types = set(selectors_by_type.keys()) - {"class", "id", "tag", "compound"}
|
|
438
|
+
if remaining_types:
|
|
439
|
+
for sel_type in remaining_types:
|
|
440
|
+
for sel_id, sel_text in selectors_by_type[sel_type]:
|
|
441
|
+
for elem_id, tag_name, elem_id_attr, classes in decoded_elements:
|
|
442
|
+
if self._selector_matches_element(sel_text, sel_type, tag_name, elem_id_attr, classes):
|
|
443
|
+
batch.append((sel_id, elem_id, 'static', 'high'))
|
|
444
|
+
|
|
445
|
+
# Batch insert all matches
|
|
446
|
+
if batch:
|
|
447
|
+
cursor.executemany(
|
|
448
|
+
"INSERT INTO style_selector_matches (selector_id, element_id, match_type, confidence) "
|
|
449
|
+
"VALUES (?, ?, ?, ?)",
|
|
450
|
+
batch
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
@staticmethod
|
|
454
|
+
def _selector_matches_element(selector_text, selector_type, tag_name, elem_id_attr, static_classes):
|
|
455
|
+
"""Check if a CSS selector matches a markup element.
|
|
456
|
+
|
|
457
|
+
static_classes can be a pre-decoded list or a JSON string.
|
|
458
|
+
"""
|
|
459
|
+
if isinstance(static_classes, str):
|
|
460
|
+
classes = json.loads(static_classes) if static_classes else []
|
|
461
|
+
else:
|
|
462
|
+
classes = static_classes
|
|
463
|
+
|
|
464
|
+
if selector_type == "class":
|
|
465
|
+
target_class = selector_text.lstrip(".")
|
|
466
|
+
return target_class in classes
|
|
467
|
+
elif selector_type == "id":
|
|
468
|
+
target_id = selector_text.lstrip("#")
|
|
469
|
+
return elem_id_attr == target_id
|
|
470
|
+
elif selector_type == "tag":
|
|
471
|
+
return tag_name.lower() == selector_text.lower()
|
|
472
|
+
elif selector_type == "compound":
|
|
473
|
+
parts = selector_text.replace(".", " .").replace("#", " #").split()
|
|
474
|
+
for part in parts:
|
|
475
|
+
if part.startswith("."):
|
|
476
|
+
if part[1:] not in classes:
|
|
477
|
+
return False
|
|
478
|
+
elif part.startswith("#"):
|
|
479
|
+
if elem_id_attr != part[1:]:
|
|
480
|
+
return False
|
|
481
|
+
else:
|
|
482
|
+
if tag_name.lower() != part.lower():
|
|
483
|
+
return False
|
|
484
|
+
return True
|
|
485
|
+
return False
|