cortexcode 0.2.2__py3-none-any.whl → 0.4.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.
- cortexcode/advanced_analysis.py +816 -0
- cortexcode/docs/__init__.py +19 -0
- cortexcode/docs/generator.py +573 -0
- cortexcode/docs/html_generators.py +114 -0
- cortexcode/docs/javascript.py +373 -0
- cortexcode/docs/templates.py +174 -0
- cortexcode/docs.py +38 -1266
- cortexcode/indexer.py +28 -4
- cortexcode/mcp_server.py +142 -0
- {cortexcode-0.2.2.dist-info → cortexcode-0.4.0.dist-info}/METADATA +80 -4
- cortexcode-0.4.0.dist-info/RECORD +27 -0
- cortexcode-0.2.2.dist-info/RECORD +0 -21
- {cortexcode-0.2.2.dist-info → cortexcode-0.4.0.dist-info}/WHEEL +0 -0
- {cortexcode-0.2.2.dist-info → cortexcode-0.4.0.dist-info}/entry_points.txt +0 -0
- {cortexcode-0.2.2.dist-info → cortexcode-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {cortexcode-0.2.2.dist-info → cortexcode-0.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""HTML fragment generators for documentation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_tree_html(tree: dict, depth: int) -> str:
|
|
7
|
+
"""Generate HTML for file tree."""
|
|
8
|
+
html = ""
|
|
9
|
+
for name, content in sorted(tree.items()):
|
|
10
|
+
if isinstance(content, dict):
|
|
11
|
+
html += f'<div class="tree-item folder" style="padding-left: {depth * 20}px;">📁 {name}/</div>'
|
|
12
|
+
html += generate_tree_html(content, depth + 1)
|
|
13
|
+
else:
|
|
14
|
+
sym_count = len(content) if isinstance(content, list) else 0
|
|
15
|
+
html += f'<div class="tree-item file" style="padding-left: {depth * 20}px;">📄 {name} <span style="color: #64748b;">({sym_count})</span></div>'
|
|
16
|
+
return html
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def generate_symbols_html(symbols: list) -> str:
|
|
20
|
+
"""Generate HTML for symbol cards."""
|
|
21
|
+
html = ""
|
|
22
|
+
for sym in symbols:
|
|
23
|
+
params_str = ", ".join(sym.get("params", [])[:3])
|
|
24
|
+
if len(sym.get("params", [])) > 3:
|
|
25
|
+
params_str += "..."
|
|
26
|
+
|
|
27
|
+
fw = sym.get("framework", "")
|
|
28
|
+
fw_html = f'<span class="badge badge-fw">{fw}</span>' if fw else ""
|
|
29
|
+
doc = sym.get("doc", "")
|
|
30
|
+
doc_html = '<span class="badge badge-doc">doc</span>' if doc else ""
|
|
31
|
+
|
|
32
|
+
html += f"""<div class="symbol-card" data-type="{sym.get('type', 'function')}" onclick='showSymbolDetail({json.dumps(sym)})'>
|
|
33
|
+
<div class="symbol-name">{sym.get('name', 'unknown')}</div>
|
|
34
|
+
<div class="symbol-meta"><span class="badge badge-type">{sym.get('type', 'function')}</span>{fw_html}{doc_html}</div>
|
|
35
|
+
<div class="symbol-file">{sym.get('file', 'unknown')}:{sym.get('line', 0)}</div>
|
|
36
|
+
{f'<div class="symbol-params">{params_str}</div>' if params_str else ''}
|
|
37
|
+
</div>"""
|
|
38
|
+
return html
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def generate_imports_html(imports: list) -> str:
|
|
42
|
+
"""Generate HTML for import cards."""
|
|
43
|
+
html = ""
|
|
44
|
+
for imp in imports:
|
|
45
|
+
module = imp.get("module", "unknown")
|
|
46
|
+
imported = imp.get("imported", [])
|
|
47
|
+
file = imp.get("file", "unknown")
|
|
48
|
+
|
|
49
|
+
is_external = not module.startswith(".")
|
|
50
|
+
|
|
51
|
+
html += f"""<div class="symbol-card" data-external="{is_external}">
|
|
52
|
+
<div class="symbol-name">{module}</div>
|
|
53
|
+
<span class="symbol-type">{'external' if is_external else 'internal'}</span>
|
|
54
|
+
<div class="symbol-file">{file}</div>
|
|
55
|
+
<div class="symbol-params">{', '.join(imported) if imported else 'default'}</div>
|
|
56
|
+
</div>"""
|
|
57
|
+
return html
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def generate_exports_html(exports: list) -> str:
|
|
61
|
+
"""Generate HTML for export cards."""
|
|
62
|
+
html = ""
|
|
63
|
+
for exp in exports:
|
|
64
|
+
name = exp.get("name", "unknown")
|
|
65
|
+
exp_type = exp.get("type", "unknown")
|
|
66
|
+
file = exp.get("file", "unknown")
|
|
67
|
+
|
|
68
|
+
html += f"""<div class="symbol-card">
|
|
69
|
+
<div class="symbol-name">{name}</div>
|
|
70
|
+
<span class="symbol-type">{exp_type}</span>
|
|
71
|
+
<div class="symbol-file">{file}</div>
|
|
72
|
+
</div>"""
|
|
73
|
+
return html
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def generate_routes_html(routes: list) -> str:
|
|
77
|
+
"""Generate HTML for API route cards."""
|
|
78
|
+
html = ""
|
|
79
|
+
for route in routes:
|
|
80
|
+
method = route.get("method", "GET")
|
|
81
|
+
path = route.get("path", "/")
|
|
82
|
+
framework = route.get("framework", "unknown")
|
|
83
|
+
file = route.get("file", "unknown")
|
|
84
|
+
|
|
85
|
+
method_colors = {"GET": "#10b981", "POST": "#3b82f6", "PUT": "#f59e0b", "DELETE": "#ef4444", "PATCH": "#8b5cf6"}
|
|
86
|
+
color = method_colors.get(method, "#6b7280")
|
|
87
|
+
|
|
88
|
+
html += f"""<div class="symbol-card">
|
|
89
|
+
<div class="symbol-name" style="display: flex; align-items: center; gap: 8px;">
|
|
90
|
+
<span style="background: {color}; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 600;">{method}</span>
|
|
91
|
+
{path}
|
|
92
|
+
</div>
|
|
93
|
+
<span class="symbol-type">{framework}</span>
|
|
94
|
+
<div class="symbol-file">{file}</div>
|
|
95
|
+
</div>"""
|
|
96
|
+
return html
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def generate_entities_html(entities: list) -> str:
|
|
100
|
+
"""Generate HTML for entity cards."""
|
|
101
|
+
html = ""
|
|
102
|
+
for ent in entities:
|
|
103
|
+
name = ent.get("name", "unknown")
|
|
104
|
+
ent_type = ent.get("type", "unknown")
|
|
105
|
+
fields = ent.get("fields", [])
|
|
106
|
+
file = ent.get("file", "unknown")
|
|
107
|
+
|
|
108
|
+
html += f"""<div class="symbol-card">
|
|
109
|
+
<div class="symbol-name">{name}</div>
|
|
110
|
+
<span class="symbol-type">{ent_type}</span>
|
|
111
|
+
<div class="symbol-file">{file}</div>
|
|
112
|
+
<div class="symbol-params">{', '.join(fields[:5])}{'...' if len(fields) > 5 else ''}</div>
|
|
113
|
+
</div>"""
|
|
114
|
+
return html
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""JavaScript code for interactive documentation."""
|
|
2
|
+
|
|
3
|
+
JS_TEMPLATE = """
|
|
4
|
+
// ─── DATA ───
|
|
5
|
+
const callGraphData = {call_graph_json};
|
|
6
|
+
const fileDepsData = {file_deps_json};
|
|
7
|
+
const typeCountsData = {type_counts_json};
|
|
8
|
+
const langCountsData = {lang_counts_json};
|
|
9
|
+
const typeColors = {type_colors_json};
|
|
10
|
+
const langColors = {lang_colors_json};
|
|
11
|
+
const searchData = {search_data_json};
|
|
12
|
+
|
|
13
|
+
// ─── TABS ───
|
|
14
|
+
function showTab(id, el) {{
|
|
15
|
+
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
|
16
|
+
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
17
|
+
document.getElementById(id).classList.add('active');
|
|
18
|
+
if (el) el.classList.add('active');
|
|
19
|
+
if (id === 'graph') initGraph();
|
|
20
|
+
if (id === 'deps') initDeps();
|
|
21
|
+
}}
|
|
22
|
+
|
|
23
|
+
// ─── DONUT CHARTS ───
|
|
24
|
+
function drawDonut(svgId, legendId, data, colorMap, defaultColor) {{
|
|
25
|
+
const entries = Object.entries(data).sort((a,b) => b[1] - a[1]);
|
|
26
|
+
const total = entries.reduce((s, e) => s + e[1], 0);
|
|
27
|
+
if (!total) return;
|
|
28
|
+
|
|
29
|
+
const svg = d3.select('#' + svgId);
|
|
30
|
+
const w = 160, h = 160, r = 65, inner = 40;
|
|
31
|
+
const g = svg.append('g').attr('transform', `translate(${{w/2}},${{h/2}})`);
|
|
32
|
+
|
|
33
|
+
const pie = d3.pie().value(d => d[1]).sort(null).padAngle(0.02);
|
|
34
|
+
const arc = d3.arc().innerRadius(inner).outerRadius(r);
|
|
35
|
+
|
|
36
|
+
g.selectAll('path').data(pie(entries)).join('path')
|
|
37
|
+
.attr('d', arc)
|
|
38
|
+
.attr('fill', d => colorMap[d.data[0]] || defaultColor || '#475569')
|
|
39
|
+
.attr('stroke', 'var(--bg2)')
|
|
40
|
+
.attr('stroke-width', 2)
|
|
41
|
+
.style('cursor', 'pointer')
|
|
42
|
+
.on('mouseover', function() {{ d3.select(this).attr('opacity', 0.8); }})
|
|
43
|
+
.on('mouseout', function() {{ d3.select(this).attr('opacity', 1); }});
|
|
44
|
+
|
|
45
|
+
g.append('text').text(total).attr('text-anchor','middle').attr('dy','-0.1em').attr('fill','white').attr('font-size','22px').attr('font-weight','700');
|
|
46
|
+
g.append('text').text('total').attr('text-anchor','middle').attr('dy','1.2em').attr('fill','var(--text3)').attr('font-size','11px');
|
|
47
|
+
|
|
48
|
+
const legend = document.getElementById(legendId);
|
|
49
|
+
legend.innerHTML = entries.slice(0, 8).map(([k, v]) => `
|
|
50
|
+
<div class="chart-legend-item">
|
|
51
|
+
<div class="chart-legend-dot" style="background:${{colorMap[k] || defaultColor || '#475569'}}"></div>
|
|
52
|
+
<span>${{k}}</span>
|
|
53
|
+
<span class="chart-legend-val">${{v}}</span>
|
|
54
|
+
</div>
|
|
55
|
+
`).join('');
|
|
56
|
+
}}
|
|
57
|
+
|
|
58
|
+
drawDonut('typeDonut', 'typeLegend', typeCountsData, typeColors, '#475569');
|
|
59
|
+
drawDonut('langDonut', 'langLegend', langCountsData, langColors, '#475569');
|
|
60
|
+
|
|
61
|
+
// ─── GLOBAL SEARCH ───
|
|
62
|
+
function doGlobalSearch(q) {{
|
|
63
|
+
const box = document.getElementById('searchResults');
|
|
64
|
+
if (!q || q.length < 2) {{ box.style.display = 'none'; return; }}
|
|
65
|
+
q = q.toLowerCase();
|
|
66
|
+
const results = searchData.filter(s => s.name.toLowerCase().includes(q) || s.file.toLowerCase().includes(q)).slice(0, 15);
|
|
67
|
+
if (!results.length) {{ box.innerHTML = '<div style="padding:14px;color:var(--text3);">No results</div>'; }}
|
|
68
|
+
else {{
|
|
69
|
+
box.innerHTML = results.map(s => `
|
|
70
|
+
<div class="search-result-item" onclick="showTab('symbols',document.querySelectorAll('.nav-item')[1]);filterSymbols('${{s.name}}');document.getElementById('searchResults').style.display='none';">
|
|
71
|
+
<span class="badge badge-type">${{s.type}}</span>
|
|
72
|
+
<span style="color:var(--accent);font-weight:600;">${{s.name}}</span>
|
|
73
|
+
<span style="color:var(--text3);font-size:11px;margin-left:auto;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${{s.file}}:${{s.line}}</span>
|
|
74
|
+
</div>
|
|
75
|
+
`).join('');
|
|
76
|
+
}}
|
|
77
|
+
box.style.display = 'block';
|
|
78
|
+
}}
|
|
79
|
+
document.addEventListener('click', e => {{
|
|
80
|
+
if (!e.target.closest('.search-wrapper')) document.getElementById('searchResults').style.display = 'none';
|
|
81
|
+
}});
|
|
82
|
+
|
|
83
|
+
// ─── SYMBOL FILTERS ───
|
|
84
|
+
function filterSymbols(q) {{
|
|
85
|
+
document.querySelectorAll('.symbol-card').forEach(c => {{
|
|
86
|
+
const name = c.querySelector('.symbol-name').textContent.toLowerCase();
|
|
87
|
+
const file = c.querySelector('.symbol-file')?.textContent?.toLowerCase() || '';
|
|
88
|
+
c.style.display = (name.includes(q.toLowerCase()) || file.includes(q.toLowerCase())) ? '' : 'none';
|
|
89
|
+
}});
|
|
90
|
+
}}
|
|
91
|
+
function filterByType(type, btn) {{
|
|
92
|
+
document.querySelectorAll('#symbolFilterTabs .filter-tab').forEach(t => t.classList.remove('active'));
|
|
93
|
+
btn.classList.add('active');
|
|
94
|
+
document.querySelectorAll('.symbol-card').forEach(c => {{
|
|
95
|
+
c.style.display = (type === 'all' || c.dataset.type === type) ? '' : 'none';
|
|
96
|
+
}});
|
|
97
|
+
}}
|
|
98
|
+
function filterTree(q) {{
|
|
99
|
+
document.querySelectorAll('.tree-item').forEach(i => {{
|
|
100
|
+
i.style.display = i.textContent.toLowerCase().includes(q.toLowerCase()) ? 'flex' : 'none';
|
|
101
|
+
}});
|
|
102
|
+
}}
|
|
103
|
+
function filterImports(type, btn) {{
|
|
104
|
+
btn.parentElement.querySelectorAll('.filter-tab').forEach(t => t.classList.remove('active'));
|
|
105
|
+
btn.classList.add('active');
|
|
106
|
+
btn.closest('.card').querySelectorAll('.symbol-card').forEach(c => {{
|
|
107
|
+
if (type === 'all') c.style.display = '';
|
|
108
|
+
else c.style.display = (c.dataset.external === (type === 'external' ? 'True' : 'False')) ? '' : 'none';
|
|
109
|
+
}});
|
|
110
|
+
}}
|
|
111
|
+
|
|
112
|
+
// ─── SYMBOL MODAL ───
|
|
113
|
+
function showSymbolDetail(sym) {{
|
|
114
|
+
document.getElementById('modalTitle').textContent = sym.name;
|
|
115
|
+
let b = `<div class="modal-section"><div style="display:flex;gap:6px;flex-wrap:wrap;">`;
|
|
116
|
+
b += `<span class="badge badge-type">${{sym.type}}</span>`;
|
|
117
|
+
if (sym.framework) b += `<span class="badge badge-fw">${{sym.framework}}</span>`;
|
|
118
|
+
if (sym.doc) b += `<span class="badge badge-doc">documented</span>`;
|
|
119
|
+
b += `</div></div>`;
|
|
120
|
+
b += `<div class="modal-section"><div class="modal-section-title">Location</div><div class="modal-code">${{sym.file}}:${{sym.line}}</div></div>`;
|
|
121
|
+
if (sym.doc) b += `<div class="modal-section"><div class="modal-section-title">Documentation</div><p style="color:var(--text2);font-size:13px;line-height:1.6;">${{sym.doc}}</p></div>`;
|
|
122
|
+
if (sym.params?.length) b += `<div class="modal-section"><div class="modal-section-title">Parameters</div><div class="modal-code">${{sym.params.join(', ')}}</div></div>`;
|
|
123
|
+
if (sym.return_type) b += `<div class="modal-section"><div class="modal-section-title">Returns</div><div class="modal-code">${{sym.return_type}}</div></div>`;
|
|
124
|
+
if (sym.calls?.length) {{
|
|
125
|
+
b += `<div class="modal-section"><div class="modal-section-title">Calls (${{sym.calls.length}})</div><div style="display:flex;flex-wrap:wrap;gap:4px;">`;
|
|
126
|
+
sym.calls.forEach(c => {{ b += `<span class="modal-tag" onclick="closeModal();highlightGraphNode('${{c}}')">${{c}}</span>`; }});
|
|
127
|
+
b += `</div></div>`;
|
|
128
|
+
}}
|
|
129
|
+
const callers = Object.entries(callGraphData).filter(([k,v]) => v.includes(sym.name)).map(([k]) => k);
|
|
130
|
+
if (callers.length) {{
|
|
131
|
+
b += `<div class="modal-section"><div class="modal-section-title">Called By (${{callers.length}})</div><div style="display:flex;flex-wrap:wrap;gap:4px;">`;
|
|
132
|
+
callers.slice(0, 15).forEach(c => {{ b += `<span class="modal-tag" onclick="closeModal();highlightGraphNode('${{c}}')">${{c}}</span>`; }});
|
|
133
|
+
b += `</div></div>`;
|
|
134
|
+
}}
|
|
135
|
+
if (sym.methods?.length) {{
|
|
136
|
+
b += `<div class="modal-section"><div class="modal-section-title">Methods (${{sym.methods.length}})</div>`;
|
|
137
|
+
sym.methods.forEach(m => {{ b += `<div style="padding:6px 0;border-bottom:1px solid var(--bg3);font-size:13px;font-family:monospace;">${{m.name}}(${{(m.params||[]).join(', ')}})</div>`; }});
|
|
138
|
+
b += `</div>`;
|
|
139
|
+
}}
|
|
140
|
+
document.getElementById('modalBody').innerHTML = b;
|
|
141
|
+
document.getElementById('symbolModal').classList.add('active');
|
|
142
|
+
}}
|
|
143
|
+
function closeModal() {{ document.getElementById('symbolModal').classList.remove('active'); }}
|
|
144
|
+
document.getElementById('symbolModal').addEventListener('click', function(e) {{ if (e.target === this) closeModal(); }});
|
|
145
|
+
|
|
146
|
+
// ─── CALL GRAPH (D3 Force) ───
|
|
147
|
+
let graphSim, graphSvg, graphG, graphZoom, graphNodes, graphLinks, labelsVisible = true;
|
|
148
|
+
let graphInited = false;
|
|
149
|
+
|
|
150
|
+
function initGraph() {{
|
|
151
|
+
if (graphInited) return;
|
|
152
|
+
graphInited = true;
|
|
153
|
+
|
|
154
|
+
const allIds = new Set(Object.keys(callGraphData));
|
|
155
|
+
Object.values(callGraphData).forEach(ts => ts.forEach(t => allIds.add(t)));
|
|
156
|
+
|
|
157
|
+
const connected = new Set();
|
|
158
|
+
Object.entries(callGraphData).forEach(([s, ts]) => {{
|
|
159
|
+
if (ts.length) {{ connected.add(s); ts.forEach(t => connected.add(t)); }}
|
|
160
|
+
}});
|
|
161
|
+
const nodeIds = Array.from(connected).slice(0, 120);
|
|
162
|
+
const nodeSet = new Set(nodeIds);
|
|
163
|
+
|
|
164
|
+
const nodes = nodeIds.map(id => ({{
|
|
165
|
+
id,
|
|
166
|
+
calls: (callGraphData[id] || []).length,
|
|
167
|
+
calledBy: Object.values(callGraphData).filter(v => v.includes(id)).length,
|
|
168
|
+
isCaller: !!callGraphData[id]?.length
|
|
169
|
+
}}));
|
|
170
|
+
|
|
171
|
+
const links = [];
|
|
172
|
+
Object.entries(callGraphData).forEach(([s, ts]) => {{
|
|
173
|
+
ts.forEach(t => {{
|
|
174
|
+
if (nodeSet.has(s) && nodeSet.has(t)) links.push({{ source: s, target: t }});
|
|
175
|
+
}});
|
|
176
|
+
}});
|
|
177
|
+
|
|
178
|
+
document.getElementById('graphNodeCount').textContent = `${{nodes.length}} nodes · ${{links.length}} edges`;
|
|
179
|
+
|
|
180
|
+
const container = document.getElementById('graphContainer');
|
|
181
|
+
const W = container.clientWidth || 900, H = 600;
|
|
182
|
+
|
|
183
|
+
graphSvg = d3.select('#graphContainer').append('svg').attr('width', W).attr('height', H);
|
|
184
|
+
graphG = graphSvg.append('g');
|
|
185
|
+
|
|
186
|
+
graphZoom = d3.zoom().scaleExtent([0.2, 5]).on('zoom', e => graphG.attr('transform', e.transform));
|
|
187
|
+
graphSvg.call(graphZoom);
|
|
188
|
+
|
|
189
|
+
graphSvg.append('defs').append('marker').attr('id','arrowhead').attr('viewBox','0 -5 10 10').attr('refX',20).attr('refY',0).attr('markerWidth',6).attr('markerHeight',6).attr('orient','auto').append('path').attr('d','M0,-5L10,0L0,5').attr('fill','#475569');
|
|
190
|
+
|
|
191
|
+
graphLinks = graphG.append('g').selectAll('line').data(links).join('line')
|
|
192
|
+
.attr('class', 'link').attr('stroke', '#475569').attr('stroke-width', 1).attr('marker-end', 'url(#arrowhead)');
|
|
193
|
+
|
|
194
|
+
const nodeG = graphG.append('g').selectAll('g').data(nodes).join('g')
|
|
195
|
+
.attr('class', 'node').style('cursor', 'pointer');
|
|
196
|
+
|
|
197
|
+
nodeG.append('circle')
|
|
198
|
+
.attr('r', d => 5 + Math.min(d.calls + d.calledBy, 15))
|
|
199
|
+
.attr('fill', d => d.isCaller ? 'var(--accent)' : 'var(--accent2)')
|
|
200
|
+
.attr('stroke', '#fff').attr('stroke-width', 1.5);
|
|
201
|
+
|
|
202
|
+
nodeG.append('text')
|
|
203
|
+
.text(d => d.id.length > 18 ? d.id.substring(0, 18) + '…' : d.id)
|
|
204
|
+
.attr('x', d => 8 + Math.min(d.calls + d.calledBy, 15))
|
|
205
|
+
.attr('y', 4).attr('fill', 'var(--text)').attr('font-size', '11px')
|
|
206
|
+
.attr('class', 'node-label');
|
|
207
|
+
|
|
208
|
+
graphNodes = nodeG;
|
|
209
|
+
|
|
210
|
+
const tooltip = document.getElementById('graphTooltip');
|
|
211
|
+
nodeG.on('mouseover', (e, d) => {{
|
|
212
|
+
tooltip.innerHTML = `<strong style="color:var(--accent)">${{d.id}}</strong><br><span style="color:var(--text3)">Calls: ${{d.calls}} · Called by: ${{d.calledBy}}</span>`;
|
|
213
|
+
tooltip.style.display = 'block';
|
|
214
|
+
tooltip.style.left = (e.offsetX + 12) + 'px';
|
|
215
|
+
tooltip.style.top = (e.offsetY - 10) + 'px';
|
|
216
|
+
}}).on('mouseout', () => {{ tooltip.style.display = 'none'; }});
|
|
217
|
+
|
|
218
|
+
nodeG.on('click', (e, d) => {{
|
|
219
|
+
e.stopPropagation();
|
|
220
|
+
highlightNode(d.id, nodes, links);
|
|
221
|
+
}});
|
|
222
|
+
|
|
223
|
+
graphSvg.on('click', () => {{ resetHighlight(); }});
|
|
224
|
+
|
|
225
|
+
nodeG.call(d3.drag()
|
|
226
|
+
.on('start', (e, d) => {{ if (!e.active) graphSim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; }})
|
|
227
|
+
.on('drag', (e, d) => {{ d.fx = e.x; d.fy = e.y; }})
|
|
228
|
+
.on('end', (e, d) => {{ if (!e.active) graphSim.alphaTarget(0); d.fx = null; d.fy = null; }}));
|
|
229
|
+
|
|
230
|
+
graphSim = d3.forceSimulation(nodes)
|
|
231
|
+
.force('link', d3.forceLink(links).id(d => d.id).distance(70))
|
|
232
|
+
.force('charge', d3.forceManyBody().strength(-150))
|
|
233
|
+
.force('center', d3.forceCenter(W / 2, H / 2))
|
|
234
|
+
.force('collision', d3.forceCollide().radius(d => 10 + Math.min(d.calls + d.calledBy, 15)));
|
|
235
|
+
|
|
236
|
+
graphSim.on('tick', () => {{
|
|
237
|
+
graphLinks.attr('x1', d => d.source.x).attr('y1', d => d.source.y).attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
|
238
|
+
graphNodes.attr('transform', d => `translate(${{d.x}},${{d.y}})`);
|
|
239
|
+
}});
|
|
240
|
+
}}
|
|
241
|
+
|
|
242
|
+
function highlightNode(id, nodes, links) {{
|
|
243
|
+
const calls = callGraphData[id] || [];
|
|
244
|
+
const callers = Object.entries(callGraphData).filter(([k,v]) => v.includes(id)).map(([k]) => k);
|
|
245
|
+
const related = new Set([id, ...calls, ...callers]);
|
|
246
|
+
|
|
247
|
+
graphNodes.classed('dimmed', d => !related.has(d.id));
|
|
248
|
+
graphLinks.classed('dimmed', d => d.source.id !== id && d.target.id !== id);
|
|
249
|
+
graphLinks.classed('highlighted', d => d.source.id === id || d.target.id === id);
|
|
250
|
+
|
|
251
|
+
const info = document.getElementById('graphInfo');
|
|
252
|
+
info.classList.add('active');
|
|
253
|
+
info.innerHTML = `
|
|
254
|
+
<strong style="color:var(--accent);font-size:16px;">${{id}}</strong>
|
|
255
|
+
<div style="margin-top:10px;display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
|
256
|
+
<div><div style="font-size:11px;color:var(--text3);text-transform:uppercase;margin-bottom:6px;">Calls (${{calls.length}})</div>
|
|
257
|
+
${{calls.length ? calls.map(c => `<span class="modal-tag" onclick="highlightGraphNode('${{c}}')">${{c}}</span>`).join('') : '<span style="color:var(--text3)">none</span>'}}</div>
|
|
258
|
+
<div><div style="font-size:11px;color:var(--text3);text-transform:uppercase;margin-bottom:6px;">Called By (${{callers.length}})</div>
|
|
259
|
+
${{callers.length ? callers.slice(0,10).map(c => `<span class="modal-tag" onclick="highlightGraphNode('${{c}}')">${{c}}</span>`).join('') : '<span style="color:var(--text3)">none</span>'}}</div>
|
|
260
|
+
</div>`;
|
|
261
|
+
}}
|
|
262
|
+
|
|
263
|
+
function resetHighlight() {{
|
|
264
|
+
if (!graphNodes) return;
|
|
265
|
+
graphNodes.classed('dimmed', false);
|
|
266
|
+
graphLinks.classed('dimmed', false).classed('highlighted', false);
|
|
267
|
+
document.getElementById('graphInfo').classList.remove('active');
|
|
268
|
+
}}
|
|
269
|
+
|
|
270
|
+
function highlightGraphNode(name) {{
|
|
271
|
+
showTab('graph', document.querySelectorAll('.nav-item')[2]);
|
|
272
|
+
setTimeout(() => {{
|
|
273
|
+
const nd = graphNodes?.data()?.find(d => d.id === name);
|
|
274
|
+
if (nd) highlightNode(name, graphNodes.data(), graphLinks.data());
|
|
275
|
+
}}, 100);
|
|
276
|
+
}}
|
|
277
|
+
|
|
278
|
+
function searchGraphNode(q) {{
|
|
279
|
+
if (!graphNodes) return;
|
|
280
|
+
if (!q) {{ resetHighlight(); return; }}
|
|
281
|
+
q = q.toLowerCase();
|
|
282
|
+
graphNodes.classed('dimmed', d => !d.id.toLowerCase().includes(q));
|
|
283
|
+
graphLinks.classed('dimmed', true);
|
|
284
|
+
}}
|
|
285
|
+
|
|
286
|
+
function resetGraph() {{ resetHighlight(); if (graphSvg) graphSvg.transition().call(graphZoom.transform, d3.zoomIdentity); }}
|
|
287
|
+
function zoomIn() {{ if (graphSvg) graphSvg.transition().call(graphZoom.scaleBy, 1.4); }}
|
|
288
|
+
function zoomOut() {{ if (graphSvg) graphSvg.transition().call(graphZoom.scaleBy, 0.7); }}
|
|
289
|
+
function toggleLabels() {{
|
|
290
|
+
labelsVisible = !labelsVisible;
|
|
291
|
+
d3.selectAll('.node-label').style('display', labelsVisible ? 'block' : 'none');
|
|
292
|
+
document.getElementById('toggleLabels').textContent = labelsVisible ? 'Hide Labels' : 'Show Labels';
|
|
293
|
+
}}
|
|
294
|
+
|
|
295
|
+
// ─── FILE DEPS GRAPH ───
|
|
296
|
+
let depsInited = false;
|
|
297
|
+
function initDeps() {{
|
|
298
|
+
if (depsInited) return;
|
|
299
|
+
depsInited = true;
|
|
300
|
+
|
|
301
|
+
const allFiles = new Set();
|
|
302
|
+
Object.entries(fileDepsData).forEach(([f, deps]) => {{
|
|
303
|
+
allFiles.add(f); deps.forEach(d => allFiles.add(d));
|
|
304
|
+
}});
|
|
305
|
+
const fileArr = Array.from(allFiles).slice(0, 80);
|
|
306
|
+
const fileSet = new Set(fileArr);
|
|
307
|
+
const nodes = fileArr.map(f => ({{ id: f, short: f.split('/').pop() }}));
|
|
308
|
+
const links = [];
|
|
309
|
+
Object.entries(fileDepsData).forEach(([s, deps]) => {{
|
|
310
|
+
deps.forEach(t => {{ if (fileSet.has(s) && fileSet.has(t)) links.push({{ source: s, target: t }}); }});
|
|
311
|
+
}});
|
|
312
|
+
|
|
313
|
+
const container = document.getElementById('depsContainer');
|
|
314
|
+
const W = container.clientWidth || 900, H = 500;
|
|
315
|
+
const svg = d3.select('#depsContainer').append('svg').attr('width', W).attr('height', H);
|
|
316
|
+
const g = svg.append('g');
|
|
317
|
+
|
|
318
|
+
const zoom = d3.zoom().scaleExtent([0.2, 4]).on('zoom', e => g.attr('transform', e.transform));
|
|
319
|
+
svg.call(zoom);
|
|
320
|
+
|
|
321
|
+
svg.append('defs').append('marker').attr('id','depArrow').attr('viewBox','0 -5 10 10').attr('refX',14).attr('refY',0).attr('markerWidth',5).attr('markerHeight',5).attr('orient','auto').append('path').attr('d','M0,-5L10,0L0,5').attr('fill','var(--green)');
|
|
322
|
+
|
|
323
|
+
const link = g.append('g').selectAll('line').data(links).join('line')
|
|
324
|
+
.attr('stroke', 'var(--green)').attr('stroke-opacity', 0.3).attr('stroke-width', 1).attr('marker-end', 'url(#depArrow)');
|
|
325
|
+
|
|
326
|
+
const node = g.append('g').selectAll('g').data(nodes).join('g').style('cursor','pointer');
|
|
327
|
+
node.append('circle').attr('r', 6).attr('fill', 'var(--green)').attr('stroke', '#fff').attr('stroke-width', 1);
|
|
328
|
+
node.append('text').text(d => d.short.length > 20 ? d.short.substring(0,20)+'…' : d.short).attr('x',10).attr('y',4).attr('fill','var(--text2)').attr('font-size','10px');
|
|
329
|
+
|
|
330
|
+
node.on('click', (e, d) => {{
|
|
331
|
+
e.stopPropagation();
|
|
332
|
+
const imports = fileDepsData[d.id] || [];
|
|
333
|
+
const importedBy = Object.entries(fileDepsData).filter(([k,v]) => v.includes(d.id)).map(([k]) => k);
|
|
334
|
+
const related = new Set([d.id, ...imports, ...importedBy]);
|
|
335
|
+
node.selectAll('circle').attr('opacity', dd => related.has(dd.id) ? 1 : 0.1);
|
|
336
|
+
node.selectAll('text').attr('opacity', dd => related.has(dd.id) ? 1 : 0.1);
|
|
337
|
+
link.attr('stroke-opacity', dd => dd.source.id === d.id || dd.target.id === d.id ? 0.8 : 0.05);
|
|
338
|
+
|
|
339
|
+
const info = document.getElementById('depsInfo');
|
|
340
|
+
info.classList.add('active');
|
|
341
|
+
info.innerHTML = `<strong style="color:var(--green)">${{d.id}}</strong>
|
|
342
|
+
<div style="margin-top:10px;display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
|
343
|
+
<div><div style="font-size:11px;color:var(--text3);text-transform:uppercase;margin-bottom:6px;">Imports (${{imports.length}})</div>${{imports.map(f=>`<div style="font-size:12px;color:var(--text2);padding:2px 0;">${{f}}</div>`).join('')||'<span style="color:var(--text3)">none</span>'}}</div>
|
|
344
|
+
<div><div style="font-size:11px;color:var(--text3);text-transform:uppercase;margin-bottom:6px;">Imported By (${{importedBy.length}})</div>${{importedBy.map(f=>`<div style="font-size:12px;color:var(--text2);padding:2px 0;">${{f}}</div>`).join('')||'<span style="color:var(--text3)">none</span>'}}</div></div>`;
|
|
345
|
+
}});
|
|
346
|
+
svg.on('click', () => {{
|
|
347
|
+
node.selectAll('circle').attr('opacity', 1); node.selectAll('text').attr('opacity', 1);
|
|
348
|
+
link.attr('stroke-opacity', 0.3);
|
|
349
|
+
document.getElementById('depsInfo').classList.remove('active');
|
|
350
|
+
}});
|
|
351
|
+
|
|
352
|
+
node.call(d3.drag()
|
|
353
|
+
.on('start', (e, d) => {{ if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; }})
|
|
354
|
+
.on('drag', (e, d) => {{ d.fx = e.x; d.fy = e.y; }})
|
|
355
|
+
.on('end', (e, d) => {{ if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; }}));
|
|
356
|
+
|
|
357
|
+
const sim = d3.forceSimulation(nodes)
|
|
358
|
+
.force('link', d3.forceLink(links).id(d => d.id).distance(60))
|
|
359
|
+
.force('charge', d3.forceManyBody().strength(-100))
|
|
360
|
+
.force('center', d3.forceCenter(W/2, H/2));
|
|
361
|
+
|
|
362
|
+
sim.on('tick', () => {{
|
|
363
|
+
link.attr('x1',d=>d.source.x).attr('y1',d=>d.source.y).attr('x2',d=>d.target.x).attr('y2',d=>d.target.y);
|
|
364
|
+
node.attr('transform', d => `translate(${{d.x}},${{d.y}})`);
|
|
365
|
+
}});
|
|
366
|
+
}}
|
|
367
|
+
|
|
368
|
+
function searchDepsNode(q) {{}}
|
|
369
|
+
function resetDeps() {{
|
|
370
|
+
const svg = d3.select('#depsContainer svg');
|
|
371
|
+
if (svg.node()) svg.transition().call(d3.zoom().transform, d3.zoomIdentity);
|
|
372
|
+
}}
|
|
373
|
+
"""
|