engin 0.1.0rc1__py3-none-any.whl → 0.1.0rc3__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.
- engin/_assembler.py +5 -7
- engin/_cli/_check.py +2 -17
- engin/_cli/_graph.html +878 -73
- engin/_cli/_graph.py +122 -63
- engin/_cli/_inspect.py +6 -3
- engin/_engin.py +6 -3
- engin/_supervisor.py +18 -4
- engin/exceptions.py +21 -6
- {engin-0.1.0rc1.dist-info → engin-0.1.0rc3.dist-info}/METADATA +28 -15
- {engin-0.1.0rc1.dist-info → engin-0.1.0rc3.dist-info}/RECORD +13 -13
- {engin-0.1.0rc1.dist-info → engin-0.1.0rc3.dist-info}/WHEEL +0 -0
- {engin-0.1.0rc1.dist-info → engin-0.1.0rc3.dist-info}/entry_points.txt +0 -0
- {engin-0.1.0rc1.dist-info → engin-0.1.0rc3.dist-info}/licenses/LICENSE +0 -0
engin/_cli/_graph.html
CHANGED
@@ -1,78 +1,883 @@
|
|
1
1
|
<!doctype html>
|
2
2
|
<html lang="en">
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
3
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
4
|
+
<head>
|
5
|
+
<title>Engin Dependency Graph</title>
|
6
|
+
<style>
|
7
|
+
body {
|
8
|
+
font-family: Arial, sans-serif;
|
9
|
+
margin: 0;
|
10
|
+
padding: 0;
|
11
|
+
background-color: #f5f5f5;
|
12
|
+
}
|
13
|
+
|
14
|
+
.controls {
|
15
|
+
background: white;
|
16
|
+
padding: 15px;
|
17
|
+
border-bottom: 2px solid #ddd;
|
18
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
19
|
+
display: flex;
|
20
|
+
gap: 20px;
|
21
|
+
align-items: center;
|
22
|
+
flex-wrap: wrap;
|
23
|
+
}
|
24
|
+
|
25
|
+
.control-group {
|
26
|
+
display: flex;
|
27
|
+
align-items: center;
|
28
|
+
gap: 8px;
|
29
|
+
}
|
30
|
+
|
31
|
+
.control-group label {
|
32
|
+
font-weight: bold;
|
33
|
+
color: #333;
|
34
|
+
}
|
35
|
+
|
36
|
+
.control-group input[type="checkbox"] {
|
37
|
+
transform: scale(1.2);
|
38
|
+
}
|
39
|
+
|
40
|
+
.control-group button {
|
41
|
+
padding: 8px 16px;
|
42
|
+
background: #007acc;
|
43
|
+
color: white;
|
44
|
+
border: none;
|
45
|
+
border-radius: 4px;
|
46
|
+
cursor: pointer;
|
47
|
+
font-size: 14px;
|
48
|
+
}
|
49
|
+
|
50
|
+
.control-group button:hover {
|
51
|
+
background: #005c99;
|
52
|
+
}
|
53
|
+
|
54
|
+
.stats {
|
55
|
+
margin-left: auto;
|
56
|
+
color: #666;
|
57
|
+
font-size: 14px;
|
58
|
+
}
|
59
|
+
|
60
|
+
#mermaid-container {
|
61
|
+
width: 100%;
|
62
|
+
height: calc(100vh - 200px);
|
63
|
+
overflow: auto;
|
64
|
+
background: white;
|
65
|
+
cursor: grab;
|
66
|
+
position: relative;
|
67
|
+
}
|
68
|
+
|
69
|
+
#mermaid-container:active {
|
70
|
+
cursor: grabbing;
|
71
|
+
}
|
72
|
+
|
73
|
+
#mermaid-content {
|
74
|
+
width: max-content;
|
75
|
+
height: max-content;
|
76
|
+
min-width: 100%;
|
77
|
+
min-height: 100%;
|
78
|
+
}
|
79
|
+
|
80
|
+
.legend-container {
|
81
|
+
background: white;
|
82
|
+
padding: 15px;
|
83
|
+
border-top: 1px solid #ddd;
|
84
|
+
overflow-x: auto;
|
85
|
+
}
|
86
|
+
|
87
|
+
.legend-container h3 {
|
88
|
+
margin: 0 0 10px 0;
|
89
|
+
color: #333;
|
90
|
+
}
|
91
|
+
|
92
|
+
.loading {
|
93
|
+
display: flex;
|
94
|
+
justify-content: center;
|
95
|
+
align-items: center;
|
96
|
+
height: 200px;
|
97
|
+
font-size: 18px;
|
98
|
+
color: #666;
|
99
|
+
}
|
100
|
+
|
101
|
+
.hidden {
|
102
|
+
display: none;
|
103
|
+
}
|
104
|
+
|
105
|
+
.error {
|
106
|
+
color: red;
|
107
|
+
background: #ffebee;
|
108
|
+
padding: 10px;
|
109
|
+
border-radius: 4px;
|
110
|
+
margin: 10px;
|
111
|
+
}
|
112
|
+
|
113
|
+
/* Tooltip styles */
|
114
|
+
.node-tooltip {
|
115
|
+
position: absolute;
|
116
|
+
background: rgba(0, 0, 0, 0.9);
|
117
|
+
color: white;
|
118
|
+
padding: 12px;
|
119
|
+
border-radius: 6px;
|
120
|
+
font-size: 12px;
|
121
|
+
max-width: 300px;
|
122
|
+
z-index: 1000;
|
123
|
+
pointer-events: none;
|
124
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
125
|
+
border: 1px solid #555;
|
126
|
+
line-height: 1.4;
|
127
|
+
display: none;
|
128
|
+
}
|
129
|
+
|
130
|
+
.tooltip-header {
|
131
|
+
font-weight: bold;
|
132
|
+
font-size: 13px;
|
133
|
+
margin-bottom: 8px;
|
134
|
+
padding-bottom: 4px;
|
135
|
+
border-bottom: 1px solid #555;
|
136
|
+
color: #87ceeb;
|
137
|
+
}
|
138
|
+
|
139
|
+
.tooltip-section {
|
140
|
+
margin: 6px 0;
|
141
|
+
}
|
142
|
+
|
143
|
+
.tooltip-label {
|
144
|
+
font-weight: bold;
|
145
|
+
color: #ffd700;
|
146
|
+
display: inline-block;
|
147
|
+
min-width: 80px;
|
148
|
+
}
|
149
|
+
|
150
|
+
.tooltip-value {
|
151
|
+
color: #e0e0e0;
|
152
|
+
}
|
153
|
+
|
154
|
+
.tooltip-list {
|
155
|
+
margin: 4px 0 4px 16px;
|
156
|
+
padding: 0;
|
157
|
+
}
|
158
|
+
|
159
|
+
.tooltip-list li {
|
160
|
+
list-style: none;
|
161
|
+
margin: 2px 0;
|
162
|
+
color: #ccc;
|
163
|
+
}
|
164
|
+
|
165
|
+
.tooltip-list li:before {
|
166
|
+
content: "• ";
|
167
|
+
color: #87ceeb;
|
168
|
+
margin-right: 4px;
|
169
|
+
}
|
170
|
+
</style>
|
171
|
+
</head>
|
172
|
+
|
173
|
+
<body>
|
174
|
+
<div class="controls">
|
175
|
+
<div class="control-group">
|
176
|
+
<label>
|
177
|
+
<input type="checkbox" id="show-external" checked>
|
178
|
+
Show External Dependencies
|
179
|
+
</label>
|
66
180
|
</div>
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
181
|
+
|
182
|
+
<div class="control-group">
|
183
|
+
<label>
|
184
|
+
<input type="checkbox" id="show-blocks" checked>
|
185
|
+
Group Block Dependencies
|
186
|
+
</label>
|
187
|
+
</div>
|
188
|
+
|
189
|
+
<div class="control-group">
|
190
|
+
<label>Layout:</label>
|
191
|
+
<select id="layout-select">
|
192
|
+
<option value="dagre" selected>Dagre (Default)</option>
|
193
|
+
<option value="elk">ELK</option>
|
194
|
+
<option value="klay">Klay</option>
|
195
|
+
</select>
|
196
|
+
</div>
|
197
|
+
|
198
|
+
<div class="control-group">
|
199
|
+
<label>Theme:</label>
|
200
|
+
<select id="theme-select">
|
201
|
+
<option value="default" selected>Default</option>
|
202
|
+
<option value="dark">Dark</option>
|
203
|
+
<option value="forest">Forest</option>
|
204
|
+
<option value="base">Base</option>
|
205
|
+
<option value="neutral">Neutral</option>
|
206
|
+
</select>
|
207
|
+
</div>
|
208
|
+
|
209
|
+
<div class="control-group">
|
210
|
+
<button id="fit-view">Fit to View</button>
|
211
|
+
<button id="refresh-graph">Refresh</button>
|
212
|
+
</div>
|
213
|
+
|
214
|
+
<div class="stats">
|
215
|
+
<span id="node-count">0 nodes</span>
|
216
|
+
<span id="edge-count">0 edges</span>
|
217
|
+
</div>
|
218
|
+
</div>
|
219
|
+
|
220
|
+
<div id="mermaid-container">
|
221
|
+
<div class="loading" id="loading">
|
222
|
+
<div>Loading dependency graph...</div>
|
223
|
+
</div>
|
224
|
+
<div id="mermaid-content">
|
225
|
+
<div id="graph"></div>
|
226
|
+
</div>
|
227
|
+
</div>
|
228
|
+
|
229
|
+
<div class="legend-container">
|
230
|
+
<h3>Legend</h3>
|
231
|
+
<div id="legend-content">
|
232
|
+
<div id="legend"></div>
|
76
233
|
</div>
|
77
|
-
|
234
|
+
</div>
|
235
|
+
|
236
|
+
<div id="node-tooltip" class="node-tooltip"></div>
|
237
|
+
|
238
|
+
<script type="application/json" id="graph-data">
|
239
|
+
%%GRAPH_DATA%%
|
240
|
+
</script>
|
241
|
+
|
242
|
+
<script type="module">
|
243
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
244
|
+
|
245
|
+
let graphData = null;
|
246
|
+
let currentConfig = {
|
247
|
+
showExternal: true,
|
248
|
+
showBlocks: true,
|
249
|
+
layout: 'dagre',
|
250
|
+
theme: 'default',
|
251
|
+
};
|
252
|
+
|
253
|
+
// Initialize mermaid (will be re-initialized with proper config later)
|
254
|
+
mermaid.initialize({
|
255
|
+
startOnLoad: false,
|
256
|
+
flowchart: {
|
257
|
+
useMaxWidth: true,
|
258
|
+
htmlLabels: true,
|
259
|
+
defaultRenderer: 'dagre'
|
260
|
+
},
|
261
|
+
theme: 'default'
|
262
|
+
});
|
263
|
+
|
264
|
+
// Load graph data
|
265
|
+
function loadGraphData() {
|
266
|
+
const dataScript = document.getElementById('graph-data');
|
267
|
+
try {
|
268
|
+
graphData = JSON.parse(dataScript.textContent);
|
269
|
+
return true;
|
270
|
+
} catch (e) {
|
271
|
+
console.error('Failed to parse graph data:', e);
|
272
|
+
showError('Error loading graph data: ' + e.message);
|
273
|
+
return false;
|
274
|
+
}
|
275
|
+
}
|
276
|
+
|
277
|
+
// Show error message
|
278
|
+
function showError(message) {
|
279
|
+
const loading = document.getElementById('loading');
|
280
|
+
loading.innerHTML = `<div class="error">${message}</div>`;
|
281
|
+
loading.classList.remove('hidden');
|
282
|
+
}
|
283
|
+
|
284
|
+
// Generate mermaid syntax from filtered data
|
285
|
+
function generateMermaidSyntax() {
|
286
|
+
if (!graphData) return '';
|
287
|
+
|
288
|
+
const {nodes, edges, blocks} = graphData;
|
289
|
+
|
290
|
+
// Filter nodes based on current settings
|
291
|
+
let filteredNodes = nodes;
|
292
|
+
if (!currentConfig.showExternal) {
|
293
|
+
filteredNodes = nodes.filter(node => !node.external);
|
294
|
+
}
|
295
|
+
|
296
|
+
// Create a set of visible node IDs for edge filtering
|
297
|
+
const visibleNodeIds = new Set(filteredNodes.map(node => node.id));
|
298
|
+
|
299
|
+
// Filter edges to only include those between visible nodes
|
300
|
+
const filteredEdges = edges.filter(edge =>
|
301
|
+
visibleNodeIds.has(edge.from) && visibleNodeIds.has(edge.to)
|
302
|
+
);
|
303
|
+
|
304
|
+
// Generate mermaid syntax
|
305
|
+
let mermaidSyntax = `%%{init: {"flowchart": {"defaultRenderer": "${currentConfig.layout}"}} }%%\n`;
|
306
|
+
mermaidSyntax += 'graph LR\n';
|
307
|
+
|
308
|
+
if (currentConfig.showBlocks && blocks.length > 0) {
|
309
|
+
// GROUPED MODE: Show blocks as subgraphs
|
310
|
+
|
311
|
+
// Add edges that originate or end outside of blocks or go between blocks
|
312
|
+
const mainEdges = filteredEdges.filter(edge => (!edge.from_block || !edge.to_block || edge.to_block !== edge.from_block));
|
313
|
+
for (const edge of mainEdges) {
|
314
|
+
const fromNode = nodes.find(n => n.id === edge.from);
|
315
|
+
const toNode = nodes.find(n => n.id === edge.to);
|
316
|
+
if (fromNode && toNode) {
|
317
|
+
mermaidSyntax += ` ${renderNode(fromNode)} --> ${renderNode(toNode)}\n`;
|
318
|
+
}
|
319
|
+
}
|
320
|
+
|
321
|
+
// Add block subgraphs
|
322
|
+
for (const block of blocks) {
|
323
|
+
const blockNodes = filteredNodes.filter(node => node.block === block);
|
324
|
+
if (blockNodes.length > 1) {
|
325
|
+
mermaidSyntax += ` subgraph ${block.replace(/[^a-zA-Z0-9]/g, '_')}\n`;
|
326
|
+
|
327
|
+
// Add edges within this block
|
328
|
+
const blockEdges = filteredEdges.filter(edge => {
|
329
|
+
const fromNode = nodes.find(n => n.id === edge.from);
|
330
|
+
const toNode = nodes.find(n => n.id === edge.to);
|
331
|
+
return fromNode?.block === block && toNode?.block === block;
|
332
|
+
});
|
333
|
+
|
334
|
+
for (const edge of blockEdges) {
|
335
|
+
const fromNode = nodes.find(n => n.id === edge.from);
|
336
|
+
const toNode = nodes.find(n => n.id === edge.to);
|
337
|
+
|
338
|
+
if (fromNode && toNode) {
|
339
|
+
mermaidSyntax += ` ${renderNode(fromNode, false)} --> ${renderNode(toNode, false)}\n`;
|
340
|
+
}
|
341
|
+
}
|
342
|
+
|
343
|
+
mermaidSyntax += ' end\n';
|
344
|
+
} else if (blockNodes.length === 1) {
|
345
|
+
mermaidSyntax += ` subgraph ${block.replace(/[^a-zA-Z0-9]/g, '_')}\n`;
|
346
|
+
mermaidSyntax += ` ${renderNode(blockNodes[0], false)}\n`
|
347
|
+
mermaidSyntax += ' end\n';
|
348
|
+
}
|
349
|
+
}
|
350
|
+
} else {
|
351
|
+
// FLAT MODE: Show all edges as regular connections without block grouping
|
352
|
+
for (const edge of filteredEdges) {
|
353
|
+
const fromNode = nodes.find(n => n.id === edge.from);
|
354
|
+
const toNode = nodes.find(n => n.id === edge.to);
|
355
|
+
if (fromNode && toNode) {
|
356
|
+
mermaidSyntax += ` ${renderNode(fromNode)} --> ${renderNode(toNode)}\n`;
|
357
|
+
}
|
358
|
+
}
|
359
|
+
}
|
360
|
+
|
361
|
+
// Add CSS classes
|
362
|
+
mermaidSyntax += ' classDef external stroke-dasharray: 5 5;\n';
|
363
|
+
mermaidSyntax += ' classDef b0 fill:#7fc97f;\n';
|
364
|
+
|
365
|
+
return mermaidSyntax;
|
366
|
+
}
|
367
|
+
|
368
|
+
// Render a single node in mermaid format
|
369
|
+
function renderNode(node, includeBlock = true) {
|
370
|
+
let label = '';
|
371
|
+
let shape;
|
372
|
+
let styleClasses = '';
|
373
|
+
|
374
|
+
if (includeBlock && node.block) {
|
375
|
+
label += `_${node.block}_<br/>`;
|
376
|
+
}
|
377
|
+
|
378
|
+
// Escape the label for mermaid
|
379
|
+
const escapedLabel = node.label.replace(/["`]/g, '');
|
380
|
+
label += escapedLabel;
|
381
|
+
|
382
|
+
switch (node.type) {
|
383
|
+
case 'Supply':
|
384
|
+
shape = `${node.id}("${label}")`;
|
385
|
+
break;
|
386
|
+
case 'Provide':
|
387
|
+
shape = `${node.id}["${label}"]`;
|
388
|
+
break;
|
389
|
+
case 'Entrypoint':
|
390
|
+
shape = `${node.id}[/"${label}"\\]`;
|
391
|
+
break;
|
392
|
+
case 'Invoke':
|
393
|
+
shape = `${node.id}[/"${label}"/]`;
|
394
|
+
break;
|
395
|
+
case 'APIRoute':
|
396
|
+
shape = `${node.id}[["${label}"]]`;
|
397
|
+
break;
|
398
|
+
default:
|
399
|
+
shape = `${node.id}["${label}"]`;
|
400
|
+
}
|
401
|
+
|
402
|
+
if (node.style_classes && node.style_classes.length > 0) {
|
403
|
+
styleClasses = `:::${node.style_classes.join(',')}`;
|
404
|
+
}
|
405
|
+
|
406
|
+
return shape + styleClasses;
|
407
|
+
}
|
408
|
+
|
409
|
+
// Update statistics
|
410
|
+
function updateStats() {
|
411
|
+
if (!graphData) return;
|
412
|
+
|
413
|
+
const {nodes, edges} = graphData;
|
414
|
+
let visibleNodes = nodes;
|
415
|
+
|
416
|
+
if (!currentConfig.showExternal) {
|
417
|
+
visibleNodes = nodes.filter(node => !node.external);
|
418
|
+
}
|
419
|
+
|
420
|
+
const visibleNodeIds = new Set(visibleNodes.map(node => node.id));
|
421
|
+
const visibleEdges = edges.filter(edge =>
|
422
|
+
visibleNodeIds.has(edge.from) && visibleNodeIds.has(edge.to)
|
423
|
+
);
|
424
|
+
|
425
|
+
document.getElementById('node-count').textContent = `${visibleNodes.length} nodes`;
|
426
|
+
document.getElementById('edge-count').textContent = `${visibleEdges.length} edges`;
|
427
|
+
}
|
428
|
+
|
429
|
+
// Tooltip functionality
|
430
|
+
function createTooltipContent(nodeData) {
|
431
|
+
const {type, label, details, block, external} = nodeData;
|
432
|
+
|
433
|
+
let content = `<div class="tooltip-header">${type}: ${label}</div>`;
|
434
|
+
|
435
|
+
// Basic information
|
436
|
+
content += `<div class="tooltip-section">`;
|
437
|
+
content += `<span class="tooltip-label">Module:</span> <span class="tooltip-value">${details.source_module}</span><br>`;
|
438
|
+
content += `<span class="tooltip-label">Package:</span> <span class="tooltip-value">${details.source_package}</span><br>`;
|
439
|
+
if (external) {
|
440
|
+
content += `<span class="tooltip-label">External:</span> <span class="tooltip-value">Yes</span><br>`;
|
441
|
+
}
|
442
|
+
if (block) {
|
443
|
+
content += `<span class="tooltip-label">Block:</span> <span class="tooltip-value">${block}</span><br>`;
|
444
|
+
}
|
445
|
+
content += `</div>`;
|
446
|
+
|
447
|
+
// Type-specific information
|
448
|
+
if (details.return_type) {
|
449
|
+
content += `<div class="tooltip-section">`;
|
450
|
+
content += `<span class="tooltip-label">Returns:</span> <span class="tooltip-value">${details.return_type}</span><br>`;
|
451
|
+
if (details.value_type) {
|
452
|
+
content += `<span class="tooltip-label">Value Type:</span> <span class="tooltip-value">${details.value_type}</span><br>`;
|
453
|
+
}
|
454
|
+
if (details.factory_function) {
|
455
|
+
content += `<span class="tooltip-label">Factory:</span> <span class="tooltip-value">${details.factory_function}</span><br>`;
|
456
|
+
}
|
457
|
+
if (details.scope) {
|
458
|
+
content += `<span class="tooltip-label">Scope:</span> <span class="tooltip-value">${details.scope}</span><br>`;
|
459
|
+
}
|
460
|
+
if (details.multiprovider) {
|
461
|
+
content += `<span class="tooltip-label">Multi:</span> <span class="tooltip-value">Yes</span><br>`;
|
462
|
+
}
|
463
|
+
content += `</div>`;
|
464
|
+
}
|
465
|
+
|
466
|
+
if (details.function) {
|
467
|
+
content += `<div class="tooltip-section">`;
|
468
|
+
content += `<span class="tooltip-label">Function:</span> <span class="tooltip-value">${details.function}</span><br>`;
|
469
|
+
content += `</div>`;
|
470
|
+
}
|
471
|
+
|
472
|
+
if (details.entrypoint_type) {
|
473
|
+
content += `<div class="tooltip-section">`;
|
474
|
+
content += `<span class="tooltip-label">Entry Type:</span> <span class="tooltip-value">${details.entrypoint_type}</span><br>`;
|
475
|
+
content += `</div>`;
|
476
|
+
}
|
477
|
+
|
478
|
+
if (details.methods && details.methods.length > 0) {
|
479
|
+
content += `<div class="tooltip-section">`;
|
480
|
+
content += `<span class="tooltip-label">Methods:</span> <span class="tooltip-value">${details.methods.join(', ')}</span><br>`;
|
481
|
+
if (details.path) {
|
482
|
+
content += `<span class="tooltip-label">Path:</span> <span class="tooltip-value">${details.path}</span><br>`;
|
483
|
+
}
|
484
|
+
content += `</div>`;
|
485
|
+
}
|
486
|
+
|
487
|
+
return content;
|
488
|
+
}
|
489
|
+
|
490
|
+
function showTooltip(event, nodeId) {
|
491
|
+
const tooltip = document.getElementById('node-tooltip');
|
492
|
+
if (!tooltip) return;
|
493
|
+
|
494
|
+
const nodeData = graphData.nodes.find(n => n.id === nodeId);
|
495
|
+
if (!nodeData) return;
|
496
|
+
|
497
|
+
tooltip.innerHTML = createTooltipContent(nodeData);
|
498
|
+
tooltip.style.display = 'block';
|
499
|
+
|
500
|
+
// Position tooltip near mouse
|
501
|
+
const rect = document.getElementById('mermaid-container').getBoundingClientRect();
|
502
|
+
let x = event.clientX - rect.left + 15;
|
503
|
+
let y = event.clientY - rect.top + 15;
|
504
|
+
|
505
|
+
// Ensure tooltip is always visible on screen
|
506
|
+
const minX = 10;
|
507
|
+
const minY = 10;
|
508
|
+
const maxX = rect.width - 320;
|
509
|
+
const maxY = rect.height - 200;
|
510
|
+
|
511
|
+
x = Math.max(minX, Math.min(x, maxX));
|
512
|
+
y = Math.max(minY, Math.min(y, maxY));
|
513
|
+
|
514
|
+
tooltip.style.left = `${x}px`;
|
515
|
+
tooltip.style.top = `${y}px`;
|
516
|
+
};
|
517
|
+
|
518
|
+
function hideTooltip() {
|
519
|
+
const tooltip = document.getElementById('node-tooltip');
|
520
|
+
tooltip.style.display = 'none';
|
521
|
+
}
|
522
|
+
|
523
|
+
// Helper function to extract node ID from flowchart format
|
524
|
+
function extractNodeId(elementId) {
|
525
|
+
if (!elementId) return null;
|
526
|
+
|
527
|
+
// Handle direct format: n1234567890
|
528
|
+
if (/^n\d+$/.test(elementId)) {
|
529
|
+
return elementId;
|
530
|
+
}
|
531
|
+
|
532
|
+
// Handle flowchart format: flowchart-n1234567890-0
|
533
|
+
const flowchartMatch = elementId.match(/^flowchart-(n\d+)-\d+$/);
|
534
|
+
if (flowchartMatch) {
|
535
|
+
return flowchartMatch[1];
|
536
|
+
}
|
537
|
+
|
538
|
+
return null;
|
539
|
+
}
|
540
|
+
|
541
|
+
function attachTooltipListeners() {
|
542
|
+
setTimeout(() => {
|
543
|
+
const svgElement = document.querySelector('#graph svg');
|
544
|
+
if (!svgElement) return;
|
545
|
+
|
546
|
+
// Find all node group elements
|
547
|
+
const nodeGroups = svgElement.querySelectorAll('g.node');
|
548
|
+
|
549
|
+
nodeGroups.forEach(group => {
|
550
|
+
// Extract node ID from flowchart format: flowchart-n1234567890-0 → n1234567890
|
551
|
+
const flowchartMatch = group.id.match(/^flowchart-(n\d+)-\d+$/);
|
552
|
+
if (flowchartMatch) {
|
553
|
+
const nodeId = flowchartMatch[1];
|
554
|
+
|
555
|
+
if (!group.hasAttribute('data-tooltip-attached')) {
|
556
|
+
group.setAttribute('data-tooltip-attached', 'true');
|
557
|
+
group.style.cursor = 'pointer';
|
558
|
+
|
559
|
+
group.addEventListener('mouseenter', (event) => {
|
560
|
+
showTooltip(event, nodeId);
|
561
|
+
});
|
562
|
+
|
563
|
+
group.addEventListener('mouseleave', () => {
|
564
|
+
hideTooltip();
|
565
|
+
});
|
566
|
+
|
567
|
+
group.addEventListener('mousemove', (event) => {
|
568
|
+
if (document.getElementById('node-tooltip').style.display === 'block') {
|
569
|
+
showTooltip(event, nodeId);
|
570
|
+
}
|
571
|
+
});
|
572
|
+
}
|
573
|
+
}
|
574
|
+
});
|
575
|
+
}, 300);
|
576
|
+
};
|
577
|
+
|
578
|
+
// Render the graph
|
579
|
+
async function renderGraph() {
|
580
|
+
if (!graphData) return;
|
581
|
+
|
582
|
+
const loading = document.getElementById('loading');
|
583
|
+
const content = document.getElementById('mermaid-content');
|
584
|
+
const graphElement = document.getElementById('graph');
|
585
|
+
|
586
|
+
loading.classList.remove('hidden');
|
587
|
+
|
588
|
+
try {
|
589
|
+
graphElement.textContent = generateMermaidSyntax();
|
590
|
+
graphElement.removeAttribute('data-processed');
|
591
|
+
|
592
|
+
await mermaid.run({
|
593
|
+
querySelector: '#graph'
|
594
|
+
});
|
595
|
+
|
596
|
+
updateStats();
|
597
|
+
attachTooltipListeners();
|
598
|
+
|
599
|
+
loading.classList.add('hidden');
|
600
|
+
content.classList.remove('hidden');
|
601
|
+
|
602
|
+
} catch (error) {
|
603
|
+
console.error('Error rendering graph:', error);
|
604
|
+
showError('Error rendering graph: ' + error.message);
|
605
|
+
}
|
606
|
+
}
|
607
|
+
|
608
|
+
// Render legend
|
609
|
+
async function renderLegend() {
|
610
|
+
if (!graphData || !graphData.legend) return;
|
611
|
+
|
612
|
+
const legendElement = document.getElementById('legend');
|
613
|
+
legendElement.textContent = `graph LR\n ${graphData.legend}\n classDef b0 fill:#7fc97f;\n classDef external stroke-dasharray: 5 5;`;
|
614
|
+
legendElement.removeAttribute('data-processed');
|
615
|
+
|
616
|
+
try {
|
617
|
+
await mermaid.run({
|
618
|
+
querySelector: '#legend'
|
619
|
+
});
|
620
|
+
} catch (error) {
|
621
|
+
console.error('Error rendering legend:', error);
|
622
|
+
}
|
623
|
+
}
|
624
|
+
|
625
|
+
function handleContainerMouseOver(event) {
|
626
|
+
const target = event.target;
|
627
|
+
|
628
|
+
// Only process events within the SVG area
|
629
|
+
const svgElement = document.querySelector('#graph svg');
|
630
|
+
if (!svgElement || !svgElement.contains(target)) {
|
631
|
+
return;
|
632
|
+
}
|
633
|
+
|
634
|
+
// Check if the target or its parent might be a mermaid node
|
635
|
+
let nodeElement = target;
|
636
|
+
let nodeId = null;
|
637
|
+
let attempts = 0;
|
638
|
+
|
639
|
+
// Traverse up the DOM tree to find a node with an ID starting with 'n' followed by digits
|
640
|
+
while (nodeElement && attempts < 5) {
|
641
|
+
// More specific check: ID starts with 'n' followed by digits (actual node IDs)
|
642
|
+
if (nodeElement.id && /^n\d+$/.test(nodeElement.id)) {
|
643
|
+
nodeId = nodeElement.id;
|
644
|
+
break;
|
645
|
+
}
|
646
|
+
|
647
|
+
// Check if this element has a child with a valid node ID
|
648
|
+
if (nodeElement.querySelector) {
|
649
|
+
const childWithId = nodeElement.querySelector('[id]');
|
650
|
+
if (childWithId && childWithId.id && /^n\d+$/.test(childWithId.id)) {
|
651
|
+
nodeId = childWithId.id;
|
652
|
+
break;
|
653
|
+
}
|
654
|
+
}
|
655
|
+
|
656
|
+
nodeElement = nodeElement.parentElement;
|
657
|
+
attempts++;
|
658
|
+
}
|
659
|
+
|
660
|
+
if (nodeId) {
|
661
|
+
console.log(`Alternative method found node: ${nodeId}`);
|
662
|
+
showTooltip(event, nodeId);
|
663
|
+
}
|
664
|
+
}
|
665
|
+
|
666
|
+
function handleContainerMouseOut(event) {
|
667
|
+
// Only hide if we're actually leaving the container area
|
668
|
+
const container = document.getElementById('mermaid-container');
|
669
|
+
if (!event.relatedTarget || !container.contains(event.relatedTarget)) {
|
670
|
+
hideTooltip();
|
671
|
+
}
|
672
|
+
}
|
673
|
+
|
674
|
+
// Setup drag functionality
|
675
|
+
function setupDragFunctionality() {
|
676
|
+
const container = document.getElementById("mermaid-container");
|
677
|
+
let isDragging = false;
|
678
|
+
let startX, startY, scrollLeft, scrollTop;
|
679
|
+
|
680
|
+
container.addEventListener("pointerdown", (e) => {
|
681
|
+
isDragging = true;
|
682
|
+
startX = e.clientX;
|
683
|
+
startY = e.clientY;
|
684
|
+
scrollLeft = container.scrollLeft;
|
685
|
+
scrollTop = container.scrollTop;
|
686
|
+
container.style.cursor = "grabbing";
|
687
|
+
// Hide tooltip when starting to drag
|
688
|
+
hideTooltip();
|
689
|
+
});
|
690
|
+
|
691
|
+
container.addEventListener("pointermove", (e) => {
|
692
|
+
if (!isDragging) return;
|
693
|
+
const x = e.clientX - startX;
|
694
|
+
const y = e.clientY - startY;
|
695
|
+
container.scrollLeft = scrollLeft - x;
|
696
|
+
container.scrollTop = scrollTop - y;
|
697
|
+
});
|
698
|
+
|
699
|
+
container.addEventListener("pointerup", () => {
|
700
|
+
isDragging = false;
|
701
|
+
container.style.cursor = "grab";
|
702
|
+
});
|
703
|
+
|
704
|
+
container.addEventListener("pointerleave", () => {
|
705
|
+
isDragging = false;
|
706
|
+
container.style.cursor = "grab";
|
707
|
+
hideTooltip();
|
708
|
+
});
|
709
|
+
|
710
|
+
container.addEventListener("scroll", () => {
|
711
|
+
hideTooltip();
|
712
|
+
});
|
713
|
+
|
714
|
+
document.addEventListener("click", (e) => {
|
715
|
+
if (!container.contains(e.target)) {
|
716
|
+
hideTooltip();
|
717
|
+
}
|
718
|
+
});
|
719
|
+
}
|
720
|
+
|
721
|
+
// Sync config with DOM elements
|
722
|
+
function syncConfigWithDOM() {
|
723
|
+
const showExternalEl = document.getElementById('show-external');
|
724
|
+
const showBlocksEl = document.getElementById('show-blocks');
|
725
|
+
const layoutSelectEl = document.getElementById('layout-select');
|
726
|
+
const themeSelectEl = document.getElementById('theme-select');
|
727
|
+
|
728
|
+
// Set DOM values to match config
|
729
|
+
showExternalEl.checked = currentConfig.showExternal;
|
730
|
+
showBlocksEl.checked = currentConfig.showBlocks;
|
731
|
+
layoutSelectEl.value = currentConfig.layout;
|
732
|
+
themeSelectEl.value = currentConfig.theme;
|
733
|
+
}
|
734
|
+
|
735
|
+
function setupEventListeners() {
|
736
|
+
document.getElementById('show-external').addEventListener('change', (e) => {
|
737
|
+
currentConfig.showExternal = e.target.checked;
|
738
|
+
renderGraph();
|
739
|
+
});
|
740
|
+
|
741
|
+
document.getElementById('show-blocks').addEventListener('change', (e) => {
|
742
|
+
currentConfig.showBlocks = e.target.checked;
|
743
|
+
renderGraph();
|
744
|
+
});
|
745
|
+
|
746
|
+
document.getElementById('layout-select').addEventListener('change', async (e) => {
|
747
|
+
currentConfig.layout = e.target.value;
|
748
|
+
|
749
|
+
// Reinitialize mermaid with new layout
|
750
|
+
mermaid.initialize({
|
751
|
+
startOnLoad: false,
|
752
|
+
flowchart: {
|
753
|
+
useMaxWidth: true,
|
754
|
+
htmlLabels: true,
|
755
|
+
defaultRenderer: e.target.value
|
756
|
+
},
|
757
|
+
theme: currentConfig.theme
|
758
|
+
});
|
759
|
+
|
760
|
+
// Clear the graph and force re-render
|
761
|
+
const graphElement = document.getElementById('graph');
|
762
|
+
graphElement.innerHTML = '';
|
763
|
+
graphElement.removeAttribute('data-processed');
|
764
|
+
|
765
|
+
await renderGraph();
|
766
|
+
});
|
767
|
+
|
768
|
+
document.getElementById('theme-select').addEventListener('change', async (e) => {
|
769
|
+
currentConfig.theme = e.target.value;
|
770
|
+
|
771
|
+
// Reinitialize mermaid with new theme
|
772
|
+
mermaid.initialize({
|
773
|
+
startOnLoad: false,
|
774
|
+
flowchart: {
|
775
|
+
useMaxWidth: true,
|
776
|
+
htmlLabels: true,
|
777
|
+
defaultRenderer: currentConfig.layout
|
778
|
+
},
|
779
|
+
theme: e.target.value
|
780
|
+
});
|
781
|
+
|
782
|
+
// Clear both graph and legend and force re-render
|
783
|
+
const graphElement = document.getElementById('graph');
|
784
|
+
const legendElement = document.getElementById('legend');
|
785
|
+
|
786
|
+
graphElement.innerHTML = '';
|
787
|
+
graphElement.removeAttribute('data-processed');
|
788
|
+
legendElement.innerHTML = '';
|
789
|
+
legendElement.removeAttribute('data-processed');
|
790
|
+
|
791
|
+
await renderGraph();
|
792
|
+
await renderLegend();
|
793
|
+
});
|
794
|
+
|
795
|
+
document.getElementById('fit-view').addEventListener('click', () => {
|
796
|
+
const container = document.getElementById('mermaid-container');
|
797
|
+
container.scrollTo({top: 0, left: 0, behavior: 'smooth'});
|
798
|
+
});
|
799
|
+
|
800
|
+
document.getElementById('refresh-graph').addEventListener('click', () => {
|
801
|
+
renderGraph();
|
802
|
+
});
|
803
|
+
}
|
804
|
+
|
805
|
+
// Initialize everything
|
806
|
+
async function init() {
|
807
|
+
try {
|
808
|
+
if (!loadGraphData()) {
|
809
|
+
return;
|
810
|
+
}
|
811
|
+
|
812
|
+
syncConfigWithDOM();
|
813
|
+
setupEventListeners();
|
814
|
+
setupDragFunctionality();
|
815
|
+
|
816
|
+
// Initialize mermaid with current config
|
817
|
+
mermaid.initialize({
|
818
|
+
startOnLoad: false,
|
819
|
+
flowchart: {
|
820
|
+
useMaxWidth: true,
|
821
|
+
htmlLabels: true,
|
822
|
+
defaultRenderer: currentConfig.layout
|
823
|
+
},
|
824
|
+
theme: currentConfig.theme
|
825
|
+
});
|
826
|
+
|
827
|
+
// Initial render
|
828
|
+
await renderGraph();
|
829
|
+
await renderLegend();
|
830
|
+
|
831
|
+
} catch (error) {
|
832
|
+
console.error('Initialization error:', error);
|
833
|
+
showError('Initialization error: ' + error.message);
|
834
|
+
}
|
835
|
+
}
|
836
|
+
|
837
|
+
// Debug function to inspect SVG structure
|
838
|
+
window.debugTooltips = function () {
|
839
|
+
const svgElement = document.querySelector('#graph svg');
|
840
|
+
if (!svgElement) {
|
841
|
+
console.log('No SVG found');
|
842
|
+
return;
|
843
|
+
}
|
844
|
+
|
845
|
+
console.log('=== SVG STRUCTURE DEBUG ===');
|
846
|
+
|
847
|
+
// Check all elements with IDs
|
848
|
+
const allElementsWithIds = svgElement.querySelectorAll('[id]');
|
849
|
+
console.log(`Found ${allElementsWithIds.length} elements with IDs:`);
|
850
|
+
allElementsWithIds.forEach(el => {
|
851
|
+
console.log(`- ${el.tagName}: "${el.id}" (classes: ${el.className.baseVal || el.className})`);
|
852
|
+
});
|
853
|
+
|
854
|
+
// Check g.node elements specifically
|
855
|
+
const nodeGroups = svgElement.querySelectorAll('g.node');
|
856
|
+
console.log(`\nFound ${nodeGroups.length} g.node elements:`);
|
857
|
+
nodeGroups.forEach((group, i) => {
|
858
|
+
console.log(`Node ${i}:`);
|
859
|
+
console.log(` Group ID: "${group.id}"`);
|
860
|
+
console.log(` Group classes: ${group.className.baseVal}`);
|
861
|
+
|
862
|
+
const children = Array.from(group.children);
|
863
|
+
children.forEach(child => {
|
864
|
+
console.log(` Child: ${child.tagName} ID:"${child.id}" classes:"${child.className.baseVal || child.className}"`);
|
865
|
+
});
|
866
|
+
});
|
867
|
+
|
868
|
+
// Test the regex pattern
|
869
|
+
console.log('\n=== PATTERN TEST ===');
|
870
|
+
allElementsWithIds.forEach(el => {
|
871
|
+
if (el.id) {
|
872
|
+
const matches = /^n\d+$/.test(el.id);
|
873
|
+
console.log(`ID "${el.id}" matches pattern: ${matches}`);
|
874
|
+
}
|
875
|
+
});
|
876
|
+
};
|
877
|
+
|
878
|
+
// Start when DOM is ready
|
879
|
+
document.addEventListener('DOMContentLoaded', init);
|
880
|
+
</script>
|
881
|
+
</body>
|
882
|
+
|
78
883
|
</html>
|