codegraph-nav 0.1.0__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.
- codegraph_nav/__init__.py +194 -0
- codegraph_nav/ast_grep_analyzer.py +448 -0
- codegraph_nav/cli.py +223 -0
- codegraph_nav/code_navigator.py +1328 -0
- codegraph_nav/code_search.py +1009 -0
- codegraph_nav/colors.py +209 -0
- codegraph_nav/completions.py +354 -0
- codegraph_nav/dart_analyzer.py +301 -0
- codegraph_nav/dependency_graph.py +814 -0
- codegraph_nav/domain/__init__.py +20 -0
- codegraph_nav/domain/routes.py +337 -0
- codegraph_nav/domain/schemas.py +229 -0
- codegraph_nav/domain/tags.py +87 -0
- codegraph_nav/exporters.py +563 -0
- codegraph_nav/go_analyzer.py +273 -0
- codegraph_nav/graph/__init__.py +72 -0
- codegraph_nav/graph/builder.py +409 -0
- codegraph_nav/graph/communities.py +402 -0
- codegraph_nav/graph/flows.py +311 -0
- codegraph_nav/graph/query.py +380 -0
- codegraph_nav/graph/schema.py +266 -0
- codegraph_nav/graph/search.py +257 -0
- codegraph_nav/graph/store.py +517 -0
- codegraph_nav/hints.py +195 -0
- codegraph_nav/import_resolver.py +891 -0
- codegraph_nav/js_ts_analyzer.py +564 -0
- codegraph_nav/line_reader.py +664 -0
- codegraph_nav/mcp/__init__.py +39 -0
- codegraph_nav/mcp/__main__.py +5 -0
- codegraph_nav/mcp/server.py +2228 -0
- codegraph_nav/py.typed +2 -0
- codegraph_nav/ruby_analyzer.py +259 -0
- codegraph_nav/rust_analyzer.py +379 -0
- codegraph_nav/token_efficient_renderer.py +743 -0
- codegraph_nav/watcher.py +382 -0
- codegraph_nav-0.1.0.dist-info/METADATA +487 -0
- codegraph_nav-0.1.0.dist-info/RECORD +41 -0
- codegraph_nav-0.1.0.dist-info/WHEEL +5 -0
- codegraph_nav-0.1.0.dist-info/entry_points.txt +4 -0
- codegraph_nav-0.1.0.dist-info/licenses/LICENSE +21 -0
- codegraph_nav-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Export code map to different formats.
|
|
3
|
+
|
|
4
|
+
Provides exporters for converting code maps to Markdown, HTML, and GraphViz
|
|
5
|
+
formats for documentation and visualization purposes.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
Command line usage:
|
|
9
|
+
$ codegraph-nav export -f markdown -o docs/codebase.md
|
|
10
|
+
$ codegraph-nav export -f html -o docs/codebase.html
|
|
11
|
+
$ codegraph-nav export -f graphviz -o docs/deps.dot
|
|
12
|
+
|
|
13
|
+
Python API usage:
|
|
14
|
+
>>> from codegraph_nav.exporters import MarkdownExporter
|
|
15
|
+
>>> exporter = MarkdownExporter('.codegraph.json')
|
|
16
|
+
>>> markdown = exporter.export()
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import html
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
from abc import ABC, abstractmethod
|
|
24
|
+
from collections import defaultdict
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from .colors import get_colors
|
|
30
|
+
|
|
31
|
+
__version__ = "0.1.0"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BaseExporter(ABC):
|
|
35
|
+
"""Base class for code map exporters.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
code_map: Loaded code map data.
|
|
39
|
+
map_path: Path to the code map file.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, map_path: str):
|
|
43
|
+
"""Initialize the exporter.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
map_path: Path to the .codegraph.json file.
|
|
47
|
+
"""
|
|
48
|
+
self.map_path = map_path
|
|
49
|
+
self.code_map = self._load_map()
|
|
50
|
+
|
|
51
|
+
def _load_map(self) -> dict[str, Any]:
|
|
52
|
+
"""Load the code map from file."""
|
|
53
|
+
with open(self.map_path, encoding="utf-8") as f:
|
|
54
|
+
data: dict[str, Any] = json.load(f)
|
|
55
|
+
return data
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def export(self) -> str:
|
|
59
|
+
"""Export the code map to the target format.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Exported content as a string.
|
|
63
|
+
"""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
def export_to_file(self, output_path: str) -> None:
|
|
67
|
+
"""Export the code map to a file.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
output_path: Path to the output file.
|
|
71
|
+
"""
|
|
72
|
+
content = self.export()
|
|
73
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
74
|
+
f.write(content)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class MarkdownExporter(BaseExporter):
|
|
78
|
+
"""Export code map to Markdown format.
|
|
79
|
+
|
|
80
|
+
Generates a Markdown document with:
|
|
81
|
+
- Overview statistics
|
|
82
|
+
- File listing with symbols
|
|
83
|
+
- Symbol index by type
|
|
84
|
+
|
|
85
|
+
Example:
|
|
86
|
+
>>> exporter = MarkdownExporter('.codegraph.json')
|
|
87
|
+
>>> print(exporter.export())
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def export(self) -> str:
|
|
91
|
+
"""Export to Markdown format.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Markdown document as a string.
|
|
95
|
+
"""
|
|
96
|
+
lines = []
|
|
97
|
+
|
|
98
|
+
# Header
|
|
99
|
+
root = self.code_map.get("root", "Unknown")
|
|
100
|
+
lines.append(f"# Code Map: {Path(root).name}")
|
|
101
|
+
lines.append("")
|
|
102
|
+
lines.append(f"Generated: {self.code_map.get('generated_at', 'Unknown')}")
|
|
103
|
+
lines.append("")
|
|
104
|
+
|
|
105
|
+
# Statistics
|
|
106
|
+
stats = self.code_map.get("stats", {})
|
|
107
|
+
lines.append("## Statistics")
|
|
108
|
+
lines.append("")
|
|
109
|
+
lines.append(f"- **Files:** {stats.get('files_processed', 0)}")
|
|
110
|
+
lines.append(f"- **Symbols:** {stats.get('symbols_found', 0)}")
|
|
111
|
+
lines.append("")
|
|
112
|
+
|
|
113
|
+
# Symbol counts by type
|
|
114
|
+
type_counts: defaultdict[str, int] = defaultdict(int)
|
|
115
|
+
for file_info in self.code_map.get("files", {}).values():
|
|
116
|
+
for sym in file_info.get("symbols", []):
|
|
117
|
+
type_counts[sym["type"]] += 1
|
|
118
|
+
|
|
119
|
+
if type_counts:
|
|
120
|
+
lines.append("### Symbols by Type")
|
|
121
|
+
lines.append("")
|
|
122
|
+
lines.append("| Type | Count |")
|
|
123
|
+
lines.append("|------|-------|")
|
|
124
|
+
for sym_type, count in sorted(type_counts.items()):
|
|
125
|
+
lines.append(f"| {sym_type} | {count} |")
|
|
126
|
+
lines.append("")
|
|
127
|
+
|
|
128
|
+
# Files
|
|
129
|
+
lines.append("## Files")
|
|
130
|
+
lines.append("")
|
|
131
|
+
|
|
132
|
+
files = self.code_map.get("files", {})
|
|
133
|
+
for file_path in sorted(files.keys()):
|
|
134
|
+
file_info = files[file_path]
|
|
135
|
+
symbols = file_info.get("symbols", [])
|
|
136
|
+
|
|
137
|
+
lines.append(f"### `{file_path}`")
|
|
138
|
+
lines.append("")
|
|
139
|
+
|
|
140
|
+
if symbols:
|
|
141
|
+
lines.append("| Symbol | Type | Lines |")
|
|
142
|
+
lines.append("|--------|------|-------|")
|
|
143
|
+
for sym in sorted(symbols, key=lambda s: s["lines"][0]):
|
|
144
|
+
name = sym["name"]
|
|
145
|
+
sym_type = sym["type"]
|
|
146
|
+
line_range = f"{sym['lines'][0]}-{sym['lines'][1]}"
|
|
147
|
+
lines.append(f"| `{name}` | {sym_type} | {line_range} |")
|
|
148
|
+
lines.append("")
|
|
149
|
+
else:
|
|
150
|
+
lines.append("*No symbols found*")
|
|
151
|
+
lines.append("")
|
|
152
|
+
|
|
153
|
+
# Symbol Index
|
|
154
|
+
lines.append("## Symbol Index")
|
|
155
|
+
lines.append("")
|
|
156
|
+
|
|
157
|
+
index = self.code_map.get("index", {})
|
|
158
|
+
for name in sorted(index.keys()):
|
|
159
|
+
entries = index[name]
|
|
160
|
+
lines.append(f"### `{name}`")
|
|
161
|
+
lines.append("")
|
|
162
|
+
for entry in entries:
|
|
163
|
+
file_path = entry["file"]
|
|
164
|
+
sym_type = entry["type"]
|
|
165
|
+
line_range = f"{entry['lines'][0]}-{entry['lines'][1]}"
|
|
166
|
+
lines.append(f"- **{sym_type}** in `{file_path}` (lines {line_range})")
|
|
167
|
+
lines.append("")
|
|
168
|
+
|
|
169
|
+
return "\n".join(lines)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class HTMLExporter(BaseExporter):
|
|
173
|
+
"""Export code map to HTML format.
|
|
174
|
+
|
|
175
|
+
Generates an interactive HTML document with:
|
|
176
|
+
- Collapsible file tree
|
|
177
|
+
- Symbol search
|
|
178
|
+
- Statistics dashboard
|
|
179
|
+
|
|
180
|
+
Example:
|
|
181
|
+
>>> exporter = HTMLExporter('.codegraph.json')
|
|
182
|
+
>>> print(exporter.export())
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
def export(self) -> str:
|
|
186
|
+
"""Export to HTML format.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
HTML document as a string.
|
|
190
|
+
"""
|
|
191
|
+
root = self.code_map.get("root", "Unknown")
|
|
192
|
+
stats = self.code_map.get("stats", {})
|
|
193
|
+
|
|
194
|
+
# Symbol counts by type
|
|
195
|
+
type_counts: defaultdict[str, int] = defaultdict(int)
|
|
196
|
+
for file_info in self.code_map.get("files", {}).values():
|
|
197
|
+
for sym in file_info.get("symbols", []):
|
|
198
|
+
type_counts[sym["type"]] += 1
|
|
199
|
+
|
|
200
|
+
# Generate file tree HTML
|
|
201
|
+
files_html = self._generate_files_html()
|
|
202
|
+
|
|
203
|
+
# Generate type stats HTML
|
|
204
|
+
type_stats_html = ""
|
|
205
|
+
for sym_type, count in sorted(type_counts.items()):
|
|
206
|
+
type_stats_html += f'<div class="stat-item"><span class="type">{html.escape(sym_type)}</span><span class="count">{count}</span></div>'
|
|
207
|
+
|
|
208
|
+
return f"""<!DOCTYPE html>
|
|
209
|
+
<html lang="en">
|
|
210
|
+
<head>
|
|
211
|
+
<meta charset="UTF-8">
|
|
212
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
213
|
+
<title>Code Map: {html.escape(Path(root).name)}</title>
|
|
214
|
+
<style>
|
|
215
|
+
:root {{
|
|
216
|
+
--bg: #1a1a2e;
|
|
217
|
+
--bg-light: #16213e;
|
|
218
|
+
--text: #eee;
|
|
219
|
+
--text-dim: #888;
|
|
220
|
+
--accent: #0f3460;
|
|
221
|
+
--highlight: #e94560;
|
|
222
|
+
--success: #4ecca3;
|
|
223
|
+
--border: #333;
|
|
224
|
+
}}
|
|
225
|
+
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
|
226
|
+
body {{
|
|
227
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
228
|
+
background: var(--bg);
|
|
229
|
+
color: var(--text);
|
|
230
|
+
line-height: 1.6;
|
|
231
|
+
padding: 20px;
|
|
232
|
+
}}
|
|
233
|
+
.container {{ max-width: 1200px; margin: 0 auto; }}
|
|
234
|
+
h1 {{ color: var(--highlight); margin-bottom: 10px; }}
|
|
235
|
+
h2 {{ color: var(--success); margin: 20px 0 10px; border-bottom: 1px solid var(--border); padding-bottom: 5px; }}
|
|
236
|
+
.meta {{ color: var(--text-dim); font-size: 0.9em; margin-bottom: 20px; }}
|
|
237
|
+
.stats {{
|
|
238
|
+
display: grid;
|
|
239
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
240
|
+
gap: 15px;
|
|
241
|
+
margin-bottom: 30px;
|
|
242
|
+
}}
|
|
243
|
+
.stat-card {{
|
|
244
|
+
background: var(--bg-light);
|
|
245
|
+
border: 1px solid var(--border);
|
|
246
|
+
border-radius: 8px;
|
|
247
|
+
padding: 15px;
|
|
248
|
+
text-align: center;
|
|
249
|
+
}}
|
|
250
|
+
.stat-card .value {{ font-size: 2em; color: var(--success); font-weight: bold; }}
|
|
251
|
+
.stat-card .label {{ color: var(--text-dim); font-size: 0.9em; }}
|
|
252
|
+
.type-stats {{ display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; }}
|
|
253
|
+
.stat-item {{
|
|
254
|
+
background: var(--accent);
|
|
255
|
+
padding: 5px 12px;
|
|
256
|
+
border-radius: 15px;
|
|
257
|
+
font-size: 0.9em;
|
|
258
|
+
}}
|
|
259
|
+
.stat-item .type {{ margin-right: 8px; }}
|
|
260
|
+
.stat-item .count {{ color: var(--success); font-weight: bold; }}
|
|
261
|
+
.search-box {{
|
|
262
|
+
width: 100%;
|
|
263
|
+
padding: 10px 15px;
|
|
264
|
+
font-size: 1em;
|
|
265
|
+
border: 1px solid var(--border);
|
|
266
|
+
border-radius: 5px;
|
|
267
|
+
background: var(--bg-light);
|
|
268
|
+
color: var(--text);
|
|
269
|
+
margin-bottom: 20px;
|
|
270
|
+
}}
|
|
271
|
+
.search-box:focus {{ outline: none; border-color: var(--highlight); }}
|
|
272
|
+
.file {{
|
|
273
|
+
background: var(--bg-light);
|
|
274
|
+
border: 1px solid var(--border);
|
|
275
|
+
border-radius: 5px;
|
|
276
|
+
margin-bottom: 10px;
|
|
277
|
+
}}
|
|
278
|
+
.file-header {{
|
|
279
|
+
padding: 10px 15px;
|
|
280
|
+
cursor: pointer;
|
|
281
|
+
display: flex;
|
|
282
|
+
justify-content: space-between;
|
|
283
|
+
align-items: center;
|
|
284
|
+
}}
|
|
285
|
+
.file-header:hover {{ background: var(--accent); }}
|
|
286
|
+
.file-path {{ font-family: monospace; color: var(--success); }}
|
|
287
|
+
.file-count {{ color: var(--text-dim); font-size: 0.9em; }}
|
|
288
|
+
.file-content {{ display: none; padding: 0 15px 15px; }}
|
|
289
|
+
.file.open .file-content {{ display: block; }}
|
|
290
|
+
.file.open .file-header {{ border-bottom: 1px solid var(--border); }}
|
|
291
|
+
.symbol {{
|
|
292
|
+
display: flex;
|
|
293
|
+
justify-content: space-between;
|
|
294
|
+
padding: 8px 0;
|
|
295
|
+
border-bottom: 1px solid var(--border);
|
|
296
|
+
}}
|
|
297
|
+
.symbol:last-child {{ border-bottom: none; }}
|
|
298
|
+
.symbol-name {{ font-family: monospace; color: var(--text); }}
|
|
299
|
+
.symbol-type {{
|
|
300
|
+
background: var(--accent);
|
|
301
|
+
padding: 2px 8px;
|
|
302
|
+
border-radius: 10px;
|
|
303
|
+
font-size: 0.8em;
|
|
304
|
+
color: var(--highlight);
|
|
305
|
+
}}
|
|
306
|
+
.symbol-lines {{ color: var(--text-dim); font-size: 0.9em; margin-left: 10px; }}
|
|
307
|
+
.hidden {{ display: none !important; }}
|
|
308
|
+
</style>
|
|
309
|
+
</head>
|
|
310
|
+
<body>
|
|
311
|
+
<div class="container">
|
|
312
|
+
<h1>Code Map: {html.escape(Path(root).name)}</h1>
|
|
313
|
+
<p class="meta">Generated: {html.escape(self.code_map.get('generated_at', 'Unknown'))}</p>
|
|
314
|
+
|
|
315
|
+
<div class="stats">
|
|
316
|
+
<div class="stat-card">
|
|
317
|
+
<div class="value">{stats.get('files_processed', 0)}</div>
|
|
318
|
+
<div class="label">Files</div>
|
|
319
|
+
</div>
|
|
320
|
+
<div class="stat-card">
|
|
321
|
+
<div class="value">{stats.get('symbols_found', 0)}</div>
|
|
322
|
+
<div class="label">Symbols</div>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<h2>Symbols by Type</h2>
|
|
327
|
+
<div class="type-stats">{type_stats_html}</div>
|
|
328
|
+
|
|
329
|
+
<h2>Files</h2>
|
|
330
|
+
<input type="text" class="search-box" placeholder="Search symbols..." id="search">
|
|
331
|
+
<div id="files">{files_html}</div>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
<script>
|
|
335
|
+
// Toggle file expansion
|
|
336
|
+
document.querySelectorAll('.file-header').forEach(header => {{
|
|
337
|
+
header.addEventListener('click', () => {{
|
|
338
|
+
header.parentElement.classList.toggle('open');
|
|
339
|
+
}});
|
|
340
|
+
}});
|
|
341
|
+
|
|
342
|
+
// Search functionality
|
|
343
|
+
const searchBox = document.getElementById('search');
|
|
344
|
+
const files = document.querySelectorAll('.file');
|
|
345
|
+
|
|
346
|
+
searchBox.addEventListener('input', (e) => {{
|
|
347
|
+
const query = e.target.value.toLowerCase();
|
|
348
|
+
files.forEach(file => {{
|
|
349
|
+
const symbols = file.querySelectorAll('.symbol');
|
|
350
|
+
let hasMatch = false;
|
|
351
|
+
symbols.forEach(symbol => {{
|
|
352
|
+
const name = symbol.querySelector('.symbol-name').textContent.toLowerCase();
|
|
353
|
+
if (name.includes(query)) {{
|
|
354
|
+
symbol.classList.remove('hidden');
|
|
355
|
+
hasMatch = true;
|
|
356
|
+
}} else {{
|
|
357
|
+
symbol.classList.add('hidden');
|
|
358
|
+
}}
|
|
359
|
+
}});
|
|
360
|
+
if (query && hasMatch) {{
|
|
361
|
+
file.classList.add('open');
|
|
362
|
+
file.classList.remove('hidden');
|
|
363
|
+
}} else if (query && !hasMatch) {{
|
|
364
|
+
file.classList.add('hidden');
|
|
365
|
+
}} else {{
|
|
366
|
+
file.classList.remove('hidden');
|
|
367
|
+
symbols.forEach(s => s.classList.remove('hidden'));
|
|
368
|
+
}}
|
|
369
|
+
}});
|
|
370
|
+
}});
|
|
371
|
+
</script>
|
|
372
|
+
</body>
|
|
373
|
+
</html>"""
|
|
374
|
+
|
|
375
|
+
def _generate_files_html(self) -> str:
|
|
376
|
+
"""Generate HTML for files section."""
|
|
377
|
+
files_html = []
|
|
378
|
+
files = self.code_map.get("files", {})
|
|
379
|
+
|
|
380
|
+
for file_path in sorted(files.keys()):
|
|
381
|
+
file_info = files[file_path]
|
|
382
|
+
symbols = file_info.get("symbols", [])
|
|
383
|
+
|
|
384
|
+
symbols_html = ""
|
|
385
|
+
for sym in sorted(symbols, key=lambda s: s["lines"][0]):
|
|
386
|
+
name = html.escape(sym["name"])
|
|
387
|
+
sym_type = html.escape(sym["type"])
|
|
388
|
+
lines = f"{sym['lines'][0]}-{sym['lines'][1]}"
|
|
389
|
+
symbols_html += f"""
|
|
390
|
+
<div class="symbol">
|
|
391
|
+
<span>
|
|
392
|
+
<span class="symbol-name">{name}</span>
|
|
393
|
+
<span class="symbol-lines">:{lines}</span>
|
|
394
|
+
</span>
|
|
395
|
+
<span class="symbol-type">{sym_type}</span>
|
|
396
|
+
</div>"""
|
|
397
|
+
|
|
398
|
+
file_html = f"""
|
|
399
|
+
<div class="file">
|
|
400
|
+
<div class="file-header">
|
|
401
|
+
<span class="file-path">{html.escape(file_path)}</span>
|
|
402
|
+
<span class="file-count">{len(symbols)} symbols</span>
|
|
403
|
+
</div>
|
|
404
|
+
<div class="file-content">{symbols_html}</div>
|
|
405
|
+
</div>"""
|
|
406
|
+
files_html.append(file_html)
|
|
407
|
+
|
|
408
|
+
return "".join(files_html)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class GraphVizExporter(BaseExporter):
|
|
412
|
+
"""Export code map dependencies to GraphViz DOT format.
|
|
413
|
+
|
|
414
|
+
Generates a DOT graph showing:
|
|
415
|
+
- Symbols as nodes
|
|
416
|
+
- Dependencies as edges
|
|
417
|
+
- Files as clusters
|
|
418
|
+
|
|
419
|
+
Example:
|
|
420
|
+
>>> exporter = GraphVizExporter('.codegraph.json')
|
|
421
|
+
>>> print(exporter.export())
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
def export(self) -> str:
|
|
425
|
+
"""Export to GraphViz DOT format.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
DOT graph as a string.
|
|
429
|
+
"""
|
|
430
|
+
lines = []
|
|
431
|
+
lines.append("digraph CodeMap {")
|
|
432
|
+
lines.append(" rankdir=LR;")
|
|
433
|
+
lines.append(" node [shape=box, style=filled, fontname=Helvetica];")
|
|
434
|
+
lines.append(" edge [color=gray60];")
|
|
435
|
+
lines.append("")
|
|
436
|
+
|
|
437
|
+
# Color scheme for symbol types
|
|
438
|
+
type_colors = {
|
|
439
|
+
"function": "#4ecca3",
|
|
440
|
+
"class": "#e94560",
|
|
441
|
+
"method": "#0f3460",
|
|
442
|
+
"interface": "#ff6b6b",
|
|
443
|
+
"struct": "#ffa502",
|
|
444
|
+
"enum": "#a29bfe",
|
|
445
|
+
"type": "#fd79a8",
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
# Group symbols by file
|
|
449
|
+
files = self.code_map.get("files", {})
|
|
450
|
+
node_ids = {} # Map (file, name) to node id
|
|
451
|
+
edges = []
|
|
452
|
+
|
|
453
|
+
for file_idx, (file_path, file_info) in enumerate(sorted(files.items())):
|
|
454
|
+
symbols = file_info.get("symbols", [])
|
|
455
|
+
if not symbols:
|
|
456
|
+
continue
|
|
457
|
+
|
|
458
|
+
# Create subgraph (cluster) for file
|
|
459
|
+
cluster_name = file_path.replace("/", "_").replace(".", "_").replace("-", "_")
|
|
460
|
+
lines.append(f" subgraph cluster_{cluster_name} {{")
|
|
461
|
+
lines.append(f' label="{self._escape_dot(file_path)}";')
|
|
462
|
+
lines.append(" style=rounded;")
|
|
463
|
+
lines.append(" bgcolor=gray95;")
|
|
464
|
+
lines.append("")
|
|
465
|
+
|
|
466
|
+
for sym_idx, sym in enumerate(symbols):
|
|
467
|
+
node_id = f"node_{file_idx}_{sym_idx}"
|
|
468
|
+
node_ids[(file_path, sym["name"])] = node_id
|
|
469
|
+
|
|
470
|
+
color = type_colors.get(sym["type"], "#dfe6e9")
|
|
471
|
+
label = f"{self._escape_dot(sym['name'])}\\n[{self._escape_dot(sym['type'])}]"
|
|
472
|
+
|
|
473
|
+
lines.append(f' {node_id} [label="{label}", fillcolor="{color}"];')
|
|
474
|
+
|
|
475
|
+
# Collect dependencies for edges
|
|
476
|
+
deps = sym.get("deps") or []
|
|
477
|
+
for dep in deps:
|
|
478
|
+
edges.append((node_id, dep))
|
|
479
|
+
|
|
480
|
+
lines.append(" }")
|
|
481
|
+
lines.append("")
|
|
482
|
+
|
|
483
|
+
# Add edges for dependencies
|
|
484
|
+
if edges:
|
|
485
|
+
lines.append(" // Dependencies")
|
|
486
|
+
for source_id, dep_name in edges:
|
|
487
|
+
# Try to find the target node
|
|
488
|
+
target_id = None
|
|
489
|
+
for (_file_path, name), nid in node_ids.items():
|
|
490
|
+
if name == dep_name:
|
|
491
|
+
target_id = nid
|
|
492
|
+
break
|
|
493
|
+
|
|
494
|
+
if target_id:
|
|
495
|
+
lines.append(f" {source_id} -> {target_id};")
|
|
496
|
+
|
|
497
|
+
lines.append("")
|
|
498
|
+
|
|
499
|
+
lines.append("}")
|
|
500
|
+
return "\n".join(lines)
|
|
501
|
+
|
|
502
|
+
def _escape_dot(self, text: str) -> str:
|
|
503
|
+
"""Escape text for DOT format."""
|
|
504
|
+
return text.replace('"', '\\"').replace("\n", "\\n")
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def get_exporter(format_type: str, map_path: str) -> BaseExporter:
|
|
508
|
+
"""Get an exporter for the specified format.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
format_type: Export format ('markdown', 'html', 'graphviz').
|
|
512
|
+
map_path: Path to the .codegraph.json file.
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
Appropriate exporter instance.
|
|
516
|
+
|
|
517
|
+
Raises:
|
|
518
|
+
ValueError: If format is not supported.
|
|
519
|
+
"""
|
|
520
|
+
exporters: dict[str, Callable[[str], BaseExporter]] = {
|
|
521
|
+
"markdown": MarkdownExporter,
|
|
522
|
+
"md": MarkdownExporter,
|
|
523
|
+
"html": HTMLExporter,
|
|
524
|
+
"graphviz": GraphVizExporter,
|
|
525
|
+
"dot": GraphVizExporter,
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
exporter_class = exporters.get(format_type.lower())
|
|
529
|
+
if not exporter_class:
|
|
530
|
+
raise ValueError(f"Unsupported format: {format_type}. Supported: markdown, html, graphviz")
|
|
531
|
+
|
|
532
|
+
return exporter_class(map_path)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def run_export(args) -> None:
|
|
536
|
+
"""Run the export command.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
args: Parsed command-line arguments.
|
|
540
|
+
"""
|
|
541
|
+
c = get_colors(no_color=getattr(args, "no_color", False))
|
|
542
|
+
|
|
543
|
+
map_path = getattr(args, "map", ".codegraph.json")
|
|
544
|
+
if not os.path.exists(map_path):
|
|
545
|
+
print(c.error(f"Code map not found: {map_path}"), file=sys.stderr)
|
|
546
|
+
sys.exit(1)
|
|
547
|
+
|
|
548
|
+
format_type = getattr(args, "format", "markdown")
|
|
549
|
+
output_path = getattr(args, "output", None)
|
|
550
|
+
|
|
551
|
+
try:
|
|
552
|
+
exporter = get_exporter(format_type, map_path)
|
|
553
|
+
content = exporter.export()
|
|
554
|
+
|
|
555
|
+
if output_path:
|
|
556
|
+
exporter.export_to_file(output_path)
|
|
557
|
+
print(c.success(f"✓ Exported to {output_path}"), file=sys.stderr)
|
|
558
|
+
else:
|
|
559
|
+
print(content)
|
|
560
|
+
|
|
561
|
+
except Exception as e:
|
|
562
|
+
print(c.error(f"Export error: {e}"), file=sys.stderr)
|
|
563
|
+
sys.exit(1)
|